From: Radek Czajka Date: Tue, 19 Aug 2025 10:14:04 +0000 (+0200) Subject: Audiobooks: narrators as authors, and top-level players. X-Git-Url: https://git.mdrn.pl/wolnelektury.git/commitdiff_plain/3d8d1e8e463efe111ed2e2a99861ada52759a1d9?hp=9466f2ccde63244ac78465ea08cb7c1b97080f26 Audiobooks: narrators as authors, and top-level players. --- diff --git a/src/catalogue/migrations/0050_book_narrators.py b/src/catalogue/migrations/0050_book_narrators.py new file mode 100644 index 000000000..388f150e9 --- /dev/null +++ b/src/catalogue/migrations/0050_book_narrators.py @@ -0,0 +1,18 @@ +# Generated by Django 4.0.8 on 2025-08-12 09:37 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('catalogue', '0049_book_html_nonotes_file_book_html_nonotes_file_etag_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='book', + name='narrators', + field=models.ManyToManyField(blank=True, related_name='narrated', to='catalogue.tag'), + ), + ] diff --git a/src/catalogue/migrations/0051_book_has_audio.py b/src/catalogue/migrations/0051_book_has_audio.py new file mode 100644 index 000000000..e333bff6c --- /dev/null +++ b/src/catalogue/migrations/0051_book_has_audio.py @@ -0,0 +1,18 @@ +# Generated by Django 4.0.8 on 2025-08-12 10:28 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('catalogue', '0050_book_narrators'), + ] + + operations = [ + migrations.AddField( + model_name='book', + name='has_audio', + field=models.BooleanField(default=False), + ), + ] diff --git a/src/catalogue/models/book.py b/src/catalogue/models/book.py index ff09853f5..0400656e2 100644 --- a/src/catalogue/models/book.py +++ b/src/catalogue/models/book.py @@ -7,6 +7,8 @@ from datetime import date, timedelta from random import randint import os.path import re +from slugify import slugify +from sortify import sortify from urllib.request import urlretrieve from django.apps import apps from django.conf import settings @@ -94,6 +96,8 @@ class Book(models.Model): tags = managers.TagDescriptor(Tag) tag_relations = GenericRelation(Tag.intermediary_table_model) translators = models.ManyToManyField(Tag, blank=True) + narrators = models.ManyToManyField(Tag, blank=True, related_name='narrated') + has_audio = models.BooleanField(default=False) html_built = django.dispatch.Signal() published = django.dispatch.Signal() @@ -266,17 +270,6 @@ class Book(models.Model): return sibling.get_first_text() return self.parent.get_next_text(inside=False) - 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.order_by('parent_number').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 [] @@ -337,9 +330,6 @@ class Book(models.Model): else: return self.media.filter(type=type_).exists() - def has_audio(self): - return self.has_media('mp3') - def get_media(self, type_): if self.has_media(type_): if type_ in Book.formats: @@ -505,7 +495,7 @@ class Book(models.Model): def media_audio_epub(self): return self.get_media('audio.epub') - def get_audiobooks(self): + def get_audiobooks(self, with_children=False, processing=False): ogg_files = {} for m in self.media.filter(type='ogg').order_by().iterator(): ogg_files[m.name] = m @@ -531,13 +521,27 @@ class Book(models.Model): media['ogg'] = ogg audiobooks.append(media) - projects = sorted(projects) - total_duration = '%d:%02d' % ( - total_duration // 60, - total_duration % 60 - ) + if with_children: + for child in self.get_children(): + ch_audiobooks, ch_projects, ch_duration = child.get_audiobooks( + with_children=True, processing=True) + audiobooks.append({'part': child}) + audiobooks += ch_audiobooks + projects.update(ch_projects) + total_duration += ch_duration + + if not processing: + projects = sorted(projects) + total_duration = '%d:%02d' % ( + total_duration // 60, + total_duration % 60 + ) + return audiobooks, projects, total_duration + def get_audiobooks_with_children(self): + return self.get_audiobooks(with_children=True) + def wldocument(self, parse_dublincore=True, inherit=True): from catalogue.import_utils import ORMDocProvider from librarian.parser import WLDocument @@ -830,6 +834,42 @@ class Book(models.Model): def references(self): return self.reference_set.all().select_related('entity') + def update_has_audio(self): + self.has_audio = False + if self.media.filter(type='mp3').exists(): + self.has_audio = True + if self.descendant.filter(has_audio=True).exists(): + self.has_audio = True + self.save(update_fields=['has_audio']) + if self.parent is not None: + self.parent.update_has_audio() + + def update_narrators(self): + narrator_names = set() + for bm in self.media.filter(type='mp3'): + narrator_names.update(set( + a.strip() for a in re.split(r',|\si\s', bm.artist) + )) + narrators = [] + + for name in narrator_names: + if not name: continue + slug = slugify(name) + try: + t = Tag.objects.get(category='author', slug=slug) + except Tag.DoesNotExist: + sort_key = sortify( + ' '.join(name.rsplit(' ', 1)[::-1]).lower() + ) + t = Tag.objects.create( + category='author', + name_pl=name, + slug=slug, + sort_key=sort_key, + ) + narrators.append(t) + self.narrators.set(narrators) + @classmethod @transaction.atomic def repopulate_ancestors(cls): diff --git a/src/catalogue/models/bookmedia.py b/src/catalogue/models/bookmedia.py index 0a1544fec..87d7f7803 100644 --- a/src/catalogue/models/bookmedia.py +++ b/src/catalogue/models/bookmedia.py @@ -101,7 +101,9 @@ class BookMedia(models.Model): 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) + super(BookMedia, self).save(*args, **kwargs) + self.book.update_narrators() + self.book.update_has_audio() def read_duration(self): try: diff --git a/src/catalogue/static/player/player.js b/src/catalogue/static/player/player.js index cbf90261b..f94dc872b 100644 --- a/src/catalogue/static/player/player.js +++ b/src/catalogue/static/player/player.js @@ -114,31 +114,28 @@ $(".c-media__caption .license", $root).html($(".license", elem).html()); $(".c-media__caption .project-logo", $root).html($(".project-icon", elem).html()); - console.log('sm 1'); doesUpdateSynchro = false; if (!$currentMedia || $currentMedia[0] != elem[0]) { - console.log('set', player.jPlayer("setMedia", media)) player.jPlayer("option", "playbackRate", speed); } doesUpdateSynchro = true; player.jPlayer(cmd, time); $currentMedia = elem; - $(".play-next", $root).prop("disabled", !elem.next().length); + $(".play-next", $root).prop("disabled", !elem.nextAll('li').length); let du = parseFloat(elem.data('duration')); currentDuration = du; - elem.nextAll().each(function() { + elem.nextAll('li').each(function() { du += parseFloat($(this).data('duration')); }); totalDurationLeft = du; let pdu = 0; - elem.prevAll().each(function() { + elem.prevAll('li').each(function() { pdu += parseFloat($(this).data('duration')); }); totalDurationBefore = pdu; - console.log('sm 3', du, pdu); return player; }; @@ -166,7 +163,6 @@ // TODO: if snap then roll locator.removeClass('up').removeClass('down'); if (locator.hasClass('snap')) { - console.log('SCROLL!'); scrollTo(); } else { if (y < miny) { @@ -212,14 +208,14 @@ }); $('.play-next', $root).click(function() { - let p = $currentMedia.next(); + let p = $currentMedia.nextAll('li').first(); if (p.length) { setMedia(p).jPlayer("play"); _paq.push(['trackEvent', 'audiobook', 'next']); } }); $('.play-prev', $root).click(function() { - let p = $currentMedia.prev(); + let p = $currentMedia.prevAll('li').first(); if (p.length) { setMedia(p).jPlayer("play"); _paq.push(['trackEvent', 'audiobook', 'prev']); @@ -236,10 +232,8 @@ _paq.push(['trackEvent', 'audiobook', 'chapter']); }); - console.log('READY 3!'); var initialElem = $('.jp-playlist li', $root).first(); var initialTime = 0; - console.log('READY 4!'); if (true || Modernizr.localstorage) { try { let speedStr = localStorage['audiobook-speed']; @@ -266,9 +260,7 @@ initialTime = last[2]; } } - console.log('READY 5!', initialElem, initialTime); setMedia($(initialElem), initialTime); - console.log('READY 6!'); }, timeupdate: function(event) { @@ -310,7 +302,7 @@ ended: function(event) { - let p = $currentMedia.next(); + let p = $currentMedia.nextAll('li'); if (p.length) { setMedia(p).jPlayer("play"); } diff --git a/src/catalogue/templates/catalogue/book_box.html b/src/catalogue/templates/catalogue/book_box.html index 4458b5a22..b82d926e8 100644 --- a/src/catalogue/templates/catalogue/book_box.html +++ b/src/catalogue/templates/catalogue/book_box.html @@ -34,7 +34,7 @@ {% if book.is_book %} {% endif %} - {% if book.has_mp3_file %} + {% if book.has_audio %} {% endif %} {% if book.is_picture %} diff --git a/src/catalogue/templates/catalogue/book_detail.html b/src/catalogue/templates/catalogue/book_detail.html index 6c89f1884..016b2f2db 100644 --- a/src/catalogue/templates/catalogue/book_detail.html +++ b/src/catalogue/templates/catalogue/book_detail.html @@ -162,7 +162,7 @@
- {% if book.has_mp3_file %} + {% if book.has_audio %} {% endif %}
@@ -184,21 +184,11 @@
- - {% if book.has_mp3_file %} + {% if book.has_audio %} {% include 'catalogue/snippets/jplayer.html' %} - {% else %} - {% with ch=book.get_child_audiobook %} - {% if ch %} - {% include 'catalogue/snippets/jplayer_link.html' with book=ch %} - {% endif %} - {% endwith %} - {% endif %} - -
diff --git a/src/catalogue/templates/catalogue/book_list.html b/src/catalogue/templates/catalogue/book_list.html index c57bdd3f9..2a3db148c 100644 --- a/src/catalogue/templates/catalogue/book_list.html +++ b/src/catalogue/templates/catalogue/book_list.html @@ -129,6 +129,17 @@
{% endif %} + {% if narrated %} +
+

Audiobooki

+
+ {% for book in narrated %} + {% include "catalogue/book_box.html" %} + {% endfor %} +
+
+ {% endif %} + {% if main_tag %}
diff --git a/src/catalogue/templates/catalogue/snippets/jplayer.html b/src/catalogue/templates/catalogue/snippets/jplayer.html index e0893facb..2399a09f0 100644 --- a/src/catalogue/templates/catalogue/snippets/jplayer.html +++ b/src/catalogue/templates/catalogue/snippets/jplayer.html @@ -1,6 +1,6 @@ {% load i18n l10n catalogue_tags %} -{% with audiobooks=book.get_audiobooks %} +{% with audiobooks=book.get_audiobooks_with_children %}
@@ -38,6 +38,9 @@ diff --git a/src/catalogue/templates/catalogue/snippets/jplayer_link.html b/src/catalogue/templates/catalogue/snippets/jplayer_link.html deleted file mode 100644 index 7bbddcdda..000000000 --- a/src/catalogue/templates/catalogue/snippets/jplayer_link.html +++ /dev/null @@ -1,24 +0,0 @@ -{% load i18n catalogue_tags %} - -
-
-   -
- -
-
- - -
- -
-
- -
-
{{ book.pretty_title }}
-
- -
-
-
-
diff --git a/src/catalogue/views.py b/src/catalogue/views.py index 3c4a55fe9..d5b83ab08 100644 --- a/src/catalogue/views.py +++ b/src/catalogue/views.py @@ -227,6 +227,7 @@ class TaggedObjectList(BookList): ] if len(self.ctx['tags']) == 1 and self.ctx['main_tag'].category == 'author': self.ctx['translation_list'] = self.ctx['main_tag'].book_set.all() + self.ctx['narrated'] = self.ctx['main_tag'].narrated.all() def get_queryset(self): qs = Book.tagged.with_all(self.ctx['work_tags']).filter(findable=True) diff --git a/src/wolnelektury/static/2022/styles/components/_player.scss b/src/wolnelektury/static/2022/styles/components/_player.scss index 52729f9f6..d1fb096f1 100644 --- a/src/wolnelektury/static/2022/styles/components/_player.scss +++ b/src/wolnelektury/static/2022/styles/components/_player.scss @@ -296,6 +296,15 @@ display: inline; } } + div.title { + font-size: 12px; + line-height: 140%; + letter-spacing: 0.05em; + list-style-type: decimal; + margin: 5px 0; + color: white; + cursor: pointer; + } } }