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',