1 from datetime import date
6 from django.conf import settings
7 from django.utils.html import escape, format_html
8 from django.utils.safestring import mark_safe
9 from librarian.builders.html import SnippetHtmlBuilder
10 from librarian.functions import lang_code_3to2
11 from catalogue.models import Audience, Author, Thema
13 from .base import BasePublisher
14 from .woblink_constants import WOBLINK_CATEGORIES
17 class WoblinkError(ValueError):
20 class NoPrice(WoblinkError):
23 'Brak <a href="/admin/depot/site/{site}">określonej ceny</a>.',
27 class NoIsbn(WoblinkError):
31 class AuthorLiteralForeign(WoblinkError):
34 'Nie obsługiwane: autor „{author}” w języku {lang}.',
35 author=str(self.args[0]),
36 lang=self.args[0].lang,
39 class AuthorNotInCatalogue(WoblinkError):
42 'Brak autora „{author}” w katalogu.',
43 author=str(self.args[0])
46 class AuthorNoWoblink(WoblinkError):
49 'Autor <a href="/admin/catalogue/author/{author_id}/">{author}</a> bez identyfikatora Woblink.',
50 author_id=self.args[0].id,
51 author=self.args[0].name
54 class NoThema(WoblinkError):
56 return format_html('Brak Thema.')
58 class UnknownThema(WoblinkError):
61 'Nieznana Thema {code}.',
66 class ThemaUnknownWoblink(WoblinkError):
69 'Thema <a href="/admin/catalogue/thema/{id}/">{code}</a> przypisana do nieznanej kategorii Woblink.',
71 code=self.args[0].code,
74 class NoWoblinkCategory(WoblinkError):
76 return 'Brak kategorii Woblink.'
78 class WoblinkWarning(Warning):
81 class NoMainThemaWarning(WoblinkWarning):
84 'Brak głównej kategorii Thema.'
87 class ThemaNoWoblink(WoblinkWarning):
90 'Thema <a href="/admin/catalogue/thema/{id}/">{code}</a> nie przypisana do kategorii Woblink.',
92 code=self.args[0].code,
95 class AuthorLiteralForeignWarning(WoblinkWarning):
98 'Nie obsługiwane: autor „{author}” w języku {lang}.',
99 author=str(self.args[0]),
100 lang=self.args[0].lang,
103 class AuthorNotInCatalogueWarning(WoblinkWarning):
106 'Brak autora „{author}” w katalogu.',
107 author=str(self.args[0])
110 class AuthorNoWoblinkWarning(WoblinkWarning):
113 'Autor <a href="/admin/catalogue/author/{author_id}/">{author}</a> bez identyfikatora Woblink.',
114 author_id=self.args[0].id,
115 author=self.args[0].name
121 class Woblink(BasePublisher):
122 BASE_URL = 'https://publisher.woblink.com/'
123 ADD_URL = BASE_URL + 'catalog/add'
124 STEP1_URL = BASE_URL + 'catalog/edit/%s'
125 STEP2_URL = BASE_URL + 'catalog/edit/%s/2'
126 STEP3_URL = BASE_URL + 'catalog/edit/%s/3'
127 STEP4_URL = BASE_URL + 'catalog/edit/%s/4'
128 STEP5_URL = BASE_URL + 'catalog/edit/%s/5'
129 UPLOAD_URL = BASE_URL + 'file/upload-%s'
130 JOB_STATUS_URL = BASE_URL + 'task/status'
131 GENERATE_DEMO_URL = BASE_URL + 'task/run/generate-%s-demo/%s/%d'
132 CHECK_DEMO_URL = BASE_URL + 'task/run/check-%s-demo/%s'
134 SEARCH_CATALOGUE_URL = BASE_URL + '{category}/autocomplete/{term}'
140 response = self.session.get('https://publisher.woblink.com/login')
142 r'name="_csrf_token" value="([^"]+)"',
146 '_csrf_token': token,
147 '_username': self.username,
148 '_password': self.password,
150 response = self.session.post(
151 'https://publisher.woblink.com/login_check',
155 def search_catalogue(self, category, term):
156 return self.session.get(
157 self.SEARCH_CATALOGUE_URL.format(category=category, term=term)
160 def search_author_catalogue(self, term):
164 'text': item['autFullname']
166 for item in self.search_catalogue('author', term)
168 def search_series_catalogue(self, term):
174 for item in self.search_catalogue('series', term)
177 def get_isbn(self, meta, errors=None):
178 if not meta.isbn_epub:
179 if errors is not None:
180 errors.append(NoIsbn())
181 return meta.isbn_epub
183 def get_authors_data(self, meta, errors=None):
185 for role, items, obligatory in [
186 (self.ROLE_AUTHOR, meta.authors, True),
187 (self.ROLE_TRANSLATOR, meta.translators, False)
189 for person_literal in items:
190 if person_literal is None: continue
191 if person_literal.lang != 'pl':
192 if errors is not None:
194 errors.append(AuthorLiteralForeign(person_literal))
196 errors.append(AuthorLiteralForeignWarning(person_literal))
198 aobj = Author.get_by_literal(str(person_literal))
200 if errors is not None:
202 errors.append(AuthorNotInCatalogue(person_literal))
204 errors.append(AuthorNotInCatalogueWarning(person_literal))
207 if errors is not None:
209 errors.append(AuthorNoWoblink(aobj))
211 errors.append(AuthorNoWoblinkWarning(aobj))
213 authors.append((role, aobj.woblink))
216 def get_genres(self, meta, errors=None):
219 thema_codes.append(meta.thema_main)
221 if errors is not None:
222 errors.append(NoMainThemaWarning())
223 thema_codes.extend(meta.thema)
226 Audience.objects.filter(code__in=meta.audiences).exclude(
227 thema='').values_list('thema', flat=True)
231 if errors is not None:
232 errors.append(NoThema())
234 for code in thema_codes:
236 thema = Thema.objects.get(code=code)
237 except Thema.DoesNotExist:
238 if errors is not None:
239 errors.append(UnknownThema(code))
241 if thema.woblink_category is None:
242 if errors is not None:
243 errors.append(ThemaNoWoblink(thema))
244 elif thema.woblink_category not in WOBLINK_CATEGORIES:
245 if errors is not None:
246 errors.append(ThemaUnknownWoblink(thema))
247 elif thema.woblink_category not in category_ids:
248 category_ids.append(thema.woblink_category)
250 if errors is not None:
251 errors.append(NoWoblinkCategory())
254 def get_series(self, meta, errors=None):
255 return list(Audience.objects.filter(code__in=meta.audiences).exclude(
256 woblink=None).values_list('woblink', flat=True))
258 def get_abstract(self, wldoc, errors=None, description_add=None):
259 description = self.get_description(wldoc, description_add)
260 parts = description.split('\n', 1)
261 if len(parts) == 1 or len(parts[0]) > 240:
262 # No newline found here.
263 # Try to find last sentence end..
264 parts = re.split(r' \.', description[240::-1], 1)
266 p1 = parts[1][::-1] + '.'
267 p2 = description[len(p1) + 1:]
269 # No sentence end found.
271 p1 = description[:240].rsplit(' ', 1)[0]
272 p2 = description[len(p1) + 1:]
277 m = re.search(r'<[^>]+$', parts[0])
279 parts[0] = parts[0][:-len(m.group(0))]
280 parts[1] = m.group(0) + parts[1]
283 for tag in re.findall(r'<[^>]*[^/>]>', parts[0]):
288 for tag in reversed(opened):
289 parts[0] += '</' + tag[1:-1].split()[0] + '>'
290 parts[1] = tag + parts[1]
296 def get_lang2code(self, meta, errors=None):
297 return lang_code_3to2(meta.language)
299 def get_price(self, site, wldoc, errors=None):
301 stats = wldoc.get_statistics()['total']
304 errors.append(NoPrice(site))
306 words = stats['words_with_fn']
307 pages = stats['chars_with_fn'] / 1800
308 price = site.get_price(words, pages)
311 errors.append(NoPrice(site))
316 def can_publish(self, site, book):
323 wldoc = book.wldocument(librarian2=True)
325 d['errors'].append('Nieprawidłowy dokument.')
328 book_data = self.get_book_data(site, wldoc, errors)
330 if not isinstance(error, Warning):
331 errlist = d['errors']
333 errlist = d['warnings']
334 errlist.append(error.as_html())
336 if book_data.get('isbn'):
337 d['info'].append(format_html(
339 isbn=book_data['isbn'],
342 if book_data.get('genres'):
343 d['info'].append(format_html(
344 'W kategoriach: {cat} ({price} zł)',
345 cat=', '.join(self.describe_category(g) for g in book_data['genres']),
346 price=book_data['price'],
348 d['info'].append(mark_safe(
349 '<strong>' + book_data['abstract']['header'] +
350 '</strong><br/>' + book_data['abstract']['rest']
355 def describe_category(self, category):
358 c = WOBLINK_CATEGORIES[category]
360 category = c.get('parent')
361 return ' / '.join(reversed(t))
363 def create_book(self, isbn):
364 isbn = ''.join(c for c in isbn if c.isdigit())
365 assert len(isbn) == 13
366 response = self.session.post(
369 'AddPublication[pubType]': 'ebook',
370 'AddPublication[pubHasIsbn]': '1',
371 'AddPublication[pubIsbn]': isbn,
375 m = re.search(r'/(\d+)$', response.url)
379 def send_book(self, site_book_publish, changes=None):
380 site_book = site_book_publish.site_book
381 book = site_book.book
382 site = site_book.site
383 wldoc = book.wldocument(librarian2=True, changes=changes, publishable=False) # TODO pub
386 book_data = self.get_book_data(site, wldoc)
388 if not site_book.external_id:
389 woblink_id = self.create_book(book_data['isbn'])
391 site_book.external_id = woblink_id
392 site_book.save(update_fields=['external_id'])
393 woblink_id = site_book.external_id
395 self.edit_step1(woblink_id, book_data)
396 self.edit_step2(woblink_id, book_data)
397 self.edit_step3(woblink_id, book_data)
398 cover_id = self.send_cover(woblink_id, wldoc)
400 texts = site.get_texts()
401 epub_id, epub_demo = self.send_epub(
402 woblink_id, wldoc, book.gallery_path(),
405 mobi_id, mobi_demo = self.send_mobi(
406 woblink_id, wldoc, book.gallery_path(),
410 woblink_id, book_data,
411 cover_id, epub_id, epub_demo, mobi_id, mobi_demo,
413 self.edit_step5(woblink_id, book_data)
415 def get_book_data(self, site, wldoc, errors=None):
417 "title": wldoc.meta.title,
418 "isbn": self.get_isbn(wldoc.meta, errors=errors),
419 "authors": self.get_authors_data(wldoc.meta, errors=errors),
420 "abstract": self.get_abstract(
421 wldoc, errors=errors, description_add=site.description_add
423 "lang2code": self.get_lang2code(wldoc.meta, errors=errors),
424 "genres": self.get_genres(wldoc.meta, errors=errors),
425 "price": self.get_price(site, wldoc, errors=errors),
426 "series": self.get_series(wldoc.meta, errors=errors),
429 def with_form_name(self, data, name):
432 for (k, v) in data.items()
435 def edit_step1(self, woblink_id, book_data):
440 "AhpPubId": woblink_id,
441 "AhpAutId": author_id,
442 "AhpType": author_type,
444 for (author_type, author_id) in data['authors']
449 'PublicationId': woblink_id,
450 'SeriesId': series_id,
452 for series_id in data['series']
456 'pubTitle': book_data['title'],
457 'npwAuthorHasPublications': json.dumps(authors_data),
458 'pubShortNote': data['abstract']['header'],
459 'pubNote': data['abstract']['rest'],
460 'pubCulture': data['lang2code'],
461 'npwPublicationHasAwards': '[]',
462 'npwPublicationHasSeriess': json.dumps(series_data),
464 d = self.with_form_name(d, 'EditPublicationStep1')
465 d['roles'] = [author_type for (author_type, author_id) in data['authors']]
466 r = self.session.post(self.STEP1_URL % woblink_id, data=d)
470 def edit_step2(self, woblink_id, book_data):
473 for i, g in enumerate(book_data['genres']):
474 gdata = WOBLINK_CATEGORIES[g]
476 legacy = gdata.get('legacy')
477 if p := gdata.get('parent'):
478 gd.setdefault(p, {'isMain': False})
479 gd[p].setdefault('children', [])
480 gd[p]['children'].append(str(g))
481 gd[p].setdefault('mainChild', str(g))
483 legacy = WOBLINK_CATEGORIES[p].get('legacy')
486 gd[g]['isMain'] = True
493 for k, v in gd.items()
497 'npwPublicationHasNewGenres': json.dumps(gd),
498 'genre': legacy or '',
500 data = self.with_form_name(data, 'AddPublicationStep2')
501 return self.session.post(self.STEP2_URL % woblink_id, data=data)
503 def edit_step3(self, woblink_id, book_data):
505 'pubBasePrice': book_data['price'],
506 'pubPremiereDate': date.today().isoformat(),
507 'pubIsLicenseIndefinite': '1',
508 'pubFileFormat': 'epub+mobi',
510 'pubPublisherIndex': '',
511 'save_and_continue': '',
513 d = self.with_form_name(d, 'EditPublicationStep3')
514 return self.session.post(self.STEP3_URL % woblink_id, data=d)
516 def edit_step4(self, woblink_id, book_data, cover_id, epub_id, epub_demo, mobi_id, mobi_demo):
518 'pubCoverResId': cover_id,
519 'pubEpubResId': epub_id,
520 'pubEpubDemoResId': epub_demo,
521 'pubMobiResId': mobi_id,
522 'pubMobiDemoResId': mobi_demo,
523 'pubFileFormat': 'epub+mobi',
525 'save_and_continue': '',
527 d = self.with_form_name(d, 'EditPublicationStep4')
528 return self.session.post(self.STEP4_URL % woblink_id, data=d)
530 def edit_step5(self, woblink_id, book_data):
532 d = self.with_form_name(d, 'EditPublicationStep5')
533 return self.session.post(self.STEP5_URL % woblink_id, data=d)
535 def wait_for_job(self, job_id):
537 response = self.session.post(
539 data={'ids[]': job_id}
541 data = response.json()[job_id]
543 assert data['successful']
544 return data.get('returnValue')
547 def upload_file(self, woblink_id, filename, content, field_name, mime_type):
548 form_name = f'Upload{field_name}'
549 id_field = f'pub{field_name}ResId'
550 field_name = field_name.lower()
556 field_name: (filename, content, mime_type)
559 response = self.session.post(
560 self.UPLOAD_URL % field_name,
561 data=self.with_form_name(data, form_name),
562 files=self.with_form_name(files, form_name),
564 resp_data = response.json()
565 assert resp_data['success'] is True
566 file_id = resp_data[id_field]
567 if 'jobId' in resp_data:
568 self.wait_for_job(resp_data['jobId'])
571 def generate_demo(self, woblink_id, file_format, check=True):
574 job_id = self.session.get(
575 self.GENERATE_DEMO_URL % (file_format, woblink_id, percent),
578 file_id = self.wait_for_job(job_id)
582 self.CHECK_DEMO_URL % (file_format, woblink_id)
585 except AssertionError:
595 def send_epub(self, woblink_id, doc, gallery_path, fundraising=None):
596 from librarian.builders import EpubBuilder
597 content = EpubBuilder(
598 base_url='file://' + gallery_path + '/',
599 fundraising=fundraising or [],
600 ).build(doc).get_file()
601 file_id = self.upload_file(
603 doc.meta.url.slug + '.epub',
606 'application/epub+zip'
608 demo_id = self.generate_demo(woblink_id, 'epub')
609 return file_id, demo_id
611 def send_mobi(self, woblink_id, doc, gallery_path, fundraising=None):
612 from librarian.builders import MobiBuilder
613 content = MobiBuilder(
614 base_url='file://' + gallery_path + '/',
615 fundraising=fundraising or [],
616 ).build(doc).get_file()
617 file_id = self.upload_file(
619 doc.meta.url.slug + '.mobi',
622 'application/x-mobipocket-ebook'
624 demo_id = self.generate_demo(woblink_id, 'mobi', check=False)
625 return file_id, demo_id
627 def send_cover(self, woblink_id, doc):
628 from librarian.cover import make_cover
631 cover = make_cover(doc.meta, cover_class='m-label', width=1748, height=2480)
632 content = io.BytesIO()
633 cover.final_image().save(content, cover.format)
635 file_id = self.upload_file(
637 doc.meta.url.slug + '.jpeg',