Allow multiple sources.
[redakcja.git] / src / depot / models.py
1 import json
2 import os
3 import tempfile
4 import traceback
5 import zipfile
6 from datetime import datetime
7 from django.conf import settings
8 from django.db import models
9 from django.utils.timezone import now
10 from librarian.cover import make_cover
11 from librarian.builders import EpubBuilder, MobiBuilder
12 from .publishers.legimi import Legimi
13 from .publishers.woblink import Woblink
14
15
16 class Package(models.Model):
17     created_at = models.DateTimeField(auto_now_add=True)
18     placed_at = models.DateTimeField(null=True, blank=True)
19     finished_at = models.DateTimeField(null=True, blank=True)
20     definition_json = models.TextField(blank=True)
21     books = models.ManyToManyField('documents.Book')
22     status_json = models.TextField(blank=True)
23     logo = models.FileField(blank=True, upload_to='depot/logo')
24     file = models.FileField(blank=True, upload_to='depot/package/')
25
26     def save(self, *args, **kwargs):
27         try:
28             self.set_status(self.get_status())
29         except:
30             pass
31
32         try:
33             self.set_definition(self.get_definition())
34         except:
35             pass
36
37         super().save(*args, **kwargs)
38
39     def get_status(self):
40         return json.loads(self.status_json)
41
42     def set_status(self, status):
43         self.status_json = json.dumps(status, indent=4, ensure_ascii=False)
44
45     def get_definition(self):
46         return json.loads(self.definition_json)
47
48     def set_definition(self, definition):
49         self.definition_json = json.dumps(definition, indent=4, ensure_ascii=False)
50
51     def build(self):
52         f = tempfile.NamedTemporaryFile(prefix='depot-', suffix='.zip', mode='wb', delete=False)
53         book_count = self.books.all().count()
54         with zipfile.ZipFile(f, 'w') as z:
55             for i, book in enumerate(self.books.all()):
56                 print(f'{i}/{book_count} {book.slug}')
57                 self.build_for(book, z)
58         f.close()
59         with open(f.name, 'rb') as ff:
60             self.file.save('package-{}.zip'.format(datetime.now().isoformat(timespec='seconds')), ff)
61         os.unlink(f.name)
62
63     def build_for(self, book, z):
64         wldoc2 = book.wldocument(librarian2=True)
65         slug = wldoc2.meta.url.slug
66         for item in self.get_definition():
67             wldoc = book.wldocument()
68             wldoc2 = book.wldocument(librarian2=True)
69             base_url = 'file://' + book.gallery_path() + '/'
70
71             ext = item['type']
72
73             if item['type'] == 'cover':
74                 kwargs = {}
75                 if self.logo:
76                     kwargs['cover_logo'] = self.logo.path
77                 for k in 'format', 'width', 'height', 'cover_class':
78                     if k in item:
79                         kwargs[k] = item[k]
80                 cover = make_cover(wldoc.book_info, **kwargs)
81                 output = cover.output_file()
82                 ext = cover.ext()
83
84             elif item['type'] == 'pdf':
85                 cover_kwargs = {}
86                 if 'cover_class' in item:
87                     cover_kwargs['cover_class'] = item['cover_class']
88                 if self.logo:
89                     cover_kwargs['cover_logo'] = self.logo.path
90                 cover = lambda *args, **kwargs: make_cover(*args, **kwargs, **cover_kwargs)
91                 output = wldoc.as_pdf(cover=cover, base_url=base_url)
92
93             elif item['type'] == 'epub':
94                 cover_kwargs = {}
95                 if 'cover_class' in item:
96                     cover_kwargs['cover_class'] = item['cover_class']
97                 if self.logo:
98                     cover_kwargs['cover_logo'] = self.logo.path
99                 cover = lambda *args, **kwargs: make_cover(*args, **kwargs, **cover_kwargs)
100
101                 output = EpubBuilder(
102                     cover=cover,
103                     base_url=base_url,
104                     fundraising=item.get('fundraising', []),
105                 ).build(wldoc2)
106
107             elif item['type'] == 'mobi':
108                 output = MobiBuilder(
109                     cover=cover,
110                     base_url=base_url,
111                     fundraising=item.get('fundraising', []),
112                 ).build(wldoc2)
113
114             fname = f'{slug}/{slug}.'
115             if 'slug' in item:
116                 fname += item['slug'] + '.'
117             fname += ext
118
119             z.writestr(
120                 fname,
121                 output.get_bytes()
122             )
123
124
125 class SiteBook(models.Model):
126     site = models.ForeignKey('Site', models.SET_NULL, null=True)
127     book = models.ForeignKey('documents.Book', models.CASCADE)
128     external_id = models.CharField(max_length=255, blank=True)
129     created_at = models.DateTimeField(auto_now_add=True)
130
131     class Meta:
132         unique_together = (('book', 'site'),)
133
134     def __str__(self):
135         return f'{self.site} : {self.book} : {self.external_id}'
136         
137
138 class SiteBookPublish(models.Model):
139     site_book = models.ForeignKey(SiteBook, models.PROTECT, null=True, blank=True)
140     user = models.ForeignKey(settings.AUTH_USER_MODEL, models.SET_NULL, null=True)
141     created_at = models.DateTimeField()
142     started_at = models.DateTimeField(null=True, blank=True)
143     finished_at = models.DateTimeField(null=True, blank=True)
144     status = models.PositiveSmallIntegerField(choices=[
145         (0, 'queued'),
146         (10, 'running'),
147         (100, 'done'),
148         (110, 'error'),
149     ], default=0)
150     error = models.TextField(blank=True)
151
152     @classmethod
153     def create_for(cls, book, user, site):
154         book.assert_publishable()
155         changes = book.get_current_changes(publishable=True)
156         site_book, created = SiteBook.objects.get_or_create(
157             site=site, book=book
158         )
159         me = cls.objects.create(
160             site_book=site_book, user=user, created_at=now())
161         for change in changes:
162             me.sitechunkpublish_set.create(change=change)
163         return me
164
165     def publish(self):
166         self.status = 10
167         self.started_at = now()
168         self.save(update_fields=['status', 'started_at'])
169         try:
170             changes = [
171                 p.change for p in
172                 self.sitechunkpublish_set.order_by('change__tree__number')
173             ]
174
175             self.site_book.site.publish(self, changes=changes)
176
177         except Exception:
178             self.status = 110
179             self.error = traceback.format_exc()
180         else:
181             self.status = 100
182             self.error = ''
183         self.finished_at = now()
184         self.save(update_fields=['status', 'finished_at', 'error'])
185
186
187 class SiteChunkPublish(models.Model):
188     book_publish = models.ForeignKey(SiteBookPublish, models.CASCADE)
189     change = models.ForeignKey('documents.ChunkChange', models.CASCADE)
190
191
192 class Site(models.Model):
193     name = models.CharField(max_length=255)
194     site_type = models.CharField(max_length=32, choices=[
195         ('legimi', 'Legimi'),
196         ('woblink', 'Woblink'),
197     ])
198     username = models.CharField(max_length=255)
199     password = models.CharField(max_length=255)
200     publisher_handle = models.CharField(max_length=255, blank=True)
201     description_add = models.TextField(blank=True)
202
203     def __str__(self):
204         return self.name
205
206     def get_texts(self):
207         return [t.text for t in self.mediainserttext_set.all()]
208
209     def get_price(self, words, pages):
210         price_obj = self.pricelevel_set.exclude(
211             min_pages__gt=pages
212         ).exclude(
213             min_words__gt=words
214         ).order_by('-price').first()
215         if price_obj is None:
216             return None
217         return price_obj.price
218
219     def get_publisher(self):
220         if self.site_type == 'legimi':
221             pub_class = Legimi
222         elif self.site_type == 'woblink':
223             pub_class = Woblink
224         return pub_class(self.username, self.password, self.publisher_handle)
225
226     def publish(self, site_book_publish, changes):
227         self.get_publisher().send_book(
228             site_book_publish,
229             changes=changes,
230         )
231
232     def can_publish(self, book):
233         return self.get_publisher().can_publish(self, book)
234
235     def get_last(self, book):
236         return SiteBookPublish.objects.filter(
237             site_book__site=self, site_book__book=book
238         ).order_by('-created_at').first()
239
240     def get_external_id_for_book(self, book):
241         site_book = self.sitebook_set.filter(book=book).first()
242         return (site_book and site_book.external_id) or ''
243
244 class PriceLevel(models.Model):
245     site = models.ForeignKey(Site, models.CASCADE)
246     min_pages = models.IntegerField(null=True, blank=True)
247     min_words = models.IntegerField(null=True, blank=True)
248     price = models.IntegerField()
249
250     class Meta:
251         ordering = ('price',)
252
253
254 class MediaInsertText(models.Model):
255     site = models.ForeignKey(Site, models.CASCADE)
256     ordering = models.IntegerField()
257     text = models.TextField()
258
259     class Meta:
260         ordering = ('ordering',)