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 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/shop/{price}">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 UPLOAD_URL = BASE_URL + 'file/upload-%s'
128 JOB_STATUS_URL = BASE_URL + 'task/status'
129 GENERATE_DEMO_URL = BASE_URL + 'task/run/generate-%s-demo/%s/%d'
130 CHECK_DEMO_URL = BASE_URL + 'task/run/check-%s-demo/%s'
136 response = self.session.get('https://publisher.woblink.com/login')
138 r'name="_csrf_token" value="([^"]+)"',
142 '_csrf_token': token,
143 '_username': self.username,
144 '_password': self.password,
146 response = self.session.post(
147 'https://publisher.woblink.com/login_check',
151 def get_isbn(self, meta, errors=None):
152 if not meta.isbn_epub:
153 if errors is not None:
154 errors.append(NoIsbn())
155 return meta.isbn_epub
157 def get_authors_data(self, meta, errors=None):
159 for role, items, obligatory in [
160 (self.ROLE_AUTHOR, meta.authors, True),
161 (self.ROLE_TRANSLATOR, meta.translators, False)
163 for person_literal in items:
164 if person_literal.lang != 'pl':
165 if errors is not None:
167 errors.append(AuthorLiteralForeign(person_literal))
169 errors.append(AuthorLiteralForeignWarning(person_literal))
171 aobj = Author.get_by_literal(str(person_literal))
173 if errors is not None:
175 errors.append(AuthorNotInCatalogue(person_literal))
177 errors.append(AuthorNotInCatalogueWarning(person_literal))
180 if errors is not None:
182 errors.append(AuthorNoWoblink(aobj))
184 errors.append(AuthorNoWoblinkWarning(aobj))
186 authors.append((role, aobj.woblink))
189 def get_genres(self, meta, errors=None):
192 thema_codes.append(meta.thema_main)
194 if errors is not None:
195 errors.append(NoMainThemaWarning())
196 thema_codes.extend(meta.thema)
198 if errors is not None:
199 errors.append(NoThema())
201 for code in thema_codes:
203 thema = Thema.objects.get(code=code)
204 except Thema.DoesNotExist:
205 if errors is not None:
206 errors.append(UnknownThema(code))
208 if thema.woblink_category is None:
209 if errors is not None:
210 errors.append(ThemaNoWoblink(thema))
211 elif thema.woblink_category not in WOBLINK_CATEGORIES:
212 if errors is not None:
213 errors.append(ThemaUnknownWoblink(thema))
214 elif thema.woblink_category not in category_ids:
215 category_ids.append(thema.woblink_category)
217 if errors is not None:
218 errors.append(NoWoblinkCategory())
221 def get_series(self, meta, errors=None):
224 def get_abstract(self, wldoc, errors=None, description_add=None):
225 description = self.get_description(wldoc, description_add)
226 parts = description.split('\n', 1)
227 if len(parts) == 1 or len(parts[0]) > 200:
228 p1 = description[:200].rsplit(' ', 1)[0]
229 p2 = description[len(p1):]
234 m = re.search(r'<[^>]+$', parts[0])
236 parts[0] = parts[:-len(m.group(0))]
237 parts[1] = m.group(0) + parts[1]
240 for tag in re.findall(r'<[^>]+[^/>]>', parts[0]):
245 for tag in reversed(opened):
246 parts[0] += '</' + tag[1:-1].split()[0] + '>'
247 parts[1] = tag + parts[1]
253 def get_lang2code(self, meta, errors=None):
254 return lang_code_3to2(meta.language)
256 def get_price(self, shop, wldoc, errors=None):
257 stats = wldoc.get_statistics()['total']
258 words = stats['words_with_fn']
259 pages = stats['chars_with_fn'] / 1800
260 price = shop.get_price(words, pages)
263 errors.append(NoPrice(shop))
268 def can_publish(self, shop, book):
269 wldoc = book.wldocument(librarian2=True)
275 book_data = self.get_book_data(shop, wldoc, errors)
277 if not isinstance(error, Warning):
278 errlist = d['errors']
280 errlist = d['warnings']
281 errlist.append(error.as_html())
283 if book_data.get('genres'):
284 d['comment'] = format_html(
285 'W kategoriach: {cat} ({price} zł)',
286 cat=', '.join(self.describe_category(g) for g in book_data['genres']),
287 price=book_data['price']
292 def describe_category(self, category):
295 c = WOBLINK_CATEGORIES[category]
297 category = c.get('parent')
298 return ' / '.join(reversed(t))
300 def create_book(self, isbn):
301 isbn = ''.join(c for c in isbn if c.isdigit())
302 assert len(isbn) == 13
303 response = self.session.post(
306 'AddPublication[pubType]': 'ebook',
307 'AddPublication[pubHasIsbn]': '1',
308 'AddPublication[pubIsbn]': isbn,
312 m = re.search(r'/(\d+)$', response.url)
316 def send_book(self, shop, book, changes=None):
317 wldoc = book.wldocument(librarian2=True, changes=changes, publishable=False) # TODO pub
320 book_data = self.get_book_data(shop, wldoc)
322 if not book.woblink_id:
323 #book.woblink_id = 2959868
324 woblink_id = self.create_book(book_data['isbn'])
326 book.woblink_id = woblink_id
327 book.save(update_fields=['woblink_id'])
329 self.edit_step1(book.woblink_id, book_data)
330 self.edit_step2(book.woblink_id, book_data)
331 self.edit_step3(book.woblink_id, book_data)
332 self.send_cover(book.woblink_id, wldoc)
333 texts = shop.get_texts()
335 book.woblink_id, wldoc, book.gallery_path(),
339 book.woblink_id, wldoc, book.gallery_path(),
343 def get_book_data(self, shop, wldoc, errors=None):
345 "title": wldoc.meta.title,
346 "isbn": self.get_isbn(wldoc.meta, errors=errors),
347 "authors": self.get_authors_data(wldoc.meta, errors=errors),
348 "abstract": self.get_abstract(
349 wldoc, errors=errors, description_add=shop.description_add
351 "lang2code": self.get_lang2code(wldoc.meta, errors=errors),
352 "genres": self.get_genres(wldoc.meta, errors=errors),
353 "price": self.get_price(shop, wldoc, errors=errors),
356 def with_form_name(self, data, name):
359 for (k, v) in data.items()
362 def edit_step1(self, woblink_id, book_data):
367 "AhpPubId": woblink_id,
368 "AhpAutId": author_id,
369 "AhpType": author_type,
371 for (author_type, author_id) in data['authors']
375 'pubTitle': book_data['title'],
376 'npwAuthorHasPublications': json.dumps(authors_data),
377 'pubShortNote': data['abstract']['header'],
378 'pubNote': data['abstract']['rest'],
379 'pubCulture': data['lang2code'],
380 'npwPublicationHasAwards': '[]',
381 'npwPublicationHasSeriess': '[]', # TODO
382 # "[{\"Id\":6153,\"PublicationId\":73876,\"SeriesId\":1615,\"Tome\":null}]"
384 d = self.with_form_name(d, 'EditPublicationStep1')
385 d['roles'] = [author_type for (author_type, author_id) in data['authors']]
386 r = self.session.post(self.STEP1_URL % woblink_id, data=d)
390 def edit_step2(self, woblink_id, book_data):
393 for i, g in enumerate(book_data['genres']):
394 gdata = WOBLINK_CATEGORIES[g]
396 legacy = gdata.get('legacy')
397 if p := gdata.get('parent'):
398 gd.setdefault(p, {'isMain': False})
399 gd[p].setdefault('children', [])
400 gd[p]['children'].append(str(g))
401 gd[p].setdefault('mainChild', str(g))
403 legacy = WOBLINK_CATEGORIES[p].get('legacy')
406 ds[p]['isMain'] = True
413 for k, v in gd.items()
417 'npwPublicationHasNewGenres': json.dumps(gd),
418 'genre': legacy or '',
420 data = self.with_form_name(data, 'AddPublicationStep2')
421 return self.session.post(self.STEP2_URL % woblink_id, data=data)
423 def edit_step3(self, woblink_id, book_data):
425 'pubBasePrice': book_data['price'],
426 'pubPremiereDate': '2023-08-09', #date.today().isoformat(),
427 'pubIsLicenseIndefinite': '1',
428 'pubFileFormat': 'epub+mobi',
430 'pubPublisherIndex': '',
432 d = self.with_form_name(d, 'EditPublicationStep3')
433 return self.session.post(self.STEP3_URL % woblink_id, data=d)
435 def wait_for_job(self, job_id):
437 response = self.session.post(
439 data={'ids[]': job_id}
441 data = response.json()[job_id]
443 assert data['successful']
447 def upload_file(self, woblink_id, filename, content, form_name, field_name, mime_type):
452 field_name: (filename, content, mime_type)
454 response = self.session.post(
455 self.UPLOAD_URL % field_name,
456 data=self.with_form_name(data, form_name),
457 files=self.with_form_name(files, form_name),
459 resp_data = response.json()
460 assert resp_data['success'] is True
461 if 'jobId' in resp_data:
462 self.wait_for_job(resp_data['jobId'])
464 def generate_demo(self, woblink_id, file_format, check=True):
467 job_id = self.session.get(
468 self.GENERATE_DEMO_URL % (file_format, woblink_id, percent),
471 self.wait_for_job(job_id)
472 except AssertionError:
483 self.CHECK_DEMO_URL % (file_format, woblink_id)
487 def send_epub(self, woblink_id, doc, gallery_path, fundraising=None):
488 from librarian.builders import EpubBuilder
489 content = EpubBuilder(
490 base_url='file://' + gallery_path + '/',
491 fundraising=fundraising or [],
492 ).build(doc).get_file()
495 doc.meta.url.slug + '.epub',
499 'application/epub+zip'
501 self.generate_demo(woblink_id, 'epub')
503 def send_mobi(self, woblink_id, doc, gallery_path, fundraising=None):
504 from librarian.builders import MobiBuilder
505 content = MobiBuilder(
506 base_url='file://' + gallery_path + '/',
507 fundraising=fundraising or [],
508 ).build(doc).get_file()
511 doc.meta.url.slug + '.mobi',
515 'application/x-mobipocket-ebook'
517 self.generate_demo(woblink_id, 'mobi', check=False)
519 def send_cover(self, woblink_id, doc):
520 from librarian.cover import make_cover
523 cover = make_cover(doc.meta, cover_class='m-label', width=1748, height=2480)
524 content = io.BytesIO()
525 cover.final_image().save(content, cover.format)
529 doc.meta.url.slug + '.jpeg',