--- /dev/null
+# 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),
+ ),
+ ]
<div class="mt-4 alert alert-warning">{{ alert }}</div>
{% endfor %}
-<div class="card mt-4">
- <div class="card-header">
- <h2>{% trans "Publishing" %}</h2>
+
+
+<div class="row mt-4">
+ <div class="col-xl-7">
+ <div class="card mt-4">
+ <div class="card-header">
+ <h2><a href="{% url 'book' audiobook.slug %}" %}">{{ audiobook.title }}</a></h2>
+ </div>
+ <div class="card-body">
+ <form method='post' action='.'>
+ {% csrf_token %}
+ {% bootstrap_form form %}
+ <input class="btn btn-primary" type="submit" value='{% trans "Commit" %}' />
+ </form>
+
+ </div>
+ </div>
</div>
- <div class="card-body">
+
+ <div class="col-xl-5">
{% if audiobook.mp3_status or audiobook.ogg_status or audiobook.youtube_status %}
<h2>{% trans "Publishing pending" %}</h2>
<form method="post" action="{% url 'cancel_publishing' audiobook.id %}">
{% csrf_token %}
<input class='btn btn-danger' type="submit" value="{% trans "Cancel publishing" %}" />
</form>
-
- {% if audiobook.mp3_status %}
- <hr/>
- <h2>MP3</h2>
-
- {% tags_table audiobook.get_mp3_tags.tags %}
-
- <p>Status: <b>{{ audiobook.get_mp3_status_display }}</b></p>
- {% endif %}
-
- {% if audiobook.ogg_status %}
- <hr/>
- <h2>Ogg Vorbis</h2>
-
- {% tags_table audiobook.get_ogg_tags.tags %}
-
- <p>Status: <b>{{ audiobook.get_ogg_status_display }}</b></p>
- {% endif %}
-
- {% if audiobook.youtube_status %}
- <hr/>
- <h2>YouTube</h2>
-
- <p>Status: <b>{{ audiobook.get_youtube_status_display }}</b></p>
- {% endif %}
-
- {% else %}
-
- <table class='table'>
- {% tags_table audiobook.new_publish_tags 0 %}
- <tr><th></th><td>
-
- <div class="row">
- <div class="col-md-6">
- {% if user_can_publish %}
- <form method="post" action="{% url 'publish' audiobook.id %}">
- {% csrf_token %}
- <input class="btn btn-primary" type="submit" value="{% trans "Publish" %}" />
- </form>
- {% else %}
- <a class="btn btn-primary" href="{% url 'apiclient_oauth' %}">Podłącz się</a>
- {% endif %}
-
- {% if not audiobook.mp3_published or not audiobook.ogg_published %}
- <form class="mt-3" method="post" action="{% url 'convert' audiobook.id %}">
- {% csrf_token %}
- <input class="btn btn-secondary" type="submit" value="{% trans "Convert without publishing" %}" />
- </form>
- {% endif %}
- </div>
- <div class="col-md-6">
- {% if audiobook.is_youtube_publishable %}
- <p>
- <a href="{% url 'youtube_preview' audiobook.id %}">
- {% trans "Preview YouTube metadata" %}
- </a>
- </p>
- <form method="post" action="{% url 'youtube_publish' audiobook.id %}">
- {% csrf_token %}
- <input class="btn btn-primary" type="submit" value="{% trans "Publish on YouTube" %}" />
- </form>
- <br>
- {% endif %}
- {% if audiobook.youtube_id %}
- <form method="post" action="{% url 'youtube_update' audiobook.id %}">
- {% csrf_token %}
- <input class="btn btn-secondary" type="submit" value="{% trans "Update YouTube metadata" %}">
- </form>
- {% endif %}
-
- </div>
- </div>
- </td></tr>
- </table>
{% endif %}
- </div>
-</div>
-<div class="row">
- <div class="col-xl-4">
<div class="card mt-4">
<div class="card-header">
- <h2>{% trans "MP3 file" %}</h2>
+ <h3>MP3, Ogg</h3>
</div>
<div class="card-body">
+ {% if audiobook.mp3_status %}
+ <hr/>
+ <h2>MP3</h2>
+
+ {% tags_table audiobook.get_mp3_tags.tags %}
+
+ <p>Status: <b>{{ audiobook.get_mp3_status_display }}</b></p>
+ {% endif %}
+
+ {% if audiobook.ogg_status %}
+ <hr/>
+ <h2>Ogg Vorbis</h2>
+
+ {% tags_table audiobook.get_ogg_tags.tags %}
+
+ <p>Status: <b>{{ audiobook.get_ogg_status_display }}</b></p>
+ {% endif %}
+
+
{% if audiobook.mp3_file %}
<p><a href="{% url 'download' audiobook.id 'mp3' %}">{% trans "Download MP3 file." %}</a></p>
{% if audiobook.mp3_published %}
{% else %}
<p>{% trans "MP3 file hasn't been generated yet." %}</p>
{% endif %}
- </div>
- </div>
- </div>
- <div class="col-xl-4">
- <div class="card mt-4">
- <div class="card-header">
- <h2>{% trans "Ogg Vorbis file" %}</h2>
- </div>
- <div class="card-body">
{% if audiobook.ogg_file %}
<p><a href="{% url 'download' audiobook.id 'ogg' %}">{% trans "Download Ogg Vorbis file." %}</a></p>
{% if audiobook.ogg_published %}
{% else %}
<p>{% trans "Ogg Vorbis file hasn't been generated yet." %}</p>
{% endif %}
+
+ {% if user_can_publish %}
+ <form method="post" action="{% url 'publish' audiobook.id %}">
+ {% csrf_token %}
+ <input class="btn btn-primary" type="submit" value="{% trans "Publish" %}" />
+ </form>
+ {% else %}
+ <a class="btn btn-primary" href="{% url 'apiclient_oauth' %}">Podłącz się</a>
+ {% endif %}
+
+ {% if not audiobook.mp3_published or not audiobook.ogg_published %}
+ <form class="mt-3" method="post" action="{% url 'convert' audiobook.id %}">
+ {% csrf_token %}
+ <input class="btn btn-secondary" type="submit" value="{% trans "Convert without publishing" %}" />
+ </form>
+ {% endif %}
+
+
</div>
</div>
- </div>
- <div class="col-xl-4">
<div class="card mt-4">
<div class="card-header">
- <h2>{% trans "YouTube" %}</h2>
+ <h3>YouTube</h3>
</div>
<div class="card-body">
+ {% if audiobook.youtube_status %}
+ <hr/>
+ <h2>YouTube</h2>
+
+ <p>Status: <b>{{ audiobook.get_youtube_status_display }}</b></p>
+ {% endif %}
+
{% if audiobook.youtube_id %}
{% if audiobook.youtube_id %}
<p>
- <a href="https://youtu.be/{{ audiobook.youtube_id }}" target="_blank">{% trans "See on YouTube" %}</a>
+ <a href="https://youtu.be/{{ audiobook.youtube_id }}" target="_blank" title="{% trans "See on YouTube" %}">
+ <img src="https://i.ytimg.com/vi/{{ audiobook.youtube_id }}/hq720.jpg" style="width: 100%">
+
+ </a>
</p>
{% endif %}
{% if audiobook.youtube_published %}
{% else %}
<p>{% trans "YouTube file hasn't been generated yet." %}</p>
{% endif %}
+
+ {% if audiobook.youtube_id %}
+ <form method="post" action="{% url 'youtube_update_thumbnail' audiobook.id %}">
+ {% csrf_token %}
+ <input class="btn btn-secondary" type="submit" value="{% trans "Update YouTube thumbnail" %}">
+ </form>
+ <br>
+
+ <form method="post" action="{% url 'youtube_update' audiobook.id %}">
+ {% csrf_token %}
+ <input class="btn btn-secondary" type="submit" value="{% trans "Update YouTube metadata" %}">
+ </form>
+ {% endif %}
+
+ {% if audiobook.is_youtube_publishable %}
+ <br>
+ <p>
+ <a href="{% url 'youtube_preview' audiobook.id %}">
+ {% trans "Preview YouTube metadata" %}
+ </a>
+ </p>
+ <form method="post" action="{% url 'youtube_publish' audiobook.id %}">
+ {% csrf_token %}
+ <input class="btn btn-primary" type="submit" value="{% trans "Publish on YouTube" %}" />
+ </form>
+ {% endif %}
+
</div>
</div>
</div>
+</div>
+
+
+
+<div class="row">
<div class="col-xl-6">
+
<div class="card mt-4">
<div class="card-header">
- <h2>Plik źródłowy</h2>
+ <h3>Metadane dla MP3, Ogg</h3>
</div>
<div class="card-body">
- <p>Last modified: {{ audiobook.modified }}</p>
- <p>Plik źródłowy: <a href='{% url "download" audiobook.id %}'>{{ path }}</a>
- (sha1: <tt>{{ audiobook.source_sha1 }}</tt>).
- </p>
- {% multiple_tags_table tags %}
-
- <form method="post" action="{% url 'remove_to_archive' audiobook.id %}"
- onsubmit='return confirm("{% trans "Are you sure you want to move this audiobook to archive?" %}")'>
- {% csrf_token %}
- <input class="btn btn-danger" type="submit" value="{% trans "Remove to archive" %}" />
- </form>
-
+ <table class='table'>
+ {% tags_table audiobook.new_publish_tags 0 %}
+ </table>
</div>
</div>
- </div>
+ </div>
<div class="col-xl-6">
- <div class="card mt-4 mb-4">
+ <div class="card mt-4">
<div class="card-header">
- <h2>{% trans "Update tags" %}</h2>
+ <h3>Metadane dla YouTube</h3>
</div>
<div class="card-body">
-
- <form method='post' action='.'>
- {% csrf_token %}
- {% bootstrap_form form %}
- <input class="btn btn-primary" type="submit" value='{% trans "Commit" %}' />
- </form>
-
+ <img src="{% url 'youtube_thumbnail' audiobook.id %}" style="width:100%">
+ <strong>{{ youtube_title }}</strong><br><br>
+ {{ youtube_description|linebreaksbr }}
</div>
</div>
</div>
+
+</div>
+
+
+<div class="card mt-4 mb-4">
+ <div class="card-header">
+ <h2>Plik źródłowy</h2>
+ </div>
+ <div class="card-body">
+ <p>Last modified: {{ audiobook.modified }}</p>
+ <p>Plik źródłowy: <a href='{% url "download" audiobook.id %}'>{{ path }}</a>
+ (sha1: <tt>{{ audiobook.source_sha1 }}</tt>).
+ </p>
+ {% multiple_tags_table tags %}
+
+ <form method="post" action="{% url 'remove_to_archive' audiobook.id %}"
+ onsubmit='return confirm("{% trans "Are you sure you want to move this audiobook to archive?" %}")'>
+ {% csrf_token %}
+ <input class="btn btn-danger" type="submit" value="{% trans "Remove to archive" %}" />
+ </form>
+
+ </div>
</div>
{% endblock %}
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 = (
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())
admin.site.register(models.Font)
+
+
+class ThumbnailTemplateAdmin(admin.ModelAdmin):
+ list_display = ['order', 'genres', 'collections']
+
+admin.site.register(models.ThumbnailTemplate, ThumbnailTemplateAdmin)
--- /dev/null
+# 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',),
+ },
+ ),
+ ]
--- /dev/null
+# 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
+ )
+ ]
--- /dev/null
+# 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',
+ ),
+ ]
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,
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')),
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):
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
</div>
<div class="card-body">
<img src="{% url 'youtube_thumbnail' object.id %}" style="width:100%">
- </div>
+ <div class="row">
+ {% for t in templates %}
+ <div class="col-md-2">
+ <a href="{% url 'youtube_thumbnail' object.id t.id %}">
+ <img src="{% url 'youtube_thumbnail' object.id t.id %}" style="width:100%">
+ </a>
+ </div>
+ {% endfor %}
+ </div>
+ </div>
+ </div>
</div>
-
<div class="card mt-4 mb-4">
<div class="card-header">
<h2>{{ title }}</h2>
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 = ''
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',
(
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):
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
urlpatterns = [
url(r'^publish/(\d+)/$', views.publish, name="youtube_publish"),
path('thumbnail/<int:aid>/', views.thumbnail, name='youtube_thumbnail'),
+ path('thumbnail/<int:aid>/<int:thumbnail_id>/', views.thumbnail, name='youtube_thumbnail'),
path('preview/<int:pk>/', views.Preview.as_view(), name="youtube_preview"),
path('update/<int:pk>/', views.Update.as_view(), name="youtube_update"),
+ path('update-thumbnail/<int:pk>/', views.UpdateThumbnail.as_view(), name="youtube_update_thumbnail"),
]
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):
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
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]))