From 33af401eb4abe15a37d0147807d06a9b19eaa802 Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Fri, 11 Mar 2022 15:51:30 +0100 Subject: [PATCH] Almost ready. --- src/catalogue/fields.py | 9 ++ .../migrations/0034_auto_20220310_1251.py | 25 ++++ .../migrations/0035_bookmedia_duration.py | 18 +++ src/catalogue/migrations/0036_book_toc.py | 18 +++ src/catalogue/models/book.py | 49 ++++++- src/catalogue/models/bookmedia.py | 25 ++-- src/catalogue/models/edition.py | 10 ++ .../templates/catalogue/2021/book_detail.html | 69 +++++----- .../catalogue/snippets/2022_jplayer.html | 129 +++++++++--------- .../catalogue/snippets/2022_jplayer_link.html | 21 +++ .../2022/styles/components/_player.scss | 29 +++- .../static/2022/styles/layout/_article.scss | 2 +- src/wolnelektury/static/js/current.js | 35 +++++ 13 files changed, 325 insertions(+), 114 deletions(-) create mode 100644 src/catalogue/migrations/0034_auto_20220310_1251.py create mode 100644 src/catalogue/migrations/0035_bookmedia_duration.py create mode 100644 src/catalogue/migrations/0036_book_toc.py create mode 100644 src/catalogue/models/edition.py create mode 100644 src/catalogue/templates/catalogue/snippets/2022_jplayer_link.html create mode 100644 src/wolnelektury/static/js/current.js diff --git a/src/catalogue/fields.py b/src/catalogue/fields.py index 581bf38a1..49fd46e2b 100644 --- a/src/catalogue/fields.py +++ b/src/catalogue/fields.py @@ -342,6 +342,15 @@ class BuildCover(BuildEbook): pass +@BuildEbook.register('cover_clean') +@task(ignore_result=True) +class BuildCoverClean(BuildCover): + @classmethod + def transform(cls, wldoc, fieldfile): + from librarian.cover import WLCover + return WLCover(wldoc.book_info, width=240).output_file() + + @BuildEbook.register('cover_thumb') @task(ignore_result=True) class BuildCoverThumb(BuildCover): diff --git a/src/catalogue/migrations/0034_auto_20220310_1251.py b/src/catalogue/migrations/0034_auto_20220310_1251.py new file mode 100644 index 000000000..758106ab7 --- /dev/null +++ b/src/catalogue/migrations/0034_auto_20220310_1251.py @@ -0,0 +1,25 @@ +# Generated by Django 2.2.25 on 2022-03-10 11:51 + +import catalogue.fields +import catalogue.models.book +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('catalogue', '0033_auto_20220128_1409'), + ] + + operations = [ + migrations.AddField( + model_name='book', + name='cover_clean', + field=catalogue.fields.EbookField('cover_clean', blank=True, max_length=255, null=True, upload_to=catalogue.models.book.UploadToPath('book/cover_clean/%s.jpg'), verbose_name='clean cover'), + ), + migrations.AddField( + model_name='book', + name='cover_clean_etag', + field=models.CharField(db_index=True, default='', editable=False, max_length=255), + ), + ] diff --git a/src/catalogue/migrations/0035_bookmedia_duration.py b/src/catalogue/migrations/0035_bookmedia_duration.py new file mode 100644 index 000000000..9412528ea --- /dev/null +++ b/src/catalogue/migrations/0035_bookmedia_duration.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.25 on 2022-03-10 12:49 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('catalogue', '0034_auto_20220310_1251'), + ] + + operations = [ + migrations.AddField( + model_name='bookmedia', + name='duration', + field=models.IntegerField(blank=True, null=True), + ), + ] diff --git a/src/catalogue/migrations/0036_book_toc.py b/src/catalogue/migrations/0036_book_toc.py new file mode 100644 index 000000000..4a9dc87c8 --- /dev/null +++ b/src/catalogue/migrations/0036_book_toc.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.25 on 2022-03-10 14:18 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('catalogue', '0035_bookmedia_duration'), + ] + + operations = [ + migrations.AddField( + model_name='book', + name='toc', + field=models.TextField(blank=True, verbose_name='toc'), + ), + ] diff --git a/src/catalogue/models/book.py b/src/catalogue/models/book.py index ee8595588..01f02816a 100644 --- a/src/catalogue/models/book.py +++ b/src/catalogue/models/book.py @@ -8,6 +8,7 @@ from random import randint import os.path import re from urllib.request import urlretrieve +from django.apps import apps from django.conf import settings from django.db import connection, models, transaction import django.dispatch @@ -17,7 +18,7 @@ from django.urls import reverse from django.utils.translation import ugettext_lazy as _, get_language from django.utils.deconstruct import deconstructible from fnpdjango.storage import BofhFileSystemStorage - +from lxml import html from librarian.cover import WLCover from librarian.html import transform_abstrakt from newtagging import managers @@ -43,6 +44,7 @@ class UploadToPath(object): _cover_upload_to = UploadToPath('book/cover/%s.jpg') +_cover_clean_upload_to = UploadToPath('book/cover_clean/%s.jpg') _cover_thumb_upload_to = UploadToPath('book/cover_thumb/%s.jpg') _cover_api_thumb_upload_to = UploadToPath('book/cover_api_thumb/%s.jpg') _simple_cover_upload_to = UploadToPath('book/cover_simple/%s.jpg') @@ -64,6 +66,7 @@ class Book(models.Model): language = models.CharField(_('language code'), max_length=3, db_index=True, default=app_settings.DEFAULT_LANGUAGE) description = models.TextField(_('description'), blank=True) abstract = models.TextField(_('abstract'), blank=True) + toc = models.TextField(_('toc'), blank=True) created_at = models.DateTimeField(_('creation date'), auto_now_add=True, db_index=True) changed_at = models.DateTimeField(_('change date'), auto_now=True, db_index=True) parent_number = models.IntegerField(_('parent number'), default=0) @@ -86,6 +89,13 @@ class Book(models.Model): storage=bofh_storage, max_length=255) cover_etag = models.CharField(max_length=255, editable=False, default='', db_index=True) # Cleaner version of cover for thumbs + cover_clean = EbookField( + 'cover_clean', _('clean cover'), + null=True, blank=True, + upload_to=_cover_clean_upload_to, + max_length=255 + ) + cover_clean_etag = models.CharField(max_length=255, editable=False, default='', db_index=True) cover_thumb = EbookField( 'cover_thumb', _('cover thumbnail'), null=True, blank=True, @@ -286,6 +296,17 @@ class Book(models.Model): return sibling.get_first_text() return self.parent.get_next_text() + def get_child_audiobook(self): + BookMedia = apps.get_model('catalogue', 'BookMedia') + if not BookMedia.objects.filter(book__ancestor=self).exists(): + return None + for child in self.children.all(): + if child.has_mp3_file(): + return child + child_sub = child.get_child_audiobook() + if child_sub is not None: + return child_sub + def get_siblings(self): if not self.parent: return [] @@ -429,6 +450,7 @@ class Book(models.Model): audiobooks = [] projects = set() + total_duration = 0 for mp3 in self.media.filter(type='mp3').iterator(): # ogg files are always from the same project meta = mp3.get_extra_info_json() @@ -438,6 +460,7 @@ class Book(models.Model): project = 'CzytamySłuchając' projects.add((project, meta.get('funded_by', ''))) + total_duration += mp3.duration or 0 media = {'mp3': mp3} @@ -447,7 +470,11 @@ class Book(models.Model): audiobooks.append(media) projects = sorted(projects) - return audiobooks, projects + total_duration = '%d:%02d' % ( + total_duration // 60, + total_duration % 60 + ) + return audiobooks, projects, total_duration def wldocument(self, parse_dublincore=True, inherit=True): from catalogue.import_utils import ORMDocProvider @@ -542,6 +569,20 @@ class Book(models.Model): else: self.abstract = '' + def load_toc(self): + self.toc = '' + if self.html_file: + parser = html.HTMLParser(encoding='utf-8') + tree = html.parse(self.html_file.path, parser=parser) + toc = tree.find('//div[@id="toc"]/ol') + if toc is None or not len(toc): + return + html_link = reverse('book_text', args=[self.slug]) + for a in toc.findall('.//a'): + a.attrib['href'] = html_link + a.attrib['href'] + self.toc = html.tostring(toc, encoding='unicode') + # div#toc + @classmethod def from_xml_file(cls, xml_file, **kwargs): from django.core.files import File @@ -607,6 +648,7 @@ class Book(models.Model): book.common_slug = book.slug book.extra_info = json.dumps(book_info.to_dict()) book.load_abstract() + book.load_toc() book.save() meta_tags = Tag.tags_from_info(book_info) @@ -649,6 +691,7 @@ class Book(models.Model): # Build cover. if 'cover' not in dont_build: book.cover.build_delay() + book.cover_clean.build_delay() book.cover_thumb.build_delay() book.cover_api_thumb.build_delay() book.simple_cover.build_delay() @@ -793,9 +836,11 @@ class Book(models.Model): if not self.cover_info(inherit=False): if 'cover' not in app_settings.DONT_BUILD: self.cover.build_delay() + self.cover_clean.build_delay() self.cover_thumb.build_delay() self.cover_api_thumb.build_delay() self.simple_cover.build_delay() + self.cover_ebookpoint.build_delay() for format_ in constants.EBOOK_FORMATS_WITH_COVERS: if format_ not in app_settings.DONT_BUILD: getattr(self, '%s_file' % format_).build_delay() diff --git a/src/catalogue/models/bookmedia.py b/src/catalogue/models/bookmedia.py index e2fc34370..c08ed1152 100644 --- a/src/catalogue/models/bookmedia.py +++ b/src/catalogue/models/bookmedia.py @@ -7,7 +7,8 @@ from collections import namedtuple from django.db import models from django.utils.translation import ugettext_lazy as _ from slugify import slugify -from mutagen import MutagenError +import mutagen +from mutagen import id3 from catalogue.fields import OverwriteStorage @@ -37,6 +38,7 @@ class BookMedia(models.Model): part_name = models.CharField(_('part name'), default='', blank=True, max_length=512) index = models.IntegerField(_('index'), default=0) file = models.FileField(_('file'), max_length=600, upload_to=_file_upload_to, storage=OverwriteStorage()) + duration = models.IntegerField(null=True, blank=True) uploaded_at = models.DateTimeField(_('creation date'), auto_now_add=True, editable=False, db_index=True) project_description = models.CharField(max_length=2048, blank=True) project_icon = models.CharField(max_length=2048, blank=True) @@ -96,15 +98,19 @@ class BookMedia(models.Model): extra_info.update(self.read_meta()) self.extra_info = json.dumps(extra_info) self.source_sha1 = self.read_source_sha1(self.file.path, self.type) + self.duration = self.read_duration() return super(BookMedia, self).save(*args, **kwargs) + def read_duration(self): + try: + return mutagen.File(self.file.path).info.length + except: + return None + def read_meta(self): """ Reads some metadata from the audiobook. """ - import mutagen - from mutagen import id3 - artist_name = director_name = project = funded_by = license = '' if self.type == 'mp3': try: @@ -118,7 +124,7 @@ class BookMedia(models.Model): funded_by = ", ".join([ t.data.decode('utf-8') for t in audio.getall('PRIV') if t.owner == 'wolnelektury.pl?funded_by']) - except MutagenError: + except mutagen.MutagenError: pass elif self.type == 'ogg': try: @@ -128,7 +134,7 @@ class BookMedia(models.Model): license = ', '.join(audio.get('license', [])) project = ", ".join(audio.get('project', [])) funded_by = ", ".join(audio.get('funded_by', [])) - except (MutagenError, AttributeError): + except (mutagen.MutagenError, AttributeError): pass else: return {} @@ -143,21 +149,18 @@ class BookMedia(models.Model): """ Reads source file SHA1 from audiobok metadata. """ - import mutagen - from mutagen import id3 - if filetype == 'mp3': try: audio = id3.ID3(filepath) return [t.data.decode('utf-8') for t in audio.getall('PRIV') if t.owner == 'wolnelektury.pl?flac_sha1'][0] - except (MutagenError, IndexError): + except (mutagen.MutagenError, IndexError): return None elif filetype == 'ogg': try: audio = mutagen.File(filepath) return audio.get('flac_sha1', [None])[0] - except (MutagenError, AttributeError, IndexError): + except (mutagen.MutagenError, AttributeError, IndexError): return None else: return None diff --git a/src/catalogue/models/edition.py b/src/catalogue/models/edition.py new file mode 100644 index 000000000..d04dd0aa9 --- /dev/null +++ b/src/catalogue/models/edition.py @@ -0,0 +1,10 @@ +from django.db import models + + +class Edition(models.Model): + book = models.ForeignKey('Book', models.CASCADE) + identifier = models.CharField(max_length=32) + created_at = models.DateTimeField(auto_now_add=True) + + + diff --git a/src/catalogue/templates/catalogue/2021/book_detail.html b/src/catalogue/templates/catalogue/2021/book_detail.html index 36515faba..5b6c1f8eb 100644 --- a/src/catalogue/templates/catalogue/2021/book_detail.html +++ b/src/catalogue/templates/catalogue/2021/book_detail.html @@ -72,7 +72,7 @@
-
-
-

Ta książka jest dostępna dla tysięcy dzieciaków dzięki darowiznom od osób takich jak Ty!

-
- Dorzuć się! - Dorzuć się! -
+
+

Ta książka jest dostępna dla tysięcy dzieciaków dzięki darowiznom od osób takich jak Ty!

+
+ Dorzuć się! + Dorzuć się! +
+
@@ -350,21 +349,23 @@ -
-
-
- {% choose_cites book 3 as cites %} - {% for fragment in cites %} -
- - {{ fragment.short_text|safe }} - -

{{ fragment.book.pretty_title }}

-
- {% endfor %} + {% choose_cites book 3 as cites %} + {% if cites %} +
+
+
+ {% for fragment in cites %} +
+ + {{ fragment.short_text|safe }} + +

{{ fragment.book.pretty_title }}

+
+ {% endfor %} +
-
+ {% endif %}
{% endfor %} @@ -483,7 +484,7 @@
- + {% javascript '2022' %} {% javascript '2022_player' %} diff --git a/src/catalogue/templates/catalogue/snippets/2022_jplayer.html b/src/catalogue/templates/catalogue/snippets/2022_jplayer.html index 9201e727b..2f7caf69d 100644 --- a/src/catalogue/templates/catalogue/snippets/2022_jplayer.html +++ b/src/catalogue/templates/catalogue/snippets/2022_jplayer.html @@ -1,70 +1,75 @@ {% load i18n catalogue_tags %} -
- +{% with audiobooks=book.get_audiobooks %} +
+ -
- - -
-
-
-
- - - -
-
-
- -
-
- + -
+
+{% endwith %} diff --git a/src/catalogue/templates/catalogue/snippets/2022_jplayer_link.html b/src/catalogue/templates/catalogue/snippets/2022_jplayer_link.html new file mode 100644 index 000000000..b49175a30 --- /dev/null +++ b/src/catalogue/templates/catalogue/snippets/2022_jplayer_link.html @@ -0,0 +1,21 @@ +{% load i18n catalogue_tags %} + +
+ + +
+ + ⏭ + +
+
+ {{ book.title }} +
+ + + +
+
+
diff --git a/src/wolnelektury/static/2022/styles/components/_player.scss b/src/wolnelektury/static/2022/styles/components/_player.scss index 060429826..9e4095088 100644 --- a/src/wolnelektury/static/2022/styles/components/_player.scss +++ b/src/wolnelektury/static/2022/styles/components/_player.scss @@ -69,13 +69,20 @@ height: 10px; width: 100%; display: block; - background: #FFFFFF; + background: #F2F2F2; border-radius: 5px; position: relative; overflow: hidden; - span { - width: 40%; + span.jp-seek-bar { + height: 100%; + position: absolute; + border-radius: 5px; + background-color: #FFFFFF; + top: 0; left: 0; + } + + span.jp-play-bar { height: 100%; position: absolute; border-radius: 5px; @@ -85,6 +92,20 @@ } } + +.c-player__title { + top: -27px; + left: 0; + position: absolute; + font-style: normal; + font-weight: normal; + font-size: 12px; + line-height: 140%; + letter-spacing: 0.05em; + color: #083F4D; +} + + .c-player__info { top: -27px; right: 0; @@ -102,7 +123,7 @@ display: block; position: relative; - span { + > span { position: absolute; padding-top: 10px; font-style: normal; diff --git a/src/wolnelektury/static/2022/styles/layout/_article.scss b/src/wolnelektury/static/2022/styles/layout/_article.scss index 66e438148..a40be7b1c 100644 --- a/src/wolnelektury/static/2022/styles/layout/_article.scss +++ b/src/wolnelektury/static/2022/styles/layout/_article.scss @@ -25,7 +25,7 @@ font-weight: bolder; } - ul { + ol { padding: 0; list-style: none; margin-top: 0.25rem; diff --git a/src/wolnelektury/static/js/current.js b/src/wolnelektury/static/js/current.js new file mode 100644 index 000000000..e963dd266 --- /dev/null +++ b/src/wolnelektury/static/js/current.js @@ -0,0 +1,35 @@ +(function($){ + $(function() { + + + if (Modernizr.localstorage) { + try { + audiobooks = JSON.parse(localStorage["audiobook-history"]); + } catch { + audiobooks = {}; + } + + latest = []; + Object.keys(audiobooks).forEach(function(slug) { + [ts, media_id, time] = audiobooks[slug]; + latest.push([ts, slug]); + }); + latest.sort().reverse().forEach(function(item) { + [ts, slug] = item; + $newitem = $('
'); // remove from history + $("#last-audiobooks").append($newitem); + (function($box) { + $.get( + "/katalog/lektura/" + slug + "/mini_box.html", + function(data) { + console.log(data); + $box.html(data); + $("#personal-history").slideDown("slow"); + }).fail(function() { + $box.remove(); + }); + })($newitem); + }); + } + }); +})(jQuery); -- 2.20.1