--- /dev/null
+# 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'),
+ ),
+ ]
--- /dev/null
+# 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),
+ ),
+ ]
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
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()
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 []
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:
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
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
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):
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:
$(".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;
};
// TODO: if snap then roll
locator.removeClass('up').removeClass('down');
if (locator.hasClass('snap')) {
- console.log('SCROLL!');
scrollTo();
} else {
if (y < miny) {
});
$('.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']);
_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'];
initialTime = last[2];
}
}
- console.log('READY 5!', initialElem, initialTime);
setMedia($(initialElem), initialTime);
- console.log('READY 6!');
},
timeupdate: function(event) {
ended: function(event) {
- let p = $currentMedia.next();
+ let p = $currentMedia.nextAll('li');
if (p.length) {
setMedia(p).jPlayer("play");
}
{% if book.is_book %}
<span class="icon icon-book-alt" title="{% trans 'książka' %}"></span>
{% endif %}
- {% if book.has_mp3_file %}
+ {% if book.has_audio %}
<span class="icon icon-audio" title="{% trans 'audiobook' %}"></span>
{% endif %}
{% if book.is_picture %}
</div>
<div class="c-media__actions lay-col lay-l-row">
<div class="c-media__btn">
- {% if book.has_mp3_file %}
+ {% if book.has_audio %}
<button class="l-button l-button--media" id="audiobook"><i class="icon icon-audio"></i> {% trans "pobierz audiobook" %}</button>
{% endif %}
</div>
</div>
-
- {% 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 %}
-
-
<div class="c-media__popup" data-popup="ebook">
<div class="c-media__popup__box">
<div class="c-media__popup__box__lead">
</div>
{% endif %}
+ {% if narrated %}
+ <div class="l-section l-section--col">
+ <h2 class="header">Audiobooki</h2>
+ <div class="l-books__grid" id="book-list">
+ {% for book in narrated %}
+ {% include "catalogue/book_box.html" %}
+ {% endfor %}
+ </div>
+ </div>
+ {% endif %}
+
{% if main_tag %}
<section class="l-section">
<div class="l-author">
{% load i18n l10n catalogue_tags %}
-{% with audiobooks=book.get_audiobooks %}
+{% with audiobooks=book.get_audiobooks_with_children %}
<div class="c-media__player" id="jp_container_{{ book.pk }}" data-book-slug="{{ book.slug }}">
<div class="jp-jplayer" data-player="jp_container_{{ book.pk }}"
data-supplied="oga,mp3"></div>
<ul class="jp-playlist">
{% localize off %}
{% for i in audiobooks.0 %}
+ {% if i.part %}
+ <div class="title"><strong>{{ i.part.title }}</strong></div>
+ {% else %}
<li
data-mp3='{{ i.mp3.file.url }}'
data-ogg='{{ i.ogg.file.url }}'
</span>
{% endwith %}
</li>
+ {% endif %}
{% endfor %}
{% endlocalize %}
</ul>
+++ /dev/null
-{% load i18n catalogue_tags %}
-
-<div class="c-media__player" id="jp_container_{{ book.pk }}">
- <div class="c-player__head">
- <span> </span>
- </div>
-
- <div class="c-player">
- <div class="c-player__btns">
- <button disabled class="play-prev"><i class="icon icon-prev"></i></button>
- <button disabled class="c-player__btn--md"><i class="icon icon-play"></i></button>
- <form action='{{ book.get_absolute_url }}'>
- <button class="play-next"><i class="icon icon-next"></i></button>
- </form>
- </div>
-
- <div class="c-player__timeline">
- <div class="c-player__info">{{ book.pretty_title }}</div>
- </div>
-
- </div>
- <div class="c-media__caption">
- </div>
-</div>
]
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)
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;
+ }
}
}