From cf1c5c7118e320bdd5b9fa6dcef8585ba117ebb6 Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Fri, 29 May 2020 15:32:26 +0200 Subject: [PATCH 01/16] More automation for YouTube: volume management (WiP). --- src/archive/constants.py | 22 +++++++++-- src/archive/migrations/0011_audiobook_slug.py | 19 ++++++++++ src/archive/migrations/0012_url_to_slug.py | 30 +++++++++++++++ .../migrations/0013_remove_audiobook_url.py | 17 +++++++++ .../0014_remove_audiobook_parts_count.py | 17 +++++++++ .../migrations/0015_auto_20200529_1430.py | 23 +++++++++++ .../migrations/0016_auto_20200529_1528.py | 38 +++++++++++++++++++ src/archive/models.py | 38 ++++++++++++++++--- src/archive/tasks.py | 5 ++- src/archive/templates/archive/book.html | 29 ++++++++++++++ src/archive/urls.py | 2 + src/archive/views.py | 20 ++++++---- src/youtube/urls.py | 1 - src/youtube/views.py | 10 ++--- 14 files changed, 247 insertions(+), 24 deletions(-) create mode 100644 src/archive/migrations/0011_audiobook_slug.py create mode 100644 src/archive/migrations/0012_url_to_slug.py create mode 100644 src/archive/migrations/0013_remove_audiobook_url.py create mode 100644 src/archive/migrations/0014_remove_audiobook_parts_count.py create mode 100644 src/archive/migrations/0015_auto_20200529_1430.py create mode 100644 src/archive/migrations/0016_auto_20200529_1528.py create mode 100644 src/archive/templates/archive/book.html diff --git a/src/archive/constants.py b/src/archive/constants.py index 6cb92a7..3493e00 100644 --- a/src/archive/constants.py +++ b/src/archive/constants.py @@ -1,14 +1,28 @@ from django.utils.translation import gettext_lazy as _ class status: - WAITING = 1 - ENCODING = 2 - TAGGING = 3 - SENDING = 4 + QUEUED = 1 + WAITING = 10 + ENCODING = 20 + TAGGING = 30 + CONVERTING_AUDIO = 40 + CONVERTING_VIDEO = 50 + ASSEMBLING_AUDIO = 60 + ASSEMBLING_VIDEO = 70 + JOINING_AUDIO_VIDEO = 80 + SENDING = 100 + SETTING_THUMBNAIL = 110 choices = [ + (QUEUED, _('Queued')), (WAITING, _('Waiting')), (ENCODING, _('Encoding')), (TAGGING, _('Tagging')), + (CONVERTING_AUDIO, _('Converting audio')), + (CONVERTING_VIDEO, _('Converting video')), + (ASSEMBLING_AUDIO, _('Assembling audio')), + (ASSEMBLING_VIDEO, _('Assembling video')), + (JOINING_AUDIO_VIDEO, _('Joining audio and video')), (SENDING, _('Sending')), + (SETTING_THUMBNAIL, _('Setting thumbnail')), ] diff --git a/src/archive/migrations/0011_audiobook_slug.py b/src/archive/migrations/0011_audiobook_slug.py new file mode 100644 index 0000000..520a0d8 --- /dev/null +++ b/src/archive/migrations/0011_audiobook_slug.py @@ -0,0 +1,19 @@ +# Generated by Django 3.0.6 on 2020-05-29 11:57 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('archive', '0010_populate_license'), + ] + + operations = [ + migrations.AddField( + model_name='audiobook', + name='slug', + field=models.SlugField(default='', max_length=120), + preserve_default=False, + ), + ] diff --git a/src/archive/migrations/0012_url_to_slug.py b/src/archive/migrations/0012_url_to_slug.py new file mode 100644 index 0000000..d880c95 --- /dev/null +++ b/src/archive/migrations/0012_url_to_slug.py @@ -0,0 +1,30 @@ +# Generated by Django 3.0.6 on 2020-05-29 11:57 + +from django.db import migrations + + +def url_to_slug(apps, schema_editor): + Audiobook = apps.get_model('archive', 'Audiobook') + for a in Audiobook.objects.all(): + a.slug = a.url.rstrip().rstrip('/').rsplit('/', 1)[-1] + a.save() + +def slug_to_url(apps, schema_editor): + Audiobook = apps.get_model('archive', 'Audiobook') + for a in Audiobook.objects.all(): + a.url = 'https://wolnelektury.pl/katalog/lektura/{}/'.format(a.slug) + a.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('archive', '0011_audiobook_slug'), + ] + + operations = [ + migrations.RunPython( + url_to_slug, + slug_to_url + ) + ] diff --git a/src/archive/migrations/0013_remove_audiobook_url.py b/src/archive/migrations/0013_remove_audiobook_url.py new file mode 100644 index 0000000..84a6741 --- /dev/null +++ b/src/archive/migrations/0013_remove_audiobook_url.py @@ -0,0 +1,17 @@ +# Generated by Django 3.0.6 on 2020-05-29 12:10 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('archive', '0012_url_to_slug'), + ] + + operations = [ + migrations.RemoveField( + model_name='audiobook', + name='url', + ), + ] diff --git a/src/archive/migrations/0014_remove_audiobook_parts_count.py b/src/archive/migrations/0014_remove_audiobook_parts_count.py new file mode 100644 index 0000000..14be63f --- /dev/null +++ b/src/archive/migrations/0014_remove_audiobook_parts_count.py @@ -0,0 +1,17 @@ +# Generated by Django 3.0.6 on 2020-05-29 14:23 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('archive', '0013_remove_audiobook_url'), + ] + + operations = [ + migrations.RemoveField( + model_name='audiobook', + name='parts_count', + ), + ] diff --git a/src/archive/migrations/0015_auto_20200529_1430.py b/src/archive/migrations/0015_auto_20200529_1430.py new file mode 100644 index 0000000..2702f97 --- /dev/null +++ b/src/archive/migrations/0015_auto_20200529_1430.py @@ -0,0 +1,23 @@ +# Generated by Django 3.0.6 on 2020-05-29 14:30 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('archive', '0014_remove_audiobook_parts_count'), + ] + + operations = [ + migrations.AddField( + model_name='audiobook', + name='youtube_queued', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name='audiobook', + name='youtube_volume', + field=models.CharField(blank=True, help_text='If set, audiobooks with the save value will be published as single YouTube video.', max_length=1000, verbose_name='Volume name for YouTube'), + ), + ] diff --git a/src/archive/migrations/0016_auto_20200529_1528.py b/src/archive/migrations/0016_auto_20200529_1528.py new file mode 100644 index 0000000..65c4cb3 --- /dev/null +++ b/src/archive/migrations/0016_auto_20200529_1528.py @@ -0,0 +1,38 @@ +# Generated by Django 3.0.6 on 2020-05-29 15:28 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('archive', '0015_auto_20200529_1430'), + ] + + operations = [ + migrations.AlterField( + model_name='audiobook', + name='index', + field=models.IntegerField(default=0, help_text='Ordering of parts of a book.', verbose_name='index'), + ), + migrations.AlterField( + model_name='audiobook', + name='mp3_status', + field=models.SmallIntegerField(choices=[(1, 'Queued'), (10, 'Waiting'), (20, 'Encoding'), (30, 'Tagging'), (40, 'Converting audio'), (50, 'Converting video'), (60, 'Assembling audio'), (70, 'Assembling video'), (80, 'Joining audio and video'), (100, 'Sending'), (110, 'Setting thumbnail')], editable=False, null=True), + ), + migrations.AlterField( + model_name='audiobook', + name='ogg_status', + field=models.SmallIntegerField(choices=[(1, 'Queued'), (10, 'Waiting'), (20, 'Encoding'), (30, 'Tagging'), (40, 'Converting audio'), (50, 'Converting video'), (60, 'Assembling audio'), (70, 'Assembling video'), (80, 'Joining audio and video'), (100, 'Sending'), (110, 'Setting thumbnail')], editable=False, null=True), + ), + migrations.AlterField( + model_name='audiobook', + name='slug', + field=models.SlugField(help_text='WL catalogue slug of the book.', max_length=120), + ), + migrations.AlterField( + model_name='audiobook', + name='youtube_status', + field=models.SmallIntegerField(choices=[(1, 'Queued'), (10, 'Waiting'), (20, 'Encoding'), (30, 'Tagging'), (40, 'Converting audio'), (50, 'Converting video'), (60, 'Assembling audio'), (70, 'Assembling video'), (80, 'Joining audio and video'), (100, 'Sending'), (110, 'Setting thumbnail')], editable=False, null=True), + ), + ] diff --git a/src/archive/models.py b/src/archive/models.py index a4420a9..ad1ef78 100644 --- a/src/archive/models.py +++ b/src/archive/models.py @@ -66,14 +66,14 @@ class Audiobook(models.Model): title = models.CharField(max_length=255, verbose_name=_('title')) part_name = models.CharField(max_length=255, verbose_name=_('part name'), help_text=_('eg. chapter in a novel'), default='', blank=True) - index = models.IntegerField(verbose_name=_('index'), default=0) - parts_count = models.IntegerField(verbose_name=_('parts count'), default=1) + index = models.IntegerField(verbose_name=_('index'), default=0, help_text=_('Ordering of parts of a book.')) + youtube_volume = models.CharField(_('Volume name for YouTube'), max_length=1000, blank=True, help_text=_('If set, audiobooks with the save value will be published as single YouTube video.')) artist = models.CharField(max_length=255, verbose_name=_('artist')) conductor = models.CharField(max_length=255, verbose_name=_('conductor')) encoded_by = models.CharField(max_length=255, verbose_name=_('encoded by')) date = models.CharField(max_length=255, verbose_name=_('date')) project = models.ForeignKey(Project, models.PROTECT, verbose_name=_('project')) - url = models.URLField(max_length=255, verbose_name=_('book url')) + slug = models.SlugField(max_length=120, help_text=_('WL catalogue slug of the book.')) translator = models.CharField(max_length=255, null=True, blank=True, verbose_name=_('translator')) modified = models.DateTimeField(null=True, editable=False) license = models.ForeignKey(License, models.PROTECT, null=True, blank=True, verbose_name=_('license')) @@ -99,6 +99,7 @@ class Audiobook(models.Model): youtube_published_tags = models.TextField(null=True, editable=False) youtube_published = models.DateTimeField(null=True, editable=False) youtube_id = models.CharField(max_length=255, blank=True, default='') + youtube_queued = models.DateTimeField(null=True, blank=True) class Meta: verbose_name = _("audiobook") @@ -108,6 +109,34 @@ class Audiobook(models.Model): def __str__(self): return self.title + @property + def url(self): + return f'https://wolnelektury.pl/katalog/lektura/{self.slug}/' + + @property + def parts_count(self): + return type(self).objects.filter(slug=self.slug).count() + + @property + def youtube_volume_count(self): + total = 0 + prev_volume = None + for a in type(self).objects.filter(slug=self.slug).order_by("index"): + if not a.youtube_volume or a.youtube_volume != prev_volume: + total += 1 + prev_volume = a.youtube_volume + return total + + @property + def youtube_volume_index(self): + index = 0 + prev_volume = None + for a in type(self).objects.filter(slug=self.slug, index__lte=self.index).order_by("index"): + if not a.youtube_volume or a.youtube_volume != prev_volume: + index += 1 + prev_volume = a.youtube_volume + return index + def get_mp3_tags(self): return json.loads(self.mp3_tags) if self.mp3_tags else None def get_ogg_tags(self): return json.loads(self.ogg_tags) if self.ogg_tags else None def get_mp3_published_tags(self): return json.loads(self.mp3_published_tags) if self.mp3_published_tags else None @@ -169,7 +198,6 @@ class Audiobook(models.Model): @cached_property def book(self): - slug = self.url.rstrip('/').rsplit('/', 1)[-1] - apidata = requests.get(f'https://wolnelektury.pl/api/books/{slug}/').json() + apidata = requests.get(f'https://wolnelektury.pl/api/books/{self.slug}/').json() return apidata diff --git a/src/archive/tasks.py b/src/archive/tasks.py index 4b8f5fe..157824a 100644 --- a/src/archive/tasks.py +++ b/src/archive/tasks.py @@ -91,7 +91,10 @@ class AudioFormatTask(Task): audiobook = Audiobook.objects.get(id=aid) self.set_status(aid, status.ENCODING) - user = User.objects.get(id=uid) + if uid: + user = User.objects.get(id=uid) + else: + user = None out_file = NamedTemporaryFile(delete=False, prefix='%d-' % aid, suffix='.%s' % self.ext) out_file.close() diff --git a/src/archive/templates/archive/book.html b/src/archive/templates/archive/book.html new file mode 100644 index 0000000..fac3700 --- /dev/null +++ b/src/archive/templates/archive/book.html @@ -0,0 +1,29 @@ +{% extends "archive/base.html" %} + +{% block content %} +
+
+

Audiobooki

+
+
+ + + + + + + + {% for audiobook in object_list %} + + + + + {% endfor %} + +
?x
{{ audiobook.index }} + + {{ audiobook.title }} + +
+
+{% endblock %} diff --git a/src/archive/urls.py b/src/archive/urls.py index 32a6f73..da1873c 100644 --- a/src/archive/urls.py +++ b/src/archive/urls.py @@ -1,4 +1,5 @@ from django.conf.urls import url +from django.urls import path from django.views.generic import RedirectView from . import views @@ -12,6 +13,7 @@ urlpatterns = [ url(r'^unpublished/$', views.list_unpublished, name="list_unpublished"), url(r'^publishing/$', views.list_publishing, name="list_publishing"), url(r'^published/$', views.list_published, name="list_published"), + path('book//', views.BookView.as_view(), name="book"), url(r'^file/(\d+)/$', views.file_managed, name="file"), url(r'^publish/(\d+)/$', views.publish, name="publish"), url(r'^convert/(\d+)/$', views.publish, {'publish': False}, name="convert"), diff --git a/src/archive/views.py b/src/archive/views.py index aee88e6..10a998f 100644 --- a/src/archive/views.py +++ b/src/archive/views.py @@ -11,6 +11,7 @@ from django.http import Http404, HttpResponse from django.shortcuts import render, redirect, get_object_or_404 from django.utils.translation import gettext as _ from django.views.decorators.http import require_POST +from django.views.generic import ListView import mutagen @@ -270,15 +271,13 @@ def file_managed(request, id): request.user.oauthconnection_set.filter(access=True).exists()) alerts = [] - series = models.Audiobook.objects.filter(url=audiobook.url) - real = series.count() - if real != audiobook.parts_count: - alerts.append(_('Parts number inconsitent. Declared number: %(declared)d. Real number: %(real)d') % {"declared": audiobook.parts_count, "real": real}) - if audiobook.parts_count > 1: + parts_count = audiobook.parts_count + if parts_count > 1: + series = models.Audiobook.objects.filter(slug=audiobook.slug) if not audiobook.index: alerts.append(_('There is more than one part, but index is not set.')) - if set(series.values_list('index', flat=True)) != set(range(1, audiobook.parts_count + 1)): - alerts.append(_('Part indexes are not 1..%(parts_count)d.') % {"parts_count": audiobook.parts_count}) + if set(series.values_list('index', flat=True)) != set(range(1, parts_count + 1)): + alerts.append(_('Part indexes are not 1..%(parts_count)d.') % {"parts_count": parts_count}) return render(request, "archive/file_managed.html", locals()) @@ -299,3 +298,10 @@ def file_unmanaged(request, filename): err_exists = request.GET.get('exists') return render(request, "archive/file_unmanaged.html", locals()) + + +class BookView(ListView): + template_name = 'archive/book.html' + + def get_queryset(self): + return models.Audiobook.objects.filter(slug=self.kwargs['slug']) diff --git a/src/youtube/urls.py b/src/youtube/urls.py index 58071e5..fb9ddc7 100644 --- a/src/youtube/urls.py +++ b/src/youtube/urls.py @@ -4,7 +4,6 @@ from . import views urlpatterns = [ url(r'^publish/(\d+)/$', views.publish, name="youtube_publish"), - url(r'^convert/(\d+)/$', views.publish, {'publish': False}, name="youtube_convert"), path('thumbnail//', views.thumbnail, name='youtube_thumbnail'), path('preview//', views.Preview.as_view(), name="youtube_preview"), path('update//', views.Update.as_view(), name="youtube_update"), diff --git a/src/youtube/views.py b/src/youtube/views.py index 73eecd3..cbe1520 100644 --- a/src/youtube/views.py +++ b/src/youtube/views.py @@ -3,6 +3,7 @@ from django.http import HttpResponse from django.shortcuts import redirect, get_object_or_404 from django.urls import reverse from django.utils.decorators import method_decorator +from django.utils.timezone import now from django.views import View from django.views.decorators.http import require_POST from django.views.generic import DetailView @@ -17,12 +18,9 @@ from . import models, tasks @permission_required('archive.change_audiobook') def publish(request, aid, publish=True): audiobook = get_object_or_404(Audiobook, id=aid) - tags = {} - #audiobook.set_youtube_tags(tags) - audiobook.youtube_status = status.WAITING - audiobook.save(update_fields=['youtube_status']) - audiobook.youtube_task = tasks.YouTubeTask.delay(request.user.id, aid, publish).task_id - audiobook.save(update_fields=['youtube_task']) + audiobook.youtube_status = status.QUEUED + audiobook.youtube_queued = now() + audiobook.save(update_fields=['youtube_status', 'youtube_queued']) return redirect(reverse('file', args=[aid])) -- 2.20.1 From ff4fa4a4ae20cfb529d495f335cce477ada7f6a1 Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Wed, 3 Jun 2020 00:34:01 +0200 Subject: [PATCH 02/16] Publish volumes of audiobooks on YT. --- .../migrations/0017_auto_20200603_0011.py | 18 ++++++++ src/archive/models.py | 19 +++++++- src/archive/tasks.py | 13 ++++-- src/archive/views.py | 5 ++- src/youtube/fixtures/tests.yaml | 44 +++++++++++++++++++ src/youtube/management/commands/youtube.py | 21 +++++++++ src/youtube/models.py | 24 +++++----- src/youtube/tasks.py | 16 ++++++- src/youtube/tests.py | 31 ++++++++++++- src/youtube/views.py | 7 +-- 10 files changed, 175 insertions(+), 23 deletions(-) create mode 100644 src/archive/migrations/0017_auto_20200603_0011.py create mode 100644 src/youtube/fixtures/tests.yaml create mode 100644 src/youtube/management/commands/youtube.py diff --git a/src/archive/migrations/0017_auto_20200603_0011.py b/src/archive/migrations/0017_auto_20200603_0011.py new file mode 100644 index 0000000..3a8c618 --- /dev/null +++ b/src/archive/migrations/0017_auto_20200603_0011.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.6 on 2020-06-03 00:11 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('archive', '0016_auto_20200529_1528'), + ] + + operations = [ + migrations.AlterField( + model_name='audiobook', + name='youtube_volume', + field=models.CharField(blank=True, help_text='If set, audiobooks with the save value will be published as single YouTube video.', max_length=100, verbose_name='Volume name for YouTube'), + ), + ] diff --git a/src/archive/models.py b/src/archive/models.py index ad1ef78..721ceee 100644 --- a/src/archive/models.py +++ b/src/archive/models.py @@ -67,7 +67,14 @@ class Audiobook(models.Model): part_name = models.CharField(max_length=255, verbose_name=_('part name'), help_text=_('eg. chapter in a novel'), default='', blank=True) index = models.IntegerField(verbose_name=_('index'), default=0, help_text=_('Ordering of parts of a book.')) - youtube_volume = models.CharField(_('Volume name for YouTube'), max_length=1000, blank=True, help_text=_('If set, audiobooks with the save value will be published as single YouTube video.')) + youtube_volume = models.CharField( + _("Volume name for YouTube"), + max_length=100, + blank=True, + help_text=_( + "If set, audiobooks with the save value will be published as single YouTube video." + ), + ) artist = models.CharField(max_length=255, verbose_name=_('artist')) conductor = models.CharField(max_length=255, verbose_name=_('conductor')) encoded_by = models.CharField(max_length=255, verbose_name=_('encoded by')) @@ -137,6 +144,15 @@ class Audiobook(models.Model): prev_volume = a.youtube_volume return index + @property + def is_youtube_publishable(self): + return ( + not self.youtube_volume + or not type(self) + .objects.filter(youtube_volume=self.youtube_volume, index__lt=self.index) + .exists() + ) + def get_mp3_tags(self): return json.loads(self.mp3_tags) if self.mp3_tags else None def get_ogg_tags(self): return json.loads(self.ogg_tags) if self.ogg_tags else None def get_mp3_published_tags(self): return json.loads(self.mp3_published_tags) if self.mp3_published_tags else None @@ -200,4 +216,3 @@ class Audiobook(models.Model): def book(self): apidata = requests.get(f'https://wolnelektury.pl/api/books/{self.slug}/').json() return apidata - diff --git a/src/archive/tasks.py b/src/archive/tasks.py index 157824a..e50d9b2 100644 --- a/src/archive/tasks.py +++ b/src/archive/tasks.py @@ -98,7 +98,7 @@ class AudioFormatTask(Task): out_file = NamedTemporaryFile(delete=False, prefix='%d-' % aid, suffix='.%s' % self.ext) out_file.close() - self.encode(audiobook.source_file.path, out_file.name) + self.encode(self.get_source_file_paths(audiobook), out_file.name) self.set_status(aid, status.TAGGING) self.set_tags(audiobook, out_file.name) self.set_status(aid, status.SENDING) @@ -111,6 +111,9 @@ class AudioFormatTask(Task): self.save(audiobook, out_file.name) + def get_source_file_paths(self, audiobook): + return [audiobook.source_file.path] + def on_failure(self, exc, task_id, args, kwargs, einfo): aid = (args[0], kwargs.get('aid'))[0] self.set_status(aid, None) @@ -149,7 +152,9 @@ class Mp3Task(AudioFormatTask): } @staticmethod - def encode(in_path, out_path): + def encode(in_paths, out_path): + assert len(in_paths) == 1 + in_path = in_paths[0] # 44.1kHz 64kbps mono MP3 subprocess.check_call(['ffmpeg', '-i', in_path.encode('utf-8'), @@ -184,7 +189,9 @@ class OggTask(AudioFormatTask): prefix = ext = 'ogg' @staticmethod - def encode(in_path, out_path): + def encode(in_paths, out_path): + assert len(in_paths) == 1 + in_path = in_paths[0] # 44.1kHz 64kbps mono Ogg Vorbis subprocess.check_call(['ffmpeg', '-i', in_path.encode('utf-8'), diff --git a/src/archive/views.py b/src/archive/views.py index 10a998f..42acffc 100644 --- a/src/archive/views.py +++ b/src/archive/views.py @@ -186,6 +186,7 @@ def cancel_publishing(request, aid): audiobook.mp3_status = None audiobook.ogg_status = None audiobook.youtube_status = None + audiobook.youtube_queued = None audiobook.save() return redirect(file_managed, aid) @@ -221,7 +222,9 @@ def list_unpublished(request): def list_publishing(request): division = 'publishing' - objects = models.Audiobook.objects.exclude(mp3_status=None, ogg_status=None, youtube_status=None) + objects = models.Audiobook.objects.exclude( + mp3_status=None, ogg_status=None, youtube_status=None + ).order_by("youtube_queued", "title") objects_by_status = {} for o in objects: statuses = set() diff --git a/src/youtube/fixtures/tests.yaml b/src/youtube/fixtures/tests.yaml new file mode 100644 index 0000000..7e1476f --- /dev/null +++ b/src/youtube/fixtures/tests.yaml @@ -0,0 +1,44 @@ +- model: archive.Project + pk: 1 + fields: + name: A project +- model: youtube.YouTube + pk: 1 + fields: + loop_video: loop.mkv + thumbnail_template: "template.jpg" + +- model: archive.Audiobook + pk: 1 + fields: + title: A title, part 1 + slug: a-slug + source_file: part1.flac + project_id: 1 + index: 1 +- model: archive.Audiobook + pk: 2 + fields: + title: A title, part 2 + slug: a-slug + source_file: part2.flac + project_id: 1 + index: 2 + youtube_volume: parts 2, 3 +- model: archive.Audiobook + pk: 3 + fields: + title: A title, part 3 + slug: a-slug + source_file: part3.flac + project_id: 1 + index: 3 + youtube_volume: parts 2, 3 +- model: archive.Audiobook + pk: 4 + fields: + title: A title, part 4 + slug: a-slug + source_file: part4.flac + project_id: 1 + index: 4 diff --git a/src/youtube/management/commands/youtube.py b/src/youtube/management/commands/youtube.py new file mode 100644 index 0000000..4a8dfb5 --- /dev/null +++ b/src/youtube/management/commands/youtube.py @@ -0,0 +1,21 @@ +from django.core.management.base import BaseCommand +from archive.constants import status +from archive.models import Audiobook +from youtube import tasks + + +class Command(BaseCommand): + help = "Schedules some audiobooks for uploading to YouTube." + + def add_arguments(self, parser): + parser.add_argument("--limit", type=int, default=6) + + def handle(self, *args, **options): + for audiobook in Audiobook.objects.exclude(youtube_queued=None).order_by( + "youtube_queued" + )[: options["limit"]]: + audiobook.youtube_task = tasks.YouTubeTask.delay( + None, audiobook.id, True + ).task_id + audiobook.youtube_status = status.WAITING + audiobook.save(update_fields=["youtube_task", "youtube_status"]) diff --git a/src/youtube/models.py b/src/youtube/models.py index 43db3b0..777b77a 100644 --- a/src/youtube/models.py +++ b/src/youtube/models.py @@ -97,30 +97,33 @@ class YouTube(models.Model): "https://www.googleapis.com/youtube/v3/videos", params={"part": part}, json=data - ) + ) - def prepare_file(self, input_path, output_path=None): - audio = self.prepare_audio(input_path) - duration = self.get_duration(input_path) + def prepare_file(self, input_paths, output_path=None): + audio = self.prepare_audio(input_paths) + duration = self.get_duration(input_paths) video = self.prepare_video(duration) output = mux([video, audio], output_path=output_path) unlink(audio) unlink(video) return output - def get_duration(self, input_path): - d = get_duration(input_path) + def get_duration(self, input_paths): + d = 0 + for input_path in input_paths: + d += get_duration(input_path) if self.intro_flac: d += get_duration(self.intro_flac.path) if self.outro_flac: d += get_duration(self.outro_flac.path) return d - - def prepare_audio(self, input_path): + + def prepare_audio(self, input_paths): files = [] if self.intro_flac: files.append(standardize_audio(self.intro_flac.path)) - files.append(standardize_audio(input_path, cache=False)) + for input_path in input_paths: + files.append(standardize_audio(input_path, cache=False)) if self.outro_flac: files.append(standardize_audio(self.outro_flac.path)) output = concat_audio(files) @@ -128,7 +131,6 @@ class YouTube(models.Model): unlink(d) return output - def prepare_video(self, duration): concat = [] outro = [] @@ -196,7 +198,7 @@ class YouTube(models.Model): buf = io.BytesIO() img.save(buf, format='PNG') return buf - + class Card(models.Model): youtube = models.ForeignKey(YouTube, models.CASCADE) diff --git a/src/youtube/tasks.py b/src/youtube/tasks.py index 32e41b7..63051c9 100644 --- a/src/youtube/tasks.py +++ b/src/youtube/tasks.py @@ -7,8 +7,8 @@ class YouTubeTask(AudioFormatTask): ext = 'mkv' prefix = 'youtube' - def encode(self, in_path, out_path): - YouTube.objects.first().prepare_file(in_path, out_path) + def encode(self, in_paths, out_path): + YouTube.objects.first().prepare_file(in_paths, out_path) def set_tags(self, audiobook, filename): pass @@ -20,3 +20,15 @@ class YouTubeTask(AudioFormatTask): def put(self, user, audiobook, filename): YouTube.objects.first().publish(audiobook, filename) + + def get_source_file_paths(self, audiobook): + if not audiobook.youtube_volume: + return [audiobook.source_file.path] + return [ + a.source_file.path + for a in type(audiobook) + .objects.filter( + slug=audiobook.slug, youtube_volume=audiobook.youtube_volume + ) + .order_by("index") + ] diff --git a/src/youtube/tests.py b/src/youtube/tests.py index 7ce503c..a7587e3 100644 --- a/src/youtube/tests.py +++ b/src/youtube/tests.py @@ -1,3 +1,32 @@ +from unittest import mock from django.test import TestCase +from archive.models import Audiobook +from . import tasks -# Create your tests here. + +@mock.patch("youtube.models.youtube_call") +@mock.patch("youtube.utils.subprocess") +@mock.patch("youtube.models.concat_audio") +@mock.patch("youtube.models.unlink") +@mock.patch("youtube.models.YouTube.prepare_thumbnail") +class YouTubeTests(TestCase): + fixtures = ["tests.yaml"] + + def test_youtube_volumes( + self, prepare_thumbnail, unlink, concat_audio, subprocess, youtube_call + ): + youtube_call.return_value = mock.Mock( + json=mock.Mock(return_value={"id": "deadbeef"}) + ) + audiobooks = Audiobook.objects.all().order_by("index") + + self.assertEqual(audiobooks[0].youtube_volume_count, 3) + self.assertEqual([a.youtube_volume_index for a in audiobooks], [1, 2, 2, 3]) + self.assertEqual( + [a.is_youtube_publishable for a in audiobooks], [True, True, False, True] + ) + + tasks.YouTubeTask().run(None, 2, True) + + # In creating a volume of two audiobooks, we should've called concat with a list of two files. + self.assertEqual(len(concat_audio.call_args[0][0]), 2) diff --git a/src/youtube/views.py b/src/youtube/views.py index cbe1520..4c002ee 100644 --- a/src/youtube/views.py +++ b/src/youtube/views.py @@ -18,9 +18,10 @@ from . import models, tasks @permission_required('archive.change_audiobook') def publish(request, aid, publish=True): audiobook = get_object_or_404(Audiobook, id=aid) - audiobook.youtube_status = status.QUEUED - audiobook.youtube_queued = now() - audiobook.save(update_fields=['youtube_status', 'youtube_queued']) + if audiobook.is_youtube_publishable: + audiobook.youtube_status = status.QUEUED + audiobook.youtube_queued = now() + audiobook.save(update_fields=['youtube_status', 'youtube_queued']) return redirect(reverse('file', args=[aid])) -- 2.20.1 From c00e6aa94827f4972dc33fa79a53b5d9a15a726e Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Thu, 4 Jun 2020 12:20:38 +0200 Subject: [PATCH 03/16] Nicer audiobooks admin and fix for YT queueing. --- src/archive/admin.py | 5 ++++- src/youtube/management/commands/youtube.py | 8 +++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/archive/admin.py b/src/archive/admin.py index 577a0ae..f6fcd4a 100644 --- a/src/archive/admin.py +++ b/src/archive/admin.py @@ -5,7 +5,10 @@ admin.site.register(Project) class AudiobookAdmin(admin.ModelAdmin): - list_filter = ['license'] + list_display = ["title", "slug", "index", "part_name", "license", "youtube_volume"] + list_filter = ["license"] + list_search = ["title", "slug", "part_name", "youtube_volume"] + list_editable = ["youtube_volume"] admin.site.register(Audiobook, AudiobookAdmin) diff --git a/src/youtube/management/commands/youtube.py b/src/youtube/management/commands/youtube.py index 4a8dfb5..4277818 100644 --- a/src/youtube/management/commands/youtube.py +++ b/src/youtube/management/commands/youtube.py @@ -11,9 +11,11 @@ class Command(BaseCommand): parser.add_argument("--limit", type=int, default=6) def handle(self, *args, **options): - for audiobook in Audiobook.objects.exclude(youtube_queued=None).order_by( - "youtube_queued" - )[: options["limit"]]: + for audiobook in ( + Audiobook.objects.filter(status=status.QUEUED) + .exclude(youtube_queued=None) + .order_by("youtube_queued")[: options["limit"]] + ): audiobook.youtube_task = tasks.YouTubeTask.delay( None, audiobook.id, True ).task_id -- 2.20.1 From 33c35cad796de192573ef8cb377e9c9dd0b11a68 Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Thu, 4 Jun 2020 12:22:38 +0200 Subject: [PATCH 04/16] bugfix --- src/youtube/management/commands/youtube.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/youtube/management/commands/youtube.py b/src/youtube/management/commands/youtube.py index 4277818..fd50e0c 100644 --- a/src/youtube/management/commands/youtube.py +++ b/src/youtube/management/commands/youtube.py @@ -12,7 +12,7 @@ class Command(BaseCommand): def handle(self, *args, **options): for audiobook in ( - Audiobook.objects.filter(status=status.QUEUED) + Audiobook.objects.filter(youtube_status=status.QUEUED) .exclude(youtube_queued=None) .order_by("youtube_queued")[: options["limit"]] ): -- 2.20.1 From a183348d000329cb699570f151c98c88d751fff9 Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Fri, 5 Jun 2020 13:56:23 +0200 Subject: [PATCH 05/16] Use youtube volume name on thumbnails. --- src/youtube/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/youtube/models.py b/src/youtube/models.py index 777b77a..b1abda9 100644 --- a/src/youtube/models.py +++ b/src/youtube/models.py @@ -191,7 +191,7 @@ class YouTube(models.Model): { "author": ', '.join((a['name'] for a in audiobook.book['authors'])), "title": audiobook.book['title'], - "part": audiobook.part_name, + "part": audiobook.youtube_volume or audiobook.part_name, }, lambda name: Font.objects.get(name=name).truetype.path ) -- 2.20.1 From 73594a90ce59bbbe51faa4c0263f3e8ccedde27b Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Fri, 5 Jun 2020 13:57:04 +0200 Subject: [PATCH 06/16] fix --- src/youtube/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/youtube/models.py b/src/youtube/models.py index b1abda9..5fe3c2b 100644 --- a/src/youtube/models.py +++ b/src/youtube/models.py @@ -191,7 +191,7 @@ class YouTube(models.Model): { "author": ', '.join((a['name'] for a in audiobook.book['authors'])), "title": audiobook.book['title'], - "part": audiobook.youtube_volume or audiobook.part_name, + "part": (audiobook.youtube_volume or audiobook.part_name).strip(), }, lambda name: Font.objects.get(name=name).truetype.path ) -- 2.20.1 From 7a728f387a0969d0ca4cfc7b7877bf4fa255eef9 Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Fri, 5 Jun 2020 14:02:01 +0200 Subject: [PATCH 07/16] Disable youtube publish button when in volume. --- src/archive/templates/archive/file_managed.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/archive/templates/archive/file_managed.html b/src/archive/templates/archive/file_managed.html index 61eaca4..f08f951 100644 --- a/src/archive/templates/archive/file_managed.html +++ b/src/archive/templates/archive/file_managed.html @@ -73,6 +73,7 @@ {% endif %}
+ {% if audiobook.is_youtube_publishable %}

{% trans "Preview YouTube metadata" %} @@ -83,6 +84,7 @@
+ {% endif %} {% if audiobook.youtube_id %}

{% csrf_token %} -- 2.20.1 From 3bbec5643861e1efd4364bdbf593d705facc6682 Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Fri, 5 Jun 2020 15:19:32 +0200 Subject: [PATCH 08/16] fix --- src/archive/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/archive/admin.py b/src/archive/admin.py index f6fcd4a..279c6dc 100644 --- a/src/archive/admin.py +++ b/src/archive/admin.py @@ -7,7 +7,7 @@ admin.site.register(Project) class AudiobookAdmin(admin.ModelAdmin): list_display = ["title", "slug", "index", "part_name", "license", "youtube_volume"] list_filter = ["license"] - list_search = ["title", "slug", "part_name", "youtube_volume"] + search_fields = ["title", "slug", "part_name", "youtube_volume"] list_editable = ["youtube_volume"] -- 2.20.1 From 133902ac65400d9a129d0cba3db3392b925d54e6 Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Sat, 6 Jun 2020 00:24:37 +0200 Subject: [PATCH 09/16] Start to reorganize views a little. --- requirements.txt | 1 + src/archive/constants.py | 7 +- .../templates/archive/audiobook_list.html | 73 +++++++++++++++++++ src/archive/templates/archive/base.html | 9 +-- src/archive/templates/archive/book.html | 66 ++++++++++++++--- .../templates/archive/file_managed.html | 5 +- src/archive/templates/archive/file_new.html | 4 + .../templates/archive/file_unmanaged.html | 4 + src/archive/templates/archive/list.html | 6 +- src/archive/templates/archive/list_new.html | 4 + .../templates/archive/list_published.html | 22 ------ .../templates/archive/list_publishing.html | 3 + .../templates/archive/list_unmanaged.html | 4 + .../templates/archive/list_unpublished.html | 40 ---------- src/archive/templates/archive/status.html | 21 ++++++ .../templates/pagination/pagination.html | 53 ++++++++++++++ src/archive/templatetags/tags.py | 15 ++++ src/archive/urls.py | 7 +- src/archive/views.py | 29 ++------ src/audiobooks/settings.py | 3 +- 20 files changed, 259 insertions(+), 117 deletions(-) create mode 100644 src/archive/templates/archive/audiobook_list.html delete mode 100644 src/archive/templates/archive/list_published.html delete mode 100644 src/archive/templates/archive/list_unpublished.html create mode 100644 src/archive/templates/archive/status.html create mode 100644 src/archive/templates/pagination/pagination.html diff --git a/requirements.txt b/requirements.txt index fe95850..c355845 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,6 +4,7 @@ Django==3.0.6 django-cas-ng==4.1.1 django-bootstrap4==1.1.1 django-pglocks==1.0.4 +fnp-django-pagination==2.2.4 celery[redis]==4.4.2 diff --git a/src/archive/constants.py b/src/archive/constants.py index 3493e00..a5527ec 100644 --- a/src/archive/constants.py +++ b/src/archive/constants.py @@ -1,8 +1,9 @@ from django.utils.translation import gettext_lazy as _ + class status: - QUEUED = 1 - WAITING = 10 + WAITING = 1 + QUEUED = 10 ENCODING = 20 TAGGING = 30 CONVERTING_AUDIO = 40 @@ -14,8 +15,8 @@ class status: SETTING_THUMBNAIL = 110 choices = [ - (QUEUED, _('Queued')), (WAITING, _('Waiting')), + (QUEUED, _("Queued")), (ENCODING, _('Encoding')), (TAGGING, _('Tagging')), (CONVERTING_AUDIO, _('Converting audio')), diff --git a/src/archive/templates/archive/audiobook_list.html b/src/archive/templates/archive/audiobook_list.html new file mode 100644 index 0000000..1521b42 --- /dev/null +++ b/src/archive/templates/archive/audiobook_list.html @@ -0,0 +1,73 @@ +{% extends "archive/list.html" %} +{% load i18n %} +{% load pagination_tags %} + + +{% block menu-active-audiobooks %}active{% endblock %} + + +{% block file-list-title %} + {% trans "Audiobooks" %} +{% endblock %} + + +{% block file-list-info %} +{% endblock %} + +{% block file-list-wrapper %} + {% autopaginate object_list 50 %} + {{ block.super }} + {% paginate %} +{% endblock %} + + +{% block file-list %} + + + Slug + {% trans "Title" %} + {% trans "YouTube volume" %} + MP3 + Ogg + YouTube + + + {% for audiobook in object_list %} + + + {% if audiobook.slug %} + {{ audiobook.slug }} + {% endif %} + + {{ audiobook }} + + + {{ audiobook.youtube_volume }} + + + {% if audiobook.mp3_status %} + + MP3: + {{ audiobook.get_mp3_status_display }} + + {% endif %} + + + {% if audiobooks.ogg_status_display %} + + Ogg: + {{ audiobook.get_ogg_status_display }} + + {% endif %} + + + {% if audiobooks.youtube_status %} + + YT: + {{ audiobook.get_youtube_status_display }} + + {% endif %} + + + {% endfor %} +{% endblock %} diff --git a/src/archive/templates/archive/base.html b/src/archive/templates/archive/base.html index 9eeaf8c..f7e5ad7 100644 --- a/src/archive/templates/archive/base.html +++ b/src/archive/templates/archive/base.html @@ -3,11 +3,10 @@ {% block repo-zones-nav %}
diff --git a/src/archive/templates/archive/list_new.html b/src/archive/templates/archive/list_new.html index 5e804ca..b7ca11d 100644 --- a/src/archive/templates/archive/list_new.html +++ b/src/archive/templates/archive/list_new.html @@ -2,6 +2,10 @@ {% load i18n %} +{% block menu-active-new %}active{% endblock %} + + + {% block file-list-title %} {% trans "New audiobooks" %} {% endblock %} diff --git a/src/archive/templates/archive/list_published.html b/src/archive/templates/archive/list_published.html deleted file mode 100644 index 04396f6..0000000 --- a/src/archive/templates/archive/list_published.html +++ /dev/null @@ -1,22 +0,0 @@ -{% extends "archive/list.html" %} -{% load i18n %} - - -{% block file-list-title %} - {% trans "Published audiobooks" %} -{% endblock %} - - -{% block file-list-info %} -{% endblock %} - - -{% block file-list %} - {% for file in objects %} - - - {{ file }} - - - {% endfor %} -{% endblock %} diff --git a/src/archive/templates/archive/list_publishing.html b/src/archive/templates/archive/list_publishing.html index 83d631d..ad13504 100644 --- a/src/archive/templates/archive/list_publishing.html +++ b/src/archive/templates/archive/list_publishing.html @@ -2,6 +2,9 @@ {% load i18n %} +{% block menu-active-publishing %}active{% endblock %} + + {% block file-list-title %} {% trans "Audiobooks being published" %} {% endblock %} diff --git a/src/archive/templates/archive/list_unmanaged.html b/src/archive/templates/archive/list_unmanaged.html index 7ca92a1..599de32 100644 --- a/src/archive/templates/archive/list_unmanaged.html +++ b/src/archive/templates/archive/list_unmanaged.html @@ -1,6 +1,10 @@ {% extends "archive/list.html" %} {% load i18n %} + +{% block menu-active-unmanaged %}active{% endblock %} + + {% block file-list-title %} {% trans "Unmanaged archive" %} {% endblock %} diff --git a/src/archive/templates/archive/list_unpublished.html b/src/archive/templates/archive/list_unpublished.html deleted file mode 100644 index d51b66b..0000000 --- a/src/archive/templates/archive/list_unpublished.html +++ /dev/null @@ -1,40 +0,0 @@ -{% extends "archive/list.html" %} -{% load i18n %} - - -{% block file-list-title %} - {% trans "Unpublished audiobooks" %} -{% endblock %} - - -{% block file-list-info %} -{% endblock %} - - -{% block file-list %} - {% for file in objects %} - - - {{ file }} - - - {% if file.mp3_published %} - MP3 - {% else %} - {% if file.mp3_status %} - MP3 - {% endif %} - {% endif %} - - - {% if file.ogg_published %} - Ogg - {% else %} - {% if file.ogg_status %} - Ogg - {% endif %} - {% endif %} - - - {% endfor %} -{% endblock %} diff --git a/src/archive/templates/archive/status.html b/src/archive/templates/archive/status.html new file mode 100644 index 0000000..2e8aa3b --- /dev/null +++ b/src/archive/templates/archive/status.html @@ -0,0 +1,21 @@ +{% load i18n %} + +{% if published %} + {% if link %} + + {% endif %} + + {% trans "OK" %} + + {% if link %} + + {% endif %} +{% elif status %} + + {{ status }} + +{% else %} + + –– + +{% endif %} diff --git a/src/archive/templates/pagination/pagination.html b/src/archive/templates/pagination/pagination.html new file mode 100644 index 0000000..6aa12ad --- /dev/null +++ b/src/archive/templates/pagination/pagination.html @@ -0,0 +1,53 @@ +{% if is_paginated %} +{% load i18n %} + + +{% endif %} diff --git a/src/archive/templatetags/tags.py b/src/archive/templatetags/tags.py index 8e208a6..c3dfa23 100755 --- a/src/archive/templatetags/tags.py +++ b/src/archive/templatetags/tags.py @@ -2,6 +2,7 @@ from django import template register = template.Library() + @register.inclusion_tag('archive/tags/multiple_tags_table.html') def multiple_tags_table(tags, table=True): new_tags = {} @@ -19,3 +20,17 @@ def tags_table(tags, table=True): if tags is None: tags = {} return locals() + + +@register.inclusion_tag("archive/status.html") +def status(audiobook, format): + if format == "youtube" and audiobook.youtube_id: + link = f"https://youtu.be/{audiobook.youtube_id}" + else: + link = None + return { + "published": getattr(audiobook, f"{format}_published"), + "status": getattr(audiobook, f"get_{format}_status_display")(), + "format": format, + "link": link, + } diff --git a/src/archive/urls.py b/src/archive/urls.py index da1873c..4743227 100644 --- a/src/archive/urls.py +++ b/src/archive/urls.py @@ -4,15 +4,11 @@ from django.views.generic import RedirectView from . import views urlpatterns = [ - url(r'^$', RedirectView.as_view(url='new/')), - + path("", views.AudiobookList.as_view(), name="list_managed"), url(r'^new/$', views.list_new, name="list_new"), url(r'^new/(.+)/$', views.file_new, name="file_new"), url(r'^move_to_archive/(.+)/$', views.move_to_archive, name="move_to_archive"), - - url(r'^unpublished/$', views.list_unpublished, name="list_unpublished"), url(r'^publishing/$', views.list_publishing, name="list_publishing"), - url(r'^published/$', views.list_published, name="list_published"), path('book//', views.BookView.as_view(), name="book"), url(r'^file/(\d+)/$', views.file_managed, name="file"), url(r'^publish/(\d+)/$', views.publish, name="publish"), @@ -21,7 +17,6 @@ urlpatterns = [ url(r'^download/(\d+)\.(mp3|ogg|mkv)$', views.download, name="download"), url(r'^cancel/(\d+)/$', views.cancel_publishing, name="cancel_publishing"), url(r'^remove_to_archive/(\d+)/$', views.remove_to_archive, name="remove_to_archive"), - url(r'^unmanaged/$', views.list_unmanaged, name="list_unmanaged"), url(r'^unmanaged/(.+)/$', views.file_unmanaged, name="file_unmanaged"), url(r'^move_to_new/(.+)/$', views.move_to_new, name="move_to_new"), diff --git a/src/archive/views.py b/src/archive/views.py index 42acffc..f1b1c1b 100644 --- a/src/archive/views.py +++ b/src/archive/views.py @@ -23,8 +23,6 @@ from archive.utils import all_files def list_new(request): - division = 'new' - path = settings.NEW_PATH objects = sorted(all_files(path)) return render(request, "archive/list_new.html", locals()) @@ -32,8 +30,6 @@ def list_new(request): @permission_required('archive.change_audiobook') def file_new(request, filename): - division = 'new' - filepath = filename root_filepath = os.path.join(settings.NEW_PATH, filename) if request.POST: @@ -212,16 +208,7 @@ def download(request, aid, which="source"): return response -def list_unpublished(request): - division = 'unpublished' - - objects = models.Audiobook.objects.filter(Q(mp3_published=None) | Q(ogg_published=None)) - return render(request, "archive/list_unpublished.html", locals()) - - def list_publishing(request): - division = 'publishing' - objects = models.Audiobook.objects.exclude( mp3_status=None, ogg_status=None, youtube_status=None ).order_by("youtube_queued", "title") @@ -241,11 +228,8 @@ def list_publishing(request): return render(request, "archive/list_publishing.html", locals()) -def list_published(request): - division = 'published' - - objects = models.Audiobook.objects.exclude(Q(mp3_published=None) | Q(ogg_published=None)) - return render(request, "archive/list_published.html", locals()) +class AudiobookList(ListView): + queryset = models.Audiobook.objects.all() @permission_required('archive.change_audiobook') @@ -260,7 +244,6 @@ def file_managed(request, id): except IOError: raise Http404 - division = 'published' if audiobook.published() else 'unpublished' path = audiobook.source_file.path[len(settings.FILES_PATH):].lstrip('/') # for tags update @@ -286,15 +269,11 @@ def file_managed(request, id): def list_unmanaged(request): - division = 'unmanaged' - objects = sorted(all_files(settings.UNMANAGED_PATH)) return render(request, "archive/list_unmanaged.html", locals()) def file_unmanaged(request, filename): - division = 'unmanaged' - tags = mutagen.File(os.path.join(settings.UNMANAGED_PATH, filename.encode('utf-8'))) if not tags: tags = {} @@ -307,4 +286,6 @@ class BookView(ListView): template_name = 'archive/book.html' def get_queryset(self): - return models.Audiobook.objects.filter(slug=self.kwargs['slug']) + return models.Audiobook.objects.filter(slug=self.kwargs["slug"]).order_by( + "index" + ) diff --git a/src/audiobooks/settings.py b/src/audiobooks/settings.py index ca3d16e..901da0f 100644 --- a/src/audiobooks/settings.py +++ b/src/audiobooks/settings.py @@ -107,6 +107,7 @@ MIDDLEWARE = ( 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django_cas_ng.middleware.CASMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', + "fnp_django_pagination.middleware.PaginationMiddleware", ) AUTHENTICATION_BACKENDS = ( @@ -128,10 +129,10 @@ INSTALLED_APPS = ( # 'django.contrib.admindocs', 'bootstrap4', 'django_cas_ng', - 'apiclient', 'archive', 'youtube', + "fnp_django_pagination", ) # A sample logging configuration. The only tangible logging -- 2.20.1 From 3d43625e3a4a34761b4aa628e7af9732a21447da Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Sat, 6 Jun 2020 00:28:40 +0200 Subject: [PATCH 10/16] l10n --- src/archive/locale/pl/LC_MESSAGES/django.mo | Bin 4174 -> 5020 bytes src/archive/locale/pl/LC_MESSAGES/django.po | 251 +++++++++++++------- 2 files changed, 163 insertions(+), 88 deletions(-) diff --git a/src/archive/locale/pl/LC_MESSAGES/django.mo b/src/archive/locale/pl/LC_MESSAGES/django.mo index 20211dd2deb863e3f191cefc8ffa17e5c731a072..a1c333d3931976705579d658df54cb1ac55ae249 100644 GIT binary patch literal 5020 zcma);ZLC~H8OH}jgbNBH0`lgd&==ZW_FfR{wwLzOa)T`oaJIoH@Ho zLkN?d^PBg1=6Rl(Xa98h`A-X^Mda&{pL(AVZSay0;Dhw@3xxPEcpCg5_!77cd>On9 zya*^Q9k}+ z@D|js1*w;Lkmss{n?M6z4SpNMRPhrKB8t->*ZUQC3HW=E^Zw%bn(u!Lny7)8A|hY!f#;+C2zVvWm)JRb!){%Mfozw7Hi1s73&5u~47 z2Ica6SAq1e5=i+MK-%Me@Op3!q~4wasjp|i8Sr}`$G-w{y}yB!=QWV~zX|eu=fT+2 z+Z4!kF9mt7%^=tP49Ib_Am`76FqK#Wxz2+i_ice37lTldcnIXaPx$sHLAYHe8<3ri zgd2(-$R%WnycL=6%6??dpG+H2p^SGRbI(sA&m!}^dsF6e|NOkAv7XpNYQ>&^L~in%1womv!{Mm}J&VJ!@_6?3C`vF_R=bT;mf8R0`0Lq6Im6=153Bb z)iBa!F=|?DG~^K**Kppv=WbKC0e7Cqz-&L6sE(F~9~H$~1%0WHL8HlyV(+RPGjngD2vk?lDiCMI5vv zO&9Ad?ic5%?A-dhNwF9QI_AObs_d%RB`hdOCo793g~R1ux`aL%QWYA02oE)oWV&3w zc~ds)!MaV2+Z|)^Y#P_$4uz*D5Vk7rO?3({mLho#V=`nRaSMg^^fR z4d|dDmYW)fhEH}Z=$gu4KKuu2*(z%gAkz&8P%&`9vRDpXq{WKqD5DxYlE3IHUFc)% zW+MgpcBdMNIAWu;aPd_ej}&xtLDr$n$Tr}_Nt9-Gqi^R1JE}s;FTC;#X8!bUUQifo zE!$S$P`FKEq|9+C5p`>Vw2l`<@DxHf%Cg>6U8iHH0G?yu){)S>vp93DFEYi5D6DIf zXkqj*C#bdT`id^S#ij`gnRgUSgf}Rk0O2xC;=wdYq`0;BrPAJ@bVSDqp3$nDI)t|^ zcBKPJBMeG+rH!PtY|*^5SUQAFIH1Wu{W9+<~90RH`%EwxC{_T2P5AEsyr7 z%0nv9ZMl1(T5TnF#451Zy;rVh4ZDo(+4?{yMs;nJv{l*0jdC}-6YH2p3WJxHwd!F1 zqF$BVXzIY;19y)_o-S9W@HRSauyel)(HzusYd2D%*-o{_aHQFl<@wUK@iVTIi;hqBK@fY2L=2q$*7}t0uFzZO69kncZY&XQ$;(S&`_zdAf4v?2O#7AuH~f zy`9BPY_9B>uE>WU&e}PqvKe!xF=uA;_NmX8?#Ul98&~C$)j{awOnIh!`_x2KtG84h z)Ou-0>>l)CX6dn(u0dM1E%lOeQkEei6{|D`)KOUC4 zo{iIN6P5;0{6Mv1ouuy^Fm2F?)=va74+Mrs4x*7U^0%(-`eHG+9`rpbO&G0c%)EQD zVtx3e(<EP|T#7%Pi`_8kXSZ6cxZc@QvFs=~)%wXg9X^Do!y^qemMeYzakw}B zk}-y~ld+2FUT`Znx(0A4*LUgg`1rqUmgH}YVaoU5cYe!}V3BS!-n~O4 z4n?eoXZi{5DFU$)rdyV=9R!1rcdjS97LNzr79n-eN0fHD-ZaJ4&cZSM!653bpX?fz uc;>>{Mc49~4q&)ECbto`8Kdcm*^4%OAwGL57#_zZEp+dO$I}ixLi`_qUnn8~ delta 2016 zcmZvcTS%W}9LFEG`=^U<$oeIIqIgr=?|INMr|Gt0sKg=@9 zAl4Ri(;mjK)gX%KB3NM&19vf|7X=j*MHFligw#c~tKD?^eDyVr`tkmr&vX7Ap67XA ztNYBEzFxNBoI!ffYP4dVF@rF&fdZMh&6thw1k8ia!*+NER=}(9PWUw}g*Ra?Tu-6? zLRbcC;TG5q^I<={&zNa5M1?hG8uFP_6q@idWP&*dH^IwL20n)C;dfAu-GDOi3)H+E zyljM}upV~7BhZ7|_XEghu27U=-z-ompyL~;h<<>D@K(rmtdfCZs0bV=M>?S#I0Tt$ zhC)VREA2Scyo<01z6WoIb7A{Sn1_9HLlw9P>)@}DV9aK`l4CU#a;P3MWZJ_1E~ry_ z7;58#Q1eEhGCB_VOqxOtJPT##C8&K~fzujzm5LU;4Yk3$@NAATAHibU2biPF*s!*qzFP==DQ5k3Vg;hV4u zUWSVJE2y*n7RtcSVf%NulXe8Jb)mcAFnkVb-|J8gEJ7Xi&0_M;8k5UGZieNMG1CMU zNe9%#J}3tUp$uk2o`#w?6Y?$i2<=Nyj{gNqU?tva{Wd7a_fg1^{%IGPrqH|52`!A_^P zJ{ER0#9Eww&rPfuO3`eH*-Hg2mFCgxNWxDaXQ`Ww`N@%FGM={I7xdYt!Up?f;hwz% z!MGc^qneP49`{drW+0eYS+uskW&<-Y5py%1lW|AWw!Emr4i#1BYilQ)O4yS{?UfUR z;soxvXH-i&(PTEr*iVbng}a+u+nt?loo%hN2O?*4w)A@w$*HF^$&{ZRUo~-UcGA{w z9JFt2e9S(x>2+IGvd^9=**kp1_tF#BX2*i$!iD+M&ZLv@qf;Zk=R~LGXBI9*{TAn_ z8}s61D>+GA%j53MCrWqN4@-xR^{!;lU(VH}n+QCE zOHOhrV{w`MZ^xSL%zzuuWLf5qDS5tm+=(X>-dNPLGi6=52`^3}tXou3?3;o9Lry&H d&!1YDvwxQL)CBI-r29{tW-0L1w)M-~{s!wbJ*5Bu diff --git a/src/archive/locale/pl/LC_MESSAGES/django.po b/src/archive/locale/pl/LC_MESSAGES/django.po index 6422547..2aa34bf 100644 --- a/src/archive/locale/pl/LC_MESSAGES/django.po +++ b/src/archive/locale/pl/LC_MESSAGES/django.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-05-28 13:31+0200\n" -"PO-Revision-Date: 2020-05-28 13:32+0200\n" +"POT-Creation-Date: 2020-06-06 00:25+0200\n" +"PO-Revision-Date: 2020-06-06 00:28+0200\n" "Last-Translator: Radek Czajka \n" "Language-Team: \n" "Language: pl\n" @@ -19,23 +19,51 @@ msgstr "" "|| n%100>=20) ? 1 : 2);\n" "X-Generator: Poedit 2.2.4\n" -#: constants.py:10 +#: constants.py:18 msgid "Waiting" +msgstr "Czeka" + +#: constants.py:19 +msgid "Queued" msgstr "W kolejce" -#: constants.py:11 +#: constants.py:20 msgid "Encoding" msgstr "Konwersja" -#: constants.py:12 +#: constants.py:21 msgid "Tagging" msgstr "Opisywanie" -#: constants.py:13 +#: constants.py:22 +msgid "Converting audio" +msgstr "Konwersja audio" + +#: constants.py:23 +msgid "Converting video" +msgstr "Konwersja wideo" + +#: constants.py:24 +msgid "Assembling audio" +msgstr "Kompletowanie audio" + +#: constants.py:25 +msgid "Assembling video" +msgstr "Kompletowanie wideo" + +#: constants.py:26 +msgid "Joining audio and video" +msgstr "Łączenie audio i wideo" + +#: constants.py:27 msgid "Sending" msgstr "Wysyłanie" -#: models.py:31 models.py:75 +#: constants.py:28 +msgid "Setting thumbnail" +msgstr "Wysyłanie miniatury" + +#: models.py:31 models.py:82 msgid "project" msgstr "projekt" @@ -63,232 +91,279 @@ msgstr "np. rozdział w powieści" msgid "index" msgstr "numer" -#: models.py:70 -msgid "parts count" -msgstr "liczba części" +#: models.py:69 +msgid "Ordering of parts of a book." +msgstr "Kolejność części w książce" #: models.py:71 +msgid "Volume name for YouTube" +msgstr "Nazwa woluminu na YouTube" + +#: models.py:75 +msgid "" +"If set, audiobooks with the save value will be published as single YouTube " +"video." +msgstr "" +"Jeśli ustawione, audiobooki z tą samą wartością będą opublikowane jako jeden " +"film na YouTube." + +#: models.py:78 msgid "artist" msgstr "lektor" -#: models.py:72 +#: models.py:79 msgid "conductor" msgstr "reżyser" -#: models.py:73 +#: models.py:80 msgid "encoded by" msgstr "przyg. techn." -#: models.py:74 +#: models.py:81 msgid "date" msgstr "data" -#: models.py:76 -msgid "book url" -msgstr "URL książki" +#: models.py:83 +msgid "WL catalogue slug of the book." +msgstr "Slug książki z katalogu WL." -#: models.py:77 +#: models.py:84 msgid "translator" msgstr "tłumacz" -#: models.py:79 +#: models.py:86 msgid "license" -msgstr "" +msgstr "licencja" -#: models.py:104 +#: models.py:112 msgid "audiobook" msgstr "audiobook" -#: models.py:105 +#: models.py:113 msgid "audiobooks" msgstr "audiobooki" -#: templates/archive/base.html:6 -msgid "New" -msgstr "Nowe" +#: templates/archive/audiobook_list.html:10 templates/archive/base.html:6 +msgid "Audiobooks" +msgstr "audiobooki" + +#: templates/archive/audiobook_list.html:28 templates/archive/book.html:19 +msgid "Title" +msgstr "tytuł" -#: templates/archive/base.html:7 -msgid "Unpublished" -msgstr "Nie opublikowane" +#: templates/archive/audiobook_list.html:29 +msgid "YouTube volume" +msgstr "wolumin YouTube" -#: templates/archive/base.html:8 templates/archive/file_managed.html:16 +#: templates/archive/base.html:7 templates/archive/file_managed.html:19 msgid "Publishing" msgstr "Publikacja" -#: templates/archive/base.html:9 -msgid "Published" -msgstr "Opublikowane" +#: templates/archive/base.html:8 +msgid "New" +msgstr "Nowe" -#: templates/archive/base.html:10 +#: templates/archive/base.html:9 msgid "Archive" msgstr "Archiwum" -#: templates/archive/base.html:15 +#: templates/archive/base.html:14 msgid "Projects" msgstr "Projekty" -#: templates/archive/base.html:18 +#: templates/archive/base.html:17 msgid "Logout" msgstr "Wyloguj" -#: templates/archive/base.html:20 templates/registration/login.html:9 +#: templates/archive/base.html:19 templates/registration/login.html:9 msgid "Login" msgstr "Zaloguj" -#: templates/archive/base.html:23 +#: templates/archive/base.html:22 msgid "Administration" msgstr "Administracja" -#: templates/archive/file_managed.html:20 +#: templates/archive/book.html:18 +msgid "Index" +msgstr "numer" + +#: templates/archive/file_managed.html:23 msgid "Publishing pending" msgstr "Czeka na publikację" -#: templates/archive/file_managed.html:23 +#: templates/archive/file_managed.html:26 msgid "Cancel publishing" msgstr "Anuluj publikację" -#: templates/archive/file_managed.html:62 +#: templates/archive/file_managed.html:65 msgid "Publish" msgstr "Opublikuj" -#: templates/archive/file_managed.html:71 +#: templates/archive/file_managed.html:74 msgid "Convert without publishing" msgstr "Konwertuj bez publikacji" -#: templates/archive/file_managed.html:78 +#: templates/archive/file_managed.html:82 msgid "Preview YouTube metadata" -msgstr "" +msgstr "Podgląd metadanych dla YouTube" -#: templates/archive/file_managed.html:83 +#: templates/archive/file_managed.html:87 msgid "Publish on YouTube" msgstr "Opublikuj na YouTube" -#: templates/archive/file_managed.html:89 +#: templates/archive/file_managed.html:94 msgid "Update YouTube metadata" -msgstr "" +msgstr "Aktualizuj metadane na YouTube" -#: templates/archive/file_managed.html:106 +#: templates/archive/file_managed.html:111 msgid "MP3 file" msgstr "Plik MP3" -#: templates/archive/file_managed.html:110 +#: templates/archive/file_managed.html:115 msgid "Download MP3 file." msgstr "Pobierz plik MP3." -#: templates/archive/file_managed.html:112 -#: templates/archive/file_managed.html:135 -#: templates/archive/file_managed.html:162 -msgid "Published:" -msgstr "Opublikowano:" - #: templates/archive/file_managed.html:117 #: templates/archive/file_managed.html:140 #: templates/archive/file_managed.html:167 +msgid "Published:" +msgstr "Opublikowano:" + +#: templates/archive/file_managed.html:122 +#: templates/archive/file_managed.html:145 +#: templates/archive/file_managed.html:172 msgid "Not published yet." msgstr "Nie opublikowane." -#: templates/archive/file_managed.html:120 +#: templates/archive/file_managed.html:125 msgid "MP3 file hasn't been generated yet." msgstr "Plik MP3 nie został jeszcze wygenerowany." -#: templates/archive/file_managed.html:129 +#: templates/archive/file_managed.html:134 msgid "Ogg Vorbis file" msgstr "Plik Ogg Vorbis" -#: templates/archive/file_managed.html:133 +#: templates/archive/file_managed.html:138 msgid "Download Ogg Vorbis file." msgstr "Pobierz plik Ogg Vorbis." -#: templates/archive/file_managed.html:143 +#: templates/archive/file_managed.html:148 msgid "Ogg Vorbis file hasn't been generated yet." msgstr "Plik Ogg Vorbis nie został jeszcze wygenerowany." -#: templates/archive/file_managed.html:152 +#: templates/archive/file_managed.html:157 msgid "YouTube" msgstr "YouTube" -#: templates/archive/file_managed.html:158 +#: templates/archive/file_managed.html:163 msgid "See on YouTube" msgstr "Zobacz na YouTube" -#: templates/archive/file_managed.html:170 +#: templates/archive/file_managed.html:175 msgid "YouTube file hasn't been generated yet." msgstr "Plik dla YouTube nie został jeszcze wygenerowany." -#: templates/archive/file_managed.html:189 +#: templates/archive/file_managed.html:194 msgid "Are you sure you want to move this audiobook to archive?" msgstr "Czy na pewno chcesz przenieść ten plik to archiwum?" -#: templates/archive/file_managed.html:191 +#: templates/archive/file_managed.html:196 msgid "Remove to archive" msgstr "Usuń do archiwum" -#: templates/archive/file_managed.html:201 +#: templates/archive/file_managed.html:206 msgid "Update tags" msgstr "Uaktualnij tagi" -#: templates/archive/file_managed.html:208 templates/archive/file_new.html:25 +#: templates/archive/file_managed.html:213 templates/archive/file_new.html:29 msgid "Commit" msgstr "Zatwierdź" -#: templates/archive/file_new.html:13 +#: templates/archive/file_new.html:17 msgid "Move to archive" msgstr "Przenieś do archiwum" -#: templates/archive/file_unmanaged.html:7 +#: templates/archive/file_unmanaged.html:11 msgid "File with same name already exists!" msgstr "Plik o tej nazwie już istnieje!" -#: templates/archive/file_unmanaged.html:23 +#: templates/archive/file_unmanaged.html:27 msgid "Move to new files" msgstr "Przenieś do nowych plików" -#: templates/archive/list_new.html:6 +#: templates/archive/list_new.html:10 msgid "New audiobooks" msgstr "Nowe audiobooki" -#: templates/archive/list_new.html:11 +#: templates/archive/list_new.html:15 msgid "Put source audiobooks in:" msgstr "Umieść nowe audiobooki w:" -#: templates/archive/list_published.html:6 -msgid "Published audiobooks" -msgstr "Opublikowane audiobooki" - -#: templates/archive/list_publishing.html:6 +#: templates/archive/list_publishing.html:9 msgid "Audiobooks being published" msgstr "Aktualnie publikowane audiobooki" -#: templates/archive/list_unmanaged.html:5 +#: templates/archive/list_unmanaged.html:9 msgid "Unmanaged archive" msgstr "Audiobooki archiwalne" -#: templates/archive/list_unpublished.html:6 -msgid "Unpublished audiobooks" -msgstr "Nie opublikowane audiobooki" +#: templates/archive/status.html:7 +msgid "Published at" +msgstr "Opublikowane" + +#: templates/archive/status.html:8 +msgid "OK" +msgstr "OK" #: templates/base.html:12 msgid "Audiobook repository" msgstr "Repozytorium audiobooków" -#: views.py:275 -#, python-format -msgid "" -"Parts number inconsitent. Declared number: %(declared)d. Real number: " -"%(real)d" -msgstr "" -"Niespójność w liczbie części. Zadeklarowana liczba: %(declared)d. Realna " -"liczba: %(real)d." +#: templates/pagination/pagination.html:3 +msgid "Pagination" +msgstr "Paginacja" + +#: templates/pagination/pagination.html:8 +#: templates/pagination/pagination.html:14 +msgid "previous" +msgstr "poprzednia" + +#: templates/pagination/pagination.html:40 +#: templates/pagination/pagination.html:46 +msgid "next" +msgstr "następna" -#: views.py:278 +#: views.py:264 msgid "There is more than one part, but index is not set." msgstr "Utwór ma więcej niż jedną część, ale indeks części nie jest ustawiony." -#: views.py:280 +#: views.py:266 #, python-format msgid "Part indexes are not 1..%(parts_count)d." msgstr "Indeksy części utworu nie tworzą zakresu 1..%(parts_count)d." +#~ msgid "parts count" +#~ msgstr "liczba części" + +#~ msgid "book url" +#~ msgstr "URL książki" + +#~ msgid "Unpublished" +#~ msgstr "Nie opublikowane" + +#~ msgid "Published audiobooks" +#~ msgstr "Opublikowane audiobooki" + +#~ msgid "Unpublished audiobooks" +#~ msgstr "Nie opublikowane audiobooki" + +#~ msgid "" +#~ "Parts number inconsitent. Declared number: %(declared)d. Real number: " +#~ "%(real)d" +#~ msgstr "" +#~ "Niespójność w liczbie części. Zadeklarowana liczba: %(declared)d. Realna " +#~ "liczba: %(real)d." + #~ msgid "Download YouTube file." #~ msgstr "Pobierz plik dla YouTube." -- 2.20.1 From 69fe4abd2389432ba3f9b017b55d4b8d500c92a0 Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Sat, 6 Jun 2020 00:32:07 +0200 Subject: [PATCH 11/16] Badges on audiobook list. --- src/archive/locale/pl/LC_MESSAGES/django.mo | Bin 5020 -> 5020 bytes src/archive/locale/pl/LC_MESSAGES/django.po | 4 ++-- .../templates/archive/audiobook_list.html | 22 ++++-------------- 3 files changed, 6 insertions(+), 20 deletions(-) diff --git a/src/archive/locale/pl/LC_MESSAGES/django.mo b/src/archive/locale/pl/LC_MESSAGES/django.mo index a1c333d3931976705579d658df54cb1ac55ae249..666b791f72faca6d33c76258618258e938b92eb8 100644 GIT binary patch delta 22 dcmbQEK1Y3n2M?36!DcU>D~ybeo8|dxSpiW!266xZ delta 22 dcmbQEK1Y3n2M?2x#bz&_D~ybZo8|dxSpia42AKc= diff --git a/src/archive/locale/pl/LC_MESSAGES/django.po b/src/archive/locale/pl/LC_MESSAGES/django.po index 2aa34bf..6fb331d 100644 --- a/src/archive/locale/pl/LC_MESSAGES/django.po +++ b/src/archive/locale/pl/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2020-06-06 00:25+0200\n" -"PO-Revision-Date: 2020-06-06 00:28+0200\n" +"PO-Revision-Date: 2020-06-06 00:30+0200\n" "Last-Translator: Radek Czajka \n" "Language-Team: \n" "Language: pl\n" @@ -145,7 +145,7 @@ msgstr "audiobooki" #: templates/archive/audiobook_list.html:10 templates/archive/base.html:6 msgid "Audiobooks" -msgstr "audiobooki" +msgstr "Audiobooki" #: templates/archive/audiobook_list.html:28 templates/archive/book.html:19 msgid "Title" diff --git a/src/archive/templates/archive/audiobook_list.html b/src/archive/templates/archive/audiobook_list.html index 1521b42..f4a49a9 100644 --- a/src/archive/templates/archive/audiobook_list.html +++ b/src/archive/templates/archive/audiobook_list.html @@ -1,6 +1,7 @@ {% extends "archive/list.html" %} {% load i18n %} {% load pagination_tags %} +{% load tags %} {% block menu-active-audiobooks %}active{% endblock %} @@ -45,28 +46,13 @@ {{ audiobook.youtube_volume }} - {% if audiobook.mp3_status %} - - MP3: - {{ audiobook.get_mp3_status_display }} - - {% endif %} + {% status audiobook "mp3" %} - {% if audiobooks.ogg_status_display %} - - Ogg: - {{ audiobook.get_ogg_status_display }} - - {% endif %} + {% status audiobook "ogg" %} - {% if audiobooks.youtube_status %} - - YT: - {{ audiobook.get_youtube_status_display }} - - {% endif %} + {% status audiobook "youtube" %} {% endfor %} -- 2.20.1 From db4b95766ce00690d38bff256d77fed006abc54d Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Mon, 6 Jul 2020 12:07:48 +0200 Subject: [PATCH 12/16] Genre- and collection-specific thumbnails. --- .../migrations/0018_auto_20200703_1718.py | 28 ++ .../templates/archive/file_managed.html | 253 ++++++++++-------- src/archive/views.py | 20 +- src/youtube/admin.py | 6 + .../migrations/0011_thumbnailtemplate.py | 28 ++ .../0012_move_thumbnail_definitions.py | 52 ++++ .../migrations/0013_auto_20200706_1123.py | 21 ++ src/youtube/models.py | 92 +++++-- src/youtube/templates/youtube/preview.html | 13 +- src/youtube/thumbnail.py | 18 +- src/youtube/urls.py | 2 + src/youtube/views.py | 24 +- 12 files changed, 395 insertions(+), 162 deletions(-) create mode 100644 src/archive/migrations/0018_auto_20200703_1718.py create mode 100644 src/youtube/migrations/0011_thumbnailtemplate.py create mode 100644 src/youtube/migrations/0012_move_thumbnail_definitions.py create mode 100644 src/youtube/migrations/0013_auto_20200706_1123.py diff --git a/src/archive/migrations/0018_auto_20200703_1718.py b/src/archive/migrations/0018_auto_20200703_1718.py new file mode 100644 index 0000000..2bdb10b --- /dev/null +++ b/src/archive/migrations/0018_auto_20200703_1718.py @@ -0,0 +1,28 @@ +# Generated by Django 3.0.6 on 2020-07-03 17:18 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('archive', '0017_auto_20200603_0011'), + ] + + operations = [ + migrations.AlterField( + model_name='audiobook', + name='mp3_status', + field=models.SmallIntegerField(choices=[(1, 'Waiting'), (10, 'Queued'), (20, 'Encoding'), (30, 'Tagging'), (40, 'Converting audio'), (50, 'Converting video'), (60, 'Assembling audio'), (70, 'Assembling video'), (80, 'Joining audio and video'), (100, 'Sending'), (110, 'Setting thumbnail')], editable=False, null=True), + ), + migrations.AlterField( + model_name='audiobook', + name='ogg_status', + field=models.SmallIntegerField(choices=[(1, 'Waiting'), (10, 'Queued'), (20, 'Encoding'), (30, 'Tagging'), (40, 'Converting audio'), (50, 'Converting video'), (60, 'Assembling audio'), (70, 'Assembling video'), (80, 'Joining audio and video'), (100, 'Sending'), (110, 'Setting thumbnail')], editable=False, null=True), + ), + migrations.AlterField( + model_name='audiobook', + name='youtube_status', + field=models.SmallIntegerField(choices=[(1, 'Waiting'), (10, 'Queued'), (20, 'Encoding'), (30, 'Tagging'), (40, 'Converting audio'), (50, 'Converting video'), (60, 'Assembling audio'), (70, 'Assembling video'), (80, 'Joining audio and video'), (100, 'Sending'), (110, 'Setting thumbnail')], editable=False, null=True), + ), + ] diff --git a/src/archive/templates/archive/file_managed.html b/src/archive/templates/archive/file_managed.html index ecb6292..d6e8be8 100644 --- a/src/archive/templates/archive/file_managed.html +++ b/src/archive/templates/archive/file_managed.html @@ -14,103 +14,59 @@
{{ alert }}
{% endfor %} -
-
-

{% trans "Publishing" %}

+ + +
+
+
+ +
+ + {% csrf_token %} + {% bootstrap_form form %} + + + +
+
-
+ +
{% if audiobook.mp3_status or audiobook.ogg_status or audiobook.youtube_status %}

{% trans "Publishing pending" %}

{% csrf_token %}
- - {% if audiobook.mp3_status %} -
-

MP3

- - {% tags_table audiobook.get_mp3_tags.tags %} - -

Status: {{ audiobook.get_mp3_status_display }}

- {% endif %} - - {% if audiobook.ogg_status %} -
-

Ogg Vorbis

- - {% tags_table audiobook.get_ogg_tags.tags %} - -

Status: {{ audiobook.get_ogg_status_display }}

- {% endif %} - - {% if audiobook.youtube_status %} -
-

YouTube

- -

Status: {{ audiobook.get_youtube_status_display }}

- {% endif %} - - {% else %} - - - {% tags_table audiobook.new_publish_tags 0 %} - -
- -
-
- {% if user_can_publish %} -
- {% csrf_token %} - -
- {% else %} - Podłącz się - {% endif %} - - {% if not audiobook.mp3_published or not audiobook.ogg_published %} -
- {% csrf_token %} - -
- {% endif %} -
-
- {% if audiobook.is_youtube_publishable %} -

- - {% trans "Preview YouTube metadata" %} - -

-
- {% csrf_token %} - -
-
- {% endif %} - {% if audiobook.youtube_id %} -
- {% csrf_token %} - -
- {% endif %} - -
-
-
{% endif %} -
-
-
-
-

{% trans "MP3 file" %}

+

MP3, Ogg

+ {% if audiobook.mp3_status %} +
+

MP3

+ + {% tags_table audiobook.get_mp3_tags.tags %} + +

Status: {{ audiobook.get_mp3_status_display }}

+ {% endif %} + + {% if audiobook.ogg_status %} +
+

Ogg Vorbis

+ + {% tags_table audiobook.get_ogg_tags.tags %} + +

Status: {{ audiobook.get_ogg_status_display }}

+ {% endif %} + + {% if audiobook.mp3_file %}

{% trans "Download MP3 file." %}

{% if audiobook.mp3_published %} @@ -124,16 +80,7 @@ {% else %}

{% trans "MP3 file hasn't been generated yet." %}

{% endif %} -
-
-
-
-
-
-

{% trans "Ogg Vorbis file" %}

-
-
{% if audiobook.ogg_file %}

{% trans "Download Ogg Vorbis file." %}

{% if audiobook.ogg_published %} @@ -147,20 +94,46 @@ {% else %}

{% trans "Ogg Vorbis file hasn't been generated yet." %}

{% endif %} + + {% if user_can_publish %} +
+ {% csrf_token %} + +
+ {% else %} + Podłącz się + {% endif %} + + {% if not audiobook.mp3_published or not audiobook.ogg_published %} +
+ {% csrf_token %} + +
+ {% endif %} + +
-
-
-

{% trans "YouTube" %}

+

YouTube

+ {% if audiobook.youtube_status %} +
+

YouTube

+ +

Status: {{ audiobook.get_youtube_status_display }}

+ {% endif %} + {% if audiobook.youtube_id %} {% if audiobook.youtube_id %}

- {% trans "See on YouTube" %} + + + +

{% endif %} {% if audiobook.youtube_published %} @@ -174,48 +147,90 @@ {% else %}

{% trans "YouTube file hasn't been generated yet." %}

{% endif %} + + {% if audiobook.youtube_id %} +
+ {% csrf_token %} + +
+
+ +
+ {% csrf_token %} + +
+ {% endif %} + + {% if audiobook.is_youtube_publishable %} +
+

+ + {% trans "Preview YouTube metadata" %} + +

+
+ {% csrf_token %} + +
+ {% endif %} +
+
+ + + +
+
-

Plik źródłowy

+

Metadane dla MP3, Ogg

-

Last modified: {{ audiobook.modified }}

-

Plik źródłowy: {{ path }} - (sha1: {{ audiobook.source_sha1 }}). -

- {% multiple_tags_table tags %} - -
- {% csrf_token %} - -
- + + {% tags_table audiobook.new_publish_tags 0 %} +
-
+
-
+
-

{% trans "Update tags" %}

+

Metadane dla YouTube

- -
- {% csrf_token %} - {% bootstrap_form form %} - -
- + + {{ youtube_title }}

+ {{ youtube_description|linebreaksbr }}
+ +
+ + +
+
+

Plik źródłowy

+
+
+

Last modified: {{ audiobook.modified }}

+

Plik źródłowy: {{ path }} + (sha1: {{ audiobook.source_sha1 }}). +

+ {% multiple_tags_table tags %} + +
+ {% csrf_token %} + +
+ +
{% endblock %} diff --git a/src/archive/views.py b/src/archive/views.py index f1b1c1b..2170aff 100644 --- a/src/archive/views.py +++ b/src/archive/views.py @@ -244,12 +244,14 @@ def file_managed(request, id): except IOError: raise Http404 - path = audiobook.source_file.path[len(settings.FILES_PATH):].lstrip('/') - - # for tags update - tags = mutagen.File(audiobook.source_file.path.encode('utf-8')) - if not tags: - tags = {} + tags = {} + if audiobook.source_file: + path = audiobook.source_file.path[len(settings.FILES_PATH):].lstrip('/') + + # for tags update + tags = mutagen.File(audiobook.source_file.path.encode('utf-8')) + if not tags: + tags = {} form = AudiobookForm(instance=audiobook) user_can_publish = ( @@ -265,6 +267,12 @@ def file_managed(request, id): if set(series.values_list('index', flat=True)) != set(range(1, parts_count + 1)): alerts.append(_('Part indexes are not 1..%(parts_count)d.') % {"parts_count": parts_count}) + from youtube.models import YouTube + youtube = YouTube.objects.first() + youtube_title = youtube.get_title(audiobook) + youtube_description = youtube.get_description(audiobook) + + return render(request, "archive/file_managed.html", locals()) diff --git a/src/youtube/admin.py b/src/youtube/admin.py index 2edc745..3c36eee 100644 --- a/src/youtube/admin.py +++ b/src/youtube/admin.py @@ -14,3 +14,9 @@ admin.site.register(models.YouTube, YouTubeAdmin) admin.site.register(models.Font) + + +class ThumbnailTemplateAdmin(admin.ModelAdmin): + list_display = ['order', 'genres', 'collections'] + +admin.site.register(models.ThumbnailTemplate, ThumbnailTemplateAdmin) diff --git a/src/youtube/migrations/0011_thumbnailtemplate.py b/src/youtube/migrations/0011_thumbnailtemplate.py new file mode 100644 index 0000000..1f9999b --- /dev/null +++ b/src/youtube/migrations/0011_thumbnailtemplate.py @@ -0,0 +1,28 @@ +# Generated by Django 3.0.6 on 2020-07-03 17:18 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('youtube', '0010_auto_20200520_1353'), + ] + + operations = [ + migrations.CreateModel( + name='ThumbnailTemplate', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('order', models.SmallIntegerField()), + ('is_active', models.BooleanField()), + ('background', models.FileField(upload_to='youtube/thumbnail')), + ('definition', models.TextField()), + ('genres', models.CharField(blank=True, max_length=255)), + ('collections', models.CharField(blank=True, max_length=255)), + ], + options={ + 'ordering': ('order',), + }, + ), + ] diff --git a/src/youtube/migrations/0012_move_thumbnail_definitions.py b/src/youtube/migrations/0012_move_thumbnail_definitions.py new file mode 100644 index 0000000..a54cf12 --- /dev/null +++ b/src/youtube/migrations/0012_move_thumbnail_definitions.py @@ -0,0 +1,52 @@ +# Generated by Django 3.0.6 on 2020-07-03 17:18 + +from django.db import migrations +import yaml + + +def generate_versions(version_lists): + if not version_lists: + yield [] + else: + for subversion in generate_versions(version_lists[1:]): + for version in version_lists[0]: + yield [version] + subversion + + +def move_definitions(apps, schema_editor): + YouTube = apps.get_model('youtube', 'YouTube') + ThumbnailTemplate = apps.get_model('youtube', 'ThumbnailTemplate') + order = 1 + for youtube in YouTube.objects.all(): + src_def = yaml.load(youtube.thumbnail_definition) + version_lists = [ + box['versions'] + for box in src_def['boxes'] + ] + for version in generate_versions(version_lists): + defn = yaml.dump( + {"boxes": version}, + allow_unicode=True, + default_flow_style=False + ) + ThumbnailTemplate.objects.create( + order=order, + is_active=True, + background=youtube.thumbnail_template, + definition=defn, + ) + order += 1 + + +class Migration(migrations.Migration): + + dependencies = [ + ('youtube', '0011_thumbnailtemplate'), + ] + + operations = [ + migrations.RunPython( + move_definitions, + migrations.RunPython.noop + ) + ] diff --git a/src/youtube/migrations/0013_auto_20200706_1123.py b/src/youtube/migrations/0013_auto_20200706_1123.py new file mode 100644 index 0000000..74485df --- /dev/null +++ b/src/youtube/migrations/0013_auto_20200706_1123.py @@ -0,0 +1,21 @@ +# Generated by Django 3.0.6 on 2020-07-06 11:23 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('youtube', '0012_move_thumbnail_definitions'), + ] + + operations = [ + migrations.RemoveField( + model_name='youtube', + name='thumbnail_definition', + ), + migrations.RemoveField( + model_name='youtube', + name='thumbnail_template', + ), + ] diff --git a/src/youtube/models.py b/src/youtube/models.py index 5fe3c2b..219a682 100644 --- a/src/youtube/models.py +++ b/src/youtube/models.py @@ -4,6 +4,7 @@ from tempfile import NamedTemporaryFile from django.db import models from django.utils.translation import gettext_lazy as _ from django.template import Template, Context +import requests from apiclient import youtube_call from .utils import ( concat_audio, @@ -32,8 +33,6 @@ class YouTube(models.Model): outro_flac = models.FileField(upload_to='youtube/outro_flac', blank=True) loop_card = models.FileField(upload_to='youtube/card', blank=True) loop_video = models.FileField(upload_to='youtube/loop_video', blank=True) - thumbnail_template = models.FileField(upload_to='youtube/thumbnail', blank=True) - thumbnail_definition = models.TextField(blank=True) privacy_status = models.CharField(max_length=16, choices=[ ('public', _('public')), ('unlisted', _('unlisted')), @@ -177,27 +176,21 @@ class YouTube(models.Model): def update_thumbnail(self, audiobook): thumbnail = self.prepare_thumbnail(audiobook) - response = youtube_call( - "POST", - "https://www.googleapis.com/upload/youtube/v3/thumbnails/set", - params={'videoId': audiobook.youtube_id}, - data=thumbnail.getvalue(), - ) + if thumbnail is not None: + response = youtube_call( + "POST", + "https://www.googleapis.com/upload/youtube/v3/thumbnails/set", + params={'videoId': audiobook.youtube_id}, + data=thumbnail.getvalue(), + ) def prepare_thumbnail(self, audiobook): - img = create_thumbnail( - self.thumbnail_template.path, - self.thumbnail_definition, - { - "author": ', '.join((a['name'] for a in audiobook.book['authors'])), - "title": audiobook.book['title'], - "part": (audiobook.youtube_volume or audiobook.part_name).strip(), - }, - lambda name: Font.objects.get(name=name).truetype.path - ) - buf = io.BytesIO() - img.save(buf, format='PNG') - return buf + for thumbnail_template in ThumbnailTemplate.objects.filter(is_active=True).order_by('order'): + if not thumbnail_template.is_for_audiobook(audiobook): + continue + thumbnail = thumbnail_template.generate(audiobook) + if thumbnail is not None: + return thumbnail class Card(models.Model): @@ -216,3 +209,60 @@ class Font(models.Model): def __str__(self): return self.name + + +class ThumbnailTemplate(models.Model): + order = models.SmallIntegerField() + is_active = models.BooleanField() + background = models.FileField(upload_to='youtube/thumbnail') + definition = models.TextField() + genres = models.CharField(max_length=255, blank=True) + collections = models.CharField(max_length=255, blank=True) + + class Meta: + ordering = ('order', ) + + def generate(self, audiobook): + try: + img = create_thumbnail( + self.background.path, + self.definition, + { + "author": ', '.join((a['name'] for a in audiobook.book['authors'])), + "title": audiobook.book['title'], + "part": (audiobook.youtube_volume or audiobook.part_name).strip(), + }, + lambda name: Font.objects.get(name=name).truetype.path + ) + except Exception as e: + print(e) + return + else: + buf = io.BytesIO() + img.save(buf, format='PNG') + return buf + + def is_for_audiobook(self, audiobook): + if self.genres: + book_genres = set([g['slug'] for g in audiobook.book['genres']]) + template_genres = set([g.strip() for g in self.genres.split(',')]) + if not book_genres.intersection(template_genres): + return False + + if self.collections: + template_collections = set([g.strip() for g in self.collections.split(',')]) + in_any = False + for collection in template_collections: + apidata = requests.get( + f'https://wolnelektury.pl/api/collections/{collection}/' + ).json() + for book in apidata['books']: + if book['slug'] == audiobook.slug: + in_any = True + break + if in_any: + break + if not in_any: + return False + + return True diff --git a/src/youtube/templates/youtube/preview.html b/src/youtube/templates/youtube/preview.html index c017265..9dee0ee 100644 --- a/src/youtube/templates/youtube/preview.html +++ b/src/youtube/templates/youtube/preview.html @@ -12,10 +12,19 @@
-
+
+ {% for t in templates %} +
+ + + +
+ {% endfor %} +
+
+
-

{{ title }}

diff --git a/src/youtube/thumbnail.py b/src/youtube/thumbnail.py index 42e8070..8eb7973 100644 --- a/src/youtube/thumbnail.py +++ b/src/youtube/thumbnail.py @@ -2,12 +2,6 @@ import yaml from PIL import Image, ImageDraw, ImageFont -def drawbox(img, d, context, get_font_path): - for version in d['versions']: - if draw_version(img, version, context, get_font_path): - break - - def split_to_lines(text, draw, font, max_width): words = text.split() current = '' @@ -28,8 +22,7 @@ def split_to_lines(text, draw, font, max_width): yield current -def draw_version(img, d, context, get_font_path): - # todo: do this in a subimg +def draw_box(img, d, context, get_font_path): newimg = Image.new( 'RGBA', ( @@ -48,7 +41,11 @@ def draw_version(img, d, context, get_font_path): continue if item.get('uppercase'): text = text.upper() - font = ImageFont.truetype(get_font_path(item['font-family']), item['font-size']) + font = ImageFont.truetype( + get_font_path(item['font-family']), + item['font-size'], + layout_engine=ImageFont.LAYOUT_BASIC + ) max_width = item.get('max-width', newimg.size[0]) for line in split_to_lines(text, draw, font, max_width): @@ -66,5 +63,6 @@ def create_thumbnail(background_path, defn, context, get_font_path): img = Image.open(background_path) d = yaml.load(defn) for boxdef in d['boxes']: - drawbox(img, boxdef, context, get_font_path) + if not draw_box(img, boxdef, context, get_font_path): + raise ValueError() return img diff --git a/src/youtube/urls.py b/src/youtube/urls.py index fb9ddc7..9de274e 100644 --- a/src/youtube/urls.py +++ b/src/youtube/urls.py @@ -5,6 +5,8 @@ from . import views urlpatterns = [ url(r'^publish/(\d+)/$', views.publish, name="youtube_publish"), path('thumbnail//', views.thumbnail, name='youtube_thumbnail'), + path('thumbnail///', views.thumbnail, name='youtube_thumbnail'), path('preview//', views.Preview.as_view(), name="youtube_preview"), path('update//', views.Update.as_view(), name="youtube_update"), + path('update-thumbnail//', views.UpdateThumbnail.as_view(), name="youtube_update_thumbnail"), ] diff --git a/src/youtube/views.py b/src/youtube/views.py index 4c002ee..058178f 100644 --- a/src/youtube/views.py +++ b/src/youtube/views.py @@ -25,11 +25,16 @@ def publish(request, aid, publish=True): return redirect(reverse('file', args=[aid])) -def thumbnail(request, aid): +def thumbnail(request, aid, thumbnail_id=None): audiobook = get_object_or_404(Audiobook, id=aid) - yt = models.YouTube.objects.first() - buf = yt.prepare_thumbnail(audiobook) - return HttpResponse(buf.getvalue(), content_type='image/png') + if thumbnail_id is None: + yt = models.YouTube.objects.first() + buf = yt.prepare_thumbnail(audiobook) + else: + template = get_object_or_404(models.ThumbnailTemplate, id=thumbnail_id) + buf = template.generate(audiobook) + buf = buf.getvalue() if buf is not None else b'' + return HttpResponse(buf, content_type='image/png') class Preview(DetailView): @@ -42,6 +47,7 @@ class Preview(DetailView): ctx['data'] = yt.get_data(ctx['object']) ctx['title'] = yt.get_title(ctx['object']) ctx['description'] = yt.get_description(ctx['object']) + ctx['templates'] = models.ThumbnailTemplate.objects.all() return ctx @@ -53,3 +59,13 @@ class Update(SingleObjectMixin, View): yt = models.YouTube.objects.first() yt.update_data(self.get_object()) return redirect(reverse('file', args=[pk])) + + +@method_decorator(permission_required('archive.change_audiobook'), name='dispatch') +class UpdateThumbnail(SingleObjectMixin, View): + model = Audiobook + + def post(self, request, pk): + yt = models.YouTube.objects.first() + yt.update_thumbnail(self.get_object()) + return redirect(reverse('file', args=[pk])) -- 2.20.1 From 1b434b4f4eb775a20054b0efeca3fea63a161879 Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Mon, 6 Jul 2020 12:48:24 +0200 Subject: [PATCH 13/16] More thumbnail options. --- .../migrations/0014_auto_20200706_1219.py | 28 +++++++++++++++++++ src/youtube/models.py | 14 ++++++---- src/youtube/thumbnail.py | 19 +++++++++---- 3 files changed, 51 insertions(+), 10 deletions(-) create mode 100644 src/youtube/migrations/0014_auto_20200706_1219.py diff --git a/src/youtube/migrations/0014_auto_20200706_1219.py b/src/youtube/migrations/0014_auto_20200706_1219.py new file mode 100644 index 0000000..d53c7cd --- /dev/null +++ b/src/youtube/migrations/0014_auto_20200706_1219.py @@ -0,0 +1,28 @@ +# Generated by Django 3.0.6 on 2020-07-06 12:19 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('youtube', '0013_auto_20200706_1123'), + ] + + operations = [ + migrations.AddField( + model_name='thumbnailtemplate', + name='authors', + field=models.CharField(blank=True, max_length=255), + ), + migrations.AddField( + model_name='thumbnailtemplate', + name='epochs', + field=models.CharField(blank=True, max_length=255), + ), + migrations.AddField( + model_name='thumbnailtemplate', + name='kinds', + field=models.CharField(blank=True, max_length=255), + ), + ] diff --git a/src/youtube/models.py b/src/youtube/models.py index 219a682..356e335 100644 --- a/src/youtube/models.py +++ b/src/youtube/models.py @@ -216,6 +216,9 @@ class ThumbnailTemplate(models.Model): is_active = models.BooleanField() background = models.FileField(upload_to='youtube/thumbnail') definition = models.TextField() + authors = models.CharField(max_length=255, blank=True) + epochs = models.CharField(max_length=255, blank=True) + kinds = models.CharField(max_length=255, blank=True) genres = models.CharField(max_length=255, blank=True) collections = models.CharField(max_length=255, blank=True) @@ -243,11 +246,12 @@ class ThumbnailTemplate(models.Model): return buf def is_for_audiobook(self, audiobook): - if self.genres: - book_genres = set([g['slug'] for g in audiobook.book['genres']]) - template_genres = set([g.strip() for g in self.genres.split(',')]) - if not book_genres.intersection(template_genres): - return False + for category in 'authors', 'epochs', 'kinds', 'genres': + if getattr(self, category): + book_slugs = set([g['slug'] for g in audiobook.book[category]]) + template_slugs = set([g.strip() for g in getattr(self, category).split(',')]) + if not book_slugs.intersection(template_slugs): + return False if self.collections: template_collections = set([g.strip() for g in self.collections.split(',')]) diff --git a/src/youtube/thumbnail.py b/src/youtube/thumbnail.py index 8eb7973..e40b552 100644 --- a/src/youtube/thumbnail.py +++ b/src/youtube/thumbnail.py @@ -22,7 +22,7 @@ def split_to_lines(text, draw, font, max_width): yield current -def draw_box(img, d, context, get_font_path): +def draw_box(img, d, context, get_font_path, scale): newimg = Image.new( 'RGBA', ( @@ -35,7 +35,7 @@ def draw_box(img, d, context, get_font_path): cursor = 0 for item in d['items']: if item.get('vskip'): - cursor += item['vskip'] + cursor += int(round(item['vskip'] * scale)) text = item['text'].format(**context) if not text: continue @@ -43,7 +43,7 @@ def draw_box(img, d, context, get_font_path): text = text.upper() font = ImageFont.truetype( get_font_path(item['font-family']), - item['font-size'], + int(round(item['font-size'] * scale)), layout_engine=ImageFont.LAYOUT_BASIC ) max_width = item.get('max-width', newimg.size[0]) @@ -53,16 +53,25 @@ def draw_box(img, d, context, get_font_path): if cursor + realheight > newimg.size[1]: return False draw.text((0, cursor), line, font=font, fill=item.get('color')) - cursor += item['line-height'] + cursor += int(round(item['line-height'] * scale)) img.paste(newimg, (d.get('x', 0), d.get('y', 0)), newimg) return True +def draw_box_with_scaling(img, d, context, get_font_path): + scale = 1.0 + while scale > 0: + if draw_box(img, d, context, get_font_path, scale): + return True + scale -= 0.05 + + + def create_thumbnail(background_path, defn, context, get_font_path): img = Image.open(background_path) d = yaml.load(defn) for boxdef in d['boxes']: - if not draw_box(img, boxdef, context, get_font_path): + if not draw_box_with_scaling(img, boxdef, context, get_font_path): raise ValueError() return img -- 2.20.1 From 0d603e3afb8bc40ea2f8d960bcd4a3faa4ed2328 Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Mon, 6 Jul 2020 13:47:10 +0200 Subject: [PATCH 14/16] Thumbnail fixes. --- src/youtube/admin.py | 2 +- src/youtube/templates/youtube/preview.html | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/youtube/admin.py b/src/youtube/admin.py index 3c36eee..32a84d0 100644 --- a/src/youtube/admin.py +++ b/src/youtube/admin.py @@ -17,6 +17,6 @@ admin.site.register(models.Font) class ThumbnailTemplateAdmin(admin.ModelAdmin): - list_display = ['order', 'genres', 'collections'] + list_display = ['order', 'collections', 'authors', 'epochs', 'genres', 'kinds'] admin.site.register(models.ThumbnailTemplate, ThumbnailTemplateAdmin) diff --git a/src/youtube/templates/youtube/preview.html b/src/youtube/templates/youtube/preview.html index 9dee0ee..48202c0 100644 --- a/src/youtube/templates/youtube/preview.html +++ b/src/youtube/templates/youtube/preview.html @@ -12,17 +12,16 @@
-
+
{% for t in templates %} -
+ {% endfor %}
-
-
+
-- 2.20.1 From 5ee0595f97148a89736d208b08021705d88784e2 Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Mon, 6 Jul 2020 13:48:20 +0200 Subject: [PATCH 15/16] Minor layout fix. --- src/youtube/templates/youtube/preview.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/youtube/templates/youtube/preview.html b/src/youtube/templates/youtube/preview.html index 48202c0..ff007f0 100644 --- a/src/youtube/templates/youtube/preview.html +++ b/src/youtube/templates/youtube/preview.html @@ -12,9 +12,9 @@
-
+
{% for t in templates %} -
+
-- 2.20.1 From 52421a3d925957f14d3fa2d8ce37ee82a4918e5e Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Tue, 18 Aug 2020 16:04:34 +0200 Subject: [PATCH 16/16] Update Django, add search. --- requirements.txt | 2 +- src/archive/templates/archive/audiobook_list.html | 8 ++++++++ src/archive/views.py | 7 ++++++- src/audiobooks/settings.py | 1 + 4 files changed, 16 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index c355845..bfe2ad6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ -i https://py.mdrn.pl:8443/simple -Django==3.0.6 +Django==3.1 django-cas-ng==4.1.1 django-bootstrap4==1.1.1 django-pglocks==1.0.4 diff --git a/src/archive/templates/archive/audiobook_list.html b/src/archive/templates/archive/audiobook_list.html index f4a49a9..b006726 100644 --- a/src/archive/templates/archive/audiobook_list.html +++ b/src/archive/templates/archive/audiobook_list.html @@ -13,6 +13,14 @@ {% block file-list-info %} +
+
+ +
+ +
+
+
{% endblock %} {% block file-list-wrapper %} diff --git a/src/archive/views.py b/src/archive/views.py index 2170aff..fab7a1c 100644 --- a/src/archive/views.py +++ b/src/archive/views.py @@ -5,6 +5,7 @@ from urllib.parse import quote from archive import settings from django.contrib.auth.decorators import permission_required +from django.contrib.postgres.search import SearchVector from django.urls import reverse from django.db.models import Q, Max from django.http import Http404, HttpResponse @@ -229,7 +230,11 @@ def list_publishing(request): class AudiobookList(ListView): - queryset = models.Audiobook.objects.all() + def get_queryset(self): + qs = models.Audiobook.objects.all() + if 's' in self.request.GET: + qs = qs.annotate(s=SearchVector('title', 'slug')).filter(s=self.request.GET['s']) + return qs @permission_required('archive.change_audiobook') diff --git a/src/audiobooks/settings.py b/src/audiobooks/settings.py index 901da0f..f36fe81 100644 --- a/src/audiobooks/settings.py +++ b/src/audiobooks/settings.py @@ -125,6 +125,7 @@ INSTALLED_APPS = ( 'django.contrib.messages', 'django.contrib.staticfiles', 'django.contrib.admin', + 'django.contrib.postgres', # Uncomment the next line to enable admin documentation: # 'django.contrib.admindocs', 'bootstrap4', -- 2.20.1