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.lang != 'pl':
 
 191                     if errors is not None:
 
 193                              errors.append(AuthorLiteralForeign(person_literal))
 
 195                             errors.append(AuthorLiteralForeignWarning(person_literal))
 
 197                 aobj = Author.get_by_literal(str(person_literal))
 
 199                     if errors is not None:
 
 201                              errors.append(AuthorNotInCatalogue(person_literal))
 
 203                             errors.append(AuthorNotInCatalogueWarning(person_literal))
 
 206                     if errors is not None:
 
 208                              errors.append(AuthorNoWoblink(aobj))
 
 210                             errors.append(AuthorNoWoblinkWarning(aobj))
 
 212                 authors.append((role, aobj.woblink))
 
 215     def get_genres(self, meta, errors=None):
 
 218             thema_codes.append(meta.thema_main)
 
 220             if errors is not None:
 
 221                 errors.append(NoMainThemaWarning())
 
 222         thema_codes.extend(meta.thema)
 
 225             Audience.objects.filter(code__in=meta.audiences).exclude(
 
 226                 thema='').values_list('thema', flat=True)
 
 230             if errors is not None:
 
 231                 errors.append(NoThema())
 
 233         for code in thema_codes:
 
 235                 thema = Thema.objects.get(code=code)
 
 236             except Thema.DoesNotExist:
 
 237                 if errors is not None:
 
 238                     errors.append(UnknownThema(code))
 
 240                 if thema.woblink_category is None:
 
 241                     if errors is not None:
 
 242                         errors.append(ThemaNoWoblink(thema))
 
 243                 elif thema.woblink_category not in WOBLINK_CATEGORIES:
 
 244                     if errors is not None:
 
 245                         errors.append(ThemaUnknownWoblink(thema))
 
 246                 elif thema.woblink_category not in category_ids:
 
 247                     category_ids.append(thema.woblink_category)
 
 249             if errors is not None:
 
 250                 errors.append(NoWoblinkCategory())
 
 253     def get_series(self, meta, errors=None):
 
 254         return list(Audience.objects.filter(code__in=meta.audiences).exclude(
 
 255             woblink=None).values_list('woblink', flat=True))
 
 257     def get_abstract(self, wldoc, errors=None, description_add=None):
 
 258         description = self.get_description(wldoc, description_add)
 
 259         parts = description.split('\n', 1)
 
 260         if len(parts) == 1 or len(parts[0]) > 240:
 
 261             # No newline found here.
 
 262             # Try to find last sentence end..
 
 263             parts = re.split(r' \.', description[240::-1], 1)
 
 265                 p1 = parts[1][::-1] + '.'
 
 266                 p2 = description[len(p1) + 1:]
 
 268                 # No sentence end found.
 
 270                 p1 = description[:240].rsplit(' ', 1)[0]
 
 271                 p2 = description[len(p1) + 1:]
 
 276         m = re.search(r'<[^>]+$', parts[0])
 
 278             parts[0] = parts[0][:-len(m.group(0))]
 
 279             parts[1] = m.group(0) + parts[1]
 
 282         for tag in re.findall(r'<[^>]*[^/>]>', parts[0]):
 
 287         for tag in reversed(opened):
 
 288             parts[0] += '</' + tag[1:-1].split()[0] + '>'
 
 289             parts[1] = tag + parts[1]
 
 295     def get_lang2code(self, meta, errors=None):
 
 296         return lang_code_3to2(meta.language)
 
 298     def get_price(self, site, wldoc, errors=None):
 
 300             stats = wldoc.get_statistics()['total']
 
 303                 errors.append(NoPrice(site))
 
 305         words = stats['words_with_fn']
 
 306         pages = stats['chars_with_fn'] / 1800
 
 307         price = site.get_price(words, pages)
 
 310                 errors.append(NoPrice(site))
 
 315     def can_publish(self, site, book):
 
 316         wldoc = book.wldocument(librarian2=True)
 
 323         book_data = self.get_book_data(site, wldoc, errors)
 
 325             if not isinstance(error, Warning):
 
 326                 errlist = d['errors']
 
 328                 errlist = d['warnings']
 
 329             errlist.append(error.as_html())
 
 331         if book_data.get('genres'):
 
 332             d['info'].append(format_html(
 
 333                 'W kategoriach: {cat} ({price} zł)',
 
 334                 cat=', '.join(self.describe_category(g) for g in book_data['genres']),
 
 335                 price=book_data['price'],
 
 337         d['info'].append(mark_safe(
 
 338             '<strong>' + book_data['abstract']['header'] +
 
 339             '</strong><br/>' + book_data['abstract']['rest']
 
 344     def describe_category(self, category):
 
 347             c = WOBLINK_CATEGORIES[category]
 
 349             category = c.get('parent')
 
 350         return ' / '.join(reversed(t))
 
 352     def create_book(self, isbn):
 
 353         isbn = ''.join(c for c in isbn if c.isdigit())
 
 354         assert len(isbn) == 13
 
 355         response = self.session.post(
 
 358                 'AddPublication[pubType]': 'ebook',
 
 359                 'AddPublication[pubHasIsbn]': '1',
 
 360                 'AddPublication[pubIsbn]': isbn,
 
 364         m = re.search(r'/(\d+)$', response.url)
 
 368     def send_book(self, site_book_publish, changes=None):
 
 369         site_book = site_book_publish.site_book
 
 370         book = site_book.book
 
 371         site = site_book.site
 
 372         wldoc = book.wldocument(librarian2=True, changes=changes, publishable=False) # TODO pub
 
 375         book_data = self.get_book_data(site, wldoc)
 
 377         if not site_book.external_id:
 
 378             woblink_id = self.create_book(book_data['isbn'])
 
 380             site_book.external_id = woblink_id
 
 381             site_book.save(update_fields=['external_id'])
 
 382         woblink_id = site_book.external_id
 
 384         self.edit_step1(woblink_id, book_data)
 
 385         self.edit_step2(woblink_id, book_data)
 
 386         self.edit_step3(woblink_id, book_data)
 
 387         cover_id = self.send_cover(woblink_id, wldoc)
 
 389         texts = site.get_texts()
 
 390         epub_id, epub_demo = self.send_epub(
 
 391             woblink_id, wldoc, book.gallery_path(),
 
 394         mobi_id, mobi_demo = self.send_mobi(
 
 395             woblink_id, wldoc, book.gallery_path(),
 
 399             woblink_id, book_data,
 
 400             cover_id, epub_id, epub_demo, mobi_id, mobi_demo,
 
 402         self.edit_step5(woblink_id, book_data)
 
 404     def get_book_data(self, site, wldoc, errors=None):
 
 406             "title": wldoc.meta.title,
 
 407             "isbn": self.get_isbn(wldoc.meta, errors=errors),
 
 408             "authors": self.get_authors_data(wldoc.meta, errors=errors),
 
 409             "abstract": self.get_abstract(
 
 410                 wldoc, errors=errors, description_add=site.description_add
 
 412             "lang2code": self.get_lang2code(wldoc.meta, errors=errors),
 
 413             "genres": self.get_genres(wldoc.meta, errors=errors),
 
 414             "price": self.get_price(site, wldoc, errors=errors),
 
 415             "series": self.get_series(wldoc.meta, errors=errors),
 
 418     def with_form_name(self, data, name):
 
 421             for (k, v) in data.items()
 
 424     def edit_step1(self, woblink_id, book_data):
 
 429                 "AhpPubId": woblink_id,
 
 430                 "AhpAutId": author_id,
 
 431                 "AhpType": author_type,
 
 433             for (author_type, author_id) in data['authors']
 
 438                 'PublicationId': woblink_id,
 
 439                 'SeriesId': series_id,
 
 441             for series_id in data['series']
 
 445             'pubTitle': book_data['title'],
 
 446             'npwAuthorHasPublications': json.dumps(authors_data),
 
 447             'pubShortNote': data['abstract']['header'],
 
 448             'pubNote': data['abstract']['rest'],
 
 449             'pubCulture': data['lang2code'],
 
 450             'npwPublicationHasAwards': '[]',
 
 451             'npwPublicationHasSeriess': json.dumps(series_data),
 
 453         d = self.with_form_name(d, 'EditPublicationStep1')
 
 454         d['roles'] = [author_type for (author_type, author_id) in data['authors']]
 
 455         r = self.session.post(self.STEP1_URL % woblink_id, data=d)
 
 459     def edit_step2(self, woblink_id, book_data):
 
 462         for i, g in enumerate(book_data['genres']):
 
 463             gdata = WOBLINK_CATEGORIES[g]
 
 465                 legacy = gdata.get('legacy')
 
 466             if p := gdata.get('parent'):
 
 467                 gd.setdefault(p, {'isMain': False})
 
 468                 gd[p].setdefault('children', [])
 
 469                 gd[p]['children'].append(str(g))
 
 470                 gd[p].setdefault('mainChild', str(g))
 
 472                     legacy = WOBLINK_CATEGORIES[p].get('legacy')
 
 475                 gd[g]['isMain'] = True
 
 482             for k, v in gd.items()
 
 486             'npwPublicationHasNewGenres': json.dumps(gd),
 
 487             'genre': legacy or '',
 
 489         data = self.with_form_name(data, 'AddPublicationStep2')
 
 490         return self.session.post(self.STEP2_URL % woblink_id, data=data)
 
 492     def edit_step3(self, woblink_id, book_data):
 
 494             'pubBasePrice': book_data['price'],
 
 495             'pubPremiereDate': date.today().isoformat(),
 
 496             'pubIsLicenseIndefinite': '1',
 
 497             'pubFileFormat': 'epub+mobi',
 
 499             'pubPublisherIndex': '',
 
 500             'save_and_continue': '',
 
 502         d = self.with_form_name(d, 'EditPublicationStep3')
 
 503         return self.session.post(self.STEP3_URL % woblink_id, data=d)
 
 505     def edit_step4(self, woblink_id, book_data, cover_id, epub_id, epub_demo, mobi_id, mobi_demo):
 
 507             'pubCoverResId': cover_id,
 
 508             'pubEpubResId': epub_id,
 
 509             'pubEpubDemoResId': epub_demo,
 
 510             'pubMobiResId': mobi_id,
 
 511             'pubMobiDemoResId': mobi_demo,
 
 512             'pubFileFormat': 'epub+mobi',
 
 514             'save_and_continue': '',
 
 516         d = self.with_form_name(d, 'EditPublicationStep4')
 
 517         return self.session.post(self.STEP4_URL % woblink_id, data=d)
 
 519     def edit_step5(self, woblink_id, book_data):
 
 521         d = self.with_form_name(d, 'EditPublicationStep5')
 
 522         return self.session.post(self.STEP5_URL % woblink_id, data=d)
 
 524     def wait_for_job(self, job_id):
 
 526             response = self.session.post(
 
 528                 data={'ids[]': job_id}
 
 530             data = response.json()[job_id]
 
 532                 assert data['successful']
 
 533                 return data.get('returnValue')
 
 536     def upload_file(self, woblink_id, filename, content, field_name, mime_type):
 
 537         form_name = f'Upload{field_name}'
 
 538         id_field = f'pub{field_name}ResId'
 
 539         field_name = field_name.lower()
 
 545             field_name: (filename, content, mime_type)
 
 548         response = self.session.post(
 
 549             self.UPLOAD_URL % field_name,
 
 550             data=self.with_form_name(data, form_name),
 
 551             files=self.with_form_name(files, form_name),
 
 553         resp_data = response.json()
 
 554         assert resp_data['success'] is True
 
 555         file_id = resp_data[id_field]
 
 556         if 'jobId' in resp_data:
 
 557             self.wait_for_job(resp_data['jobId'])
 
 560     def generate_demo(self, woblink_id, file_format, check=True):
 
 563             job_id = self.session.get(
 
 564                 self.GENERATE_DEMO_URL % (file_format, woblink_id, percent),
 
 567                 file_id = self.wait_for_job(job_id)
 
 571                             self.CHECK_DEMO_URL % (file_format, woblink_id)
 
 574             except AssertionError:
 
 584     def send_epub(self, woblink_id, doc, gallery_path, fundraising=None):
 
 585         from librarian.builders import EpubBuilder
 
 586         content = EpubBuilder(
 
 587             base_url='file://' + gallery_path + '/',
 
 588             fundraising=fundraising or [],
 
 589         ).build(doc).get_file()
 
 590         file_id = self.upload_file(
 
 592             doc.meta.url.slug + '.epub',
 
 595             'application/epub+zip'
 
 597         demo_id = self.generate_demo(woblink_id, 'epub')
 
 598         return file_id, demo_id
 
 600     def send_mobi(self, woblink_id, doc, gallery_path, fundraising=None):
 
 601         from librarian.builders import MobiBuilder
 
 602         content = MobiBuilder(
 
 603             base_url='file://' + gallery_path + '/',
 
 604             fundraising=fundraising or [],
 
 605         ).build(doc).get_file()
 
 606         file_id = self.upload_file(
 
 608             doc.meta.url.slug + '.mobi',
 
 611             'application/x-mobipocket-ebook'
 
 613         demo_id = self.generate_demo(woblink_id, 'mobi', check=False)
 
 614         return file_id, demo_id
 
 616     def send_cover(self, woblink_id, doc):
 
 617         from librarian.cover import make_cover
 
 620         cover = make_cover(doc.meta, cover_class='m-label', width=1748, height=2480)
 
 621         content = io.BytesIO()
 
 622         cover.final_image().save(content, cover.format)
 
 624         file_id = self.upload_file(
 
 626             doc.meta.url.slug + '.jpeg',