else:
raise ApiError("WL API call error %s, path: %s" % (r.status_code, path))
+
+def youtube_call(method, url, params=None, data=None, media_data=None):
+ from .models import YouTubeToken
+ yt = YouTubeToken.objects.first()
+ return yt.call(method, url, params=params, data=data, media_data=media_data)
--- /dev/null
+# Generated by Django 3.0.4 on 2020-04-06 10:41
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('apiclient', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='YouTubeToken',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('token', models.TextField()),
+ ],
+ ),
+ ]
+import json
from django.db import models
from django.contrib.auth.models import User
+from requests_oauthlib import OAuth2Session
+from .settings import YOUTUBE_CLIENT_ID, YOUTUBE_CLIENT_SECRET, YOUTUBE_TOKEN_URL
class OAuthConnection(models.Model):
return o
+class YouTubeToken(models.Model):
+ token = models.TextField()
+
+ def token_updater(self, token):
+ self.token = json.dumps(token)
+ self.save()
+
+ def get_session(self):
+ return OAuth2Session(
+ client_id=YOUTUBE_CLIENT_ID,
+ auto_refresh_url=YOUTUBE_TOKEN_URL,
+ token=json.loads(self.token),
+ auto_refresh_kwargs={'client_id':YOUTUBE_CLIENT_ID,'client_secret':YOUTUBE_CLIENT_SECRET},
+ token_updater=self.token_updater
+ )
+
+ def call(self, method, url, params=None, data=None, media_data=None):
+ params = params or {}
+ params['uploadType'] = 'resumable'
+
+ session = self.get_session()
+ response = session.request(
+ method=method,
+ url=url,
+ json=data,
+ params=params,
+ headers={
+ 'X-Upload-Content-Length': str(len(media_data)),
+ 'x-upload-content-type': 'application/octet-stream',
+ }
+ )
+ location = response.headers['Location']
+ return session.put(
+ url=location,
+ data=media_data,
+ headers={"Content-Type": "application/octet-stream"},
+ )
WL_API_URL + 'oauth/access_token/')
WL_AUTHORIZE_URL = getattr(settings, 'APICLIENT_WL_AUTHORIZE_URL',
WL_API_URL + 'oauth/authorize/')
+
+
+YOUTUBE_SCOPE = [
+ 'https://www.googleapis.com/auth/youtube',
+]
+YOUTUBE_AUTH_URL = 'https://accounts.google.com/o/oauth2/auth'
+YOUTUBE_TOKEN_URL = 'https://oauth2.googleapis.com/token'
+YOUTUBE_CLIENT_ID = getattr(settings, 'YOUTUBE_CLIENT_ID', None)
+YOUTUBE_CLIENT_SECRET = getattr(settings, 'YOUTUBE_CLIENT_SECRET', None)
+
urlpatterns = [
url(r'^oauth/$', views.oauth, name='apiclient_oauth'),
url(r'^oauth_callback/$', views.oauth_callback, name='apiclient_oauth_callback'),
+ url(r'^oauth2/$', views.oauth2, name='apiclient_oauth2'),
+ url(r'^oauth2_redirect/$', views.oauth2_redirect, name='apiclient_oauth2_redirect'),
]
+import json
from django.contrib.auth.decorators import login_required
from django.urls import reverse
from django.http import HttpResponseRedirect, HttpResponse
import requests
-from requests_oauthlib import OAuth1Session
+from requests_oauthlib import OAuth1Session, OAuth2Session
-from apiclient.models import OAuthConnection
+from apiclient.models import OAuthConnection, YouTubeToken
from apiclient.settings import WL_CONSUMER_KEY, WL_CONSUMER_SECRET
-from apiclient.settings import WL_REQUEST_TOKEN_URL, WL_ACCESS_TOKEN_URL, WL_AUTHORIZE_URL
+from apiclient.settings import WL_REQUEST_TOKEN_URL, WL_ACCESS_TOKEN_URL, WL_AUTHORIZE_URL, YOUTUBE_AUTH_URL, YOUTUBE_TOKEN_URL, YOUTUBE_CLIENT_ID, YOUTUBE_CLIENT_SECRET, YOUTUBE_SCOPE
@login_required
conn.save()
return HttpResponseRedirect('/')
+
+
+@login_required
+def oauth2(request):
+ redirect_uri = request.build_absolute_uri(reverse("apiclient_oauth2_redirect"))
+ oauth = OAuth2Session(YOUTUBE_CLIENT_ID, redirect_uri=redirect_uri, scope=YOUTUBE_SCOPE)
+ authorization_url, state = oauth.authorization_url(YOUTUBE_AUTH_URL)
+ return HttpResponseRedirect(authorization_url)
+
+
+@login_required
+def oauth2_redirect(request):
+ redirect_uri = request.build_absolute_uri(reverse("apiclient_oauth2_redirect"))
+ oauth = OAuth2Session(YOUTUBE_CLIENT_ID, redirect_uri=redirect_uri, scope=YOUTUBE_SCOPE)
+ token = oauth.fetch_token(
+ YOUTUBE_TOKEN_URL,
+ authorization_response=request.get_full_path(),
+ client_secret=YOUTUBE_CLIENT_SECRET,
+ )
+ YouTubeToken.objects.all().delete()
+ YouTubeToken.objects.create(token=json.dumps(token))
+ return HttpResponseRedirect('/')
#
msgid ""
msgstr ""
-"Project-Id-Version: PACKAGE VERSION\n"
+"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2018-03-02 09:36+0100\n"
-"PO-Revision-Date: 2012-07-11 15:52+0100\n"
+"POT-Creation-Date: 2020-04-01 14:18+0200\n"
+"PO-Revision-Date: 2020-04-01 14:19+0200\n"
"Last-Translator: Radek Czajka <radoslaw.czajka@nowoczesnapolska.org.pl>\n"
-"Language-Team: LANGUAGE <LL@li.org>\n"
-"Language: \n"
+"Language: pl\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 "
-"|| n%100>=20) ? 1 : 2)\n"
+"|| n%100>=20) ? 1 : 2);\n"
+"Language-Team: \n"
+"X-Generator: Poedit 2.2.4\n"
#: constants.py:10
msgid "Waiting"
msgid "Sending"
msgstr "Wysyłanie"
-#: models.py:23 models.py:49
+#: models.py:20 models.py:46
msgid "project"
msgstr "projekt"
-#: models.py:24
+#: models.py:21
msgid "projects"
msgstr "projekty"
-#: models.py:37
+#: models.py:34
msgid "source file"
msgstr "plik źródłowy"
-#: models.py:40
+#: models.py:37
msgid "title"
msgstr "tytuł"
-#: models.py:41
+#: models.py:38
msgid "part name"
msgstr "nazwa części"
-#: models.py:41
+#: models.py:38
msgid "eg. chapter in a novel"
msgstr "np. rozdział w powieści"
-#: models.py:43
+#: models.py:40
msgid "index"
msgstr "numer"
-#: models.py:44
+#: models.py:41
msgid "parts count"
msgstr "liczba części"
-#: models.py:45
+#: models.py:42
msgid "artist"
msgstr "lektor"
-#: models.py:46
+#: models.py:43
msgid "conductor"
msgstr "reżyser"
-#: models.py:47
+#: models.py:44
msgid "encoded by"
msgstr "przyg. techn."
-#: models.py:48
+#: models.py:45
msgid "date"
msgstr "data"
-#: models.py:50
+#: models.py:47
msgid "book url"
msgstr "URL książki"
-#: models.py:51
+#: models.py:48
msgid "translator"
msgstr "tłumacz"
-#: models.py:71
+#: models.py:74
msgid "audiobook"
msgstr "audiobook"
-#: models.py:72
+#: models.py:75
msgid "audiobooks"
msgstr "audiobooki"
-#: templates/base.html:7
-msgid "Audiobook repository"
-msgstr "Repozytorium audiobooków"
-
-#: templates/archive/base.html:5
+#: templates/archive/base.html:6
msgid "New"
msgstr "Nowe"
-#: templates/archive/base.html:6
+#: templates/archive/base.html:7
msgid "Unpublished"
msgstr "Nie opublikowane"
-#: templates/archive/base.html:7 templates/archive/file_managed.html:11
+#: templates/archive/base.html:8 templates/archive/file_managed.html:12
msgid "Publishing"
msgstr "Publikacja"
-#: templates/archive/base.html:8
+#: templates/archive/base.html:9
msgid "Published"
msgstr "Opublikowane"
-#: templates/archive/base.html:9
+#: templates/archive/base.html:10
msgid "Archive"
msgstr "Archiwum"
-#: templates/archive/base.html:11
+#: templates/archive/base.html:15
msgid "Projects"
msgstr "Projekty"
-#: templates/archive/base.html:14
+#: templates/archive/base.html:18
msgid "Logout"
msgstr "Wyloguj"
-#: templates/archive/base.html:16 templates/registration/login.html:9
+#: templates/archive/base.html:20 templates/registration/login.html:9
msgid "Login"
msgstr "Zaloguj"
-#: templates/archive/base.html:19
+#: templates/archive/base.html:23
msgid "Administration"
msgstr "Administracja"
-#: templates/archive/file_managed.html:15
+#: templates/archive/file_managed.html:16
msgid "Publishing pending"
msgstr "Czeka na publikację"
msgid "Cancel publishing"
msgstr "Anuluj publikację"
-#: templates/archive/file_managed.html:51
+#: templates/archive/file_managed.html:58
msgid "Publish"
msgstr "Opublikuj"
-#: templates/archive/file_managed.html:57
+#: templates/archive/file_managed.html:67
+#: templates/archive/file_managed.html:79
msgid "Convert without publishing"
msgstr "Konwertuj bez publikacji"
-#: templates/archive/file_managed.html:67
+#: templates/archive/file_managed.html:74
+msgid "Publish on YouTube"
+msgstr "Opublikuj na YouTube"
+
+#: templates/archive/file_managed.html:95
msgid "MP3 file"
msgstr "Plik MP3"
-#: templates/archive/file_managed.html:68
+#: templates/archive/file_managed.html:99
msgid "Download MP3 file."
msgstr "Pobierz plik MP3."
-#: templates/archive/file_managed.html:70
-#: templates/archive/file_managed.html:86
+#: templates/archive/file_managed.html:101
+#: templates/archive/file_managed.html:124
+#: templates/archive/file_managed.html:147
msgid "Published:"
msgstr "Opublikowano:"
-#: templates/archive/file_managed.html:75
-#: templates/archive/file_managed.html:91
+#: templates/archive/file_managed.html:106
+#: templates/archive/file_managed.html:129
+#: templates/archive/file_managed.html:152
msgid "Not published yet."
msgstr "Nie opublikowane."
-#: templates/archive/file_managed.html:78
+#: templates/archive/file_managed.html:109
msgid "MP3 file hasn't been generated yet."
msgstr "Plik MP3 nie został jeszcze wygenerowany."
-#: templates/archive/file_managed.html:83
+#: templates/archive/file_managed.html:118
msgid "Ogg Vorbis file"
msgstr "Plik Ogg Vorbis"
-#: templates/archive/file_managed.html:84
+#: templates/archive/file_managed.html:122
msgid "Download Ogg Vorbis file."
msgstr "Pobierz plik Ogg Vorbis."
-#: templates/archive/file_managed.html:94
+#: templates/archive/file_managed.html:132
msgid "Ogg Vorbis file hasn't been generated yet."
msgstr "Plik Ogg Vorbis nie został jeszcze wygenerowany."
-#: templates/archive/file_managed.html:105
-msgid "Update tags"
-msgstr "Uaktualnij tagi"
+#: templates/archive/file_managed.html:141
+msgid "YouTube"
+msgstr "YouTube"
-#: templates/archive/file_managed.html:117 templates/archive/file_new.html:19
-msgid "Commit"
-msgstr "Zatwierdź"
+#: templates/archive/file_managed.html:145
+msgid "Download YouTube file."
+msgstr "Pobierz plik dla YouTube."
-#: templates/archive/file_managed.html:128
+#: templates/archive/file_managed.html:155
+msgid "YouTube file hasn't been generated yet."
+msgstr "Plik dla YouTube nie został jeszcze wygenerowany."
+
+#: templates/archive/file_managed.html:174
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:130
+#: templates/archive/file_managed.html:176
msgid "Remove to archive"
msgstr "Usuń do archiwum"
-#: templates/archive/file_new.html:8
+#: templates/archive/file_managed.html:186
+msgid "Update tags"
+msgstr "Uaktualnij tagi"
+
+#: templates/archive/file_managed.html:193 templates/archive/file_new.html:25
+msgid "Commit"
+msgstr "Zatwierdź"
+
+#: templates/archive/file_new.html:13
msgid "Move to archive"
msgstr "Przenieś do archiwum"
-#: templates/archive/file_unmanaged.html:6
+#: templates/archive/file_unmanaged.html:7
msgid "File with same name already exists!"
msgstr "Plik o tej nazwie już istnieje!"
-#: templates/archive/file_unmanaged.html:28
+#: templates/archive/file_unmanaged.html:23
msgid "Move to new files"
msgstr "Przenieś do nowych plików"
msgid "Unpublished audiobooks"
msgstr "Nie opublikowane audiobooki"
+#: templates/base.html:12
+msgid "Audiobook repository"
+msgstr "Repozytorium audiobooków"
+
#~ msgid "arranger"
#~ msgstr "aranżer"
--- /dev/null
+# Generated by Django 3.0.4 on 2020-04-01 11:54
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('archive', '0002_auto_20200330_1446'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='audiobook',
+ name='youtube_published',
+ field=models.DateTimeField(editable=False, null=True),
+ ),
+ migrations.AddField(
+ model_name='audiobook',
+ name='youtube_published_tags',
+ field=models.TextField(editable=False, null=True),
+ ),
+ migrations.AddField(
+ model_name='audiobook',
+ name='youtube_status',
+ field=models.SmallIntegerField(choices=[(1, 'Waiting'), (2, 'Encoding'), (3, 'Tagging'), (4, 'Sending')], editable=False, null=True),
+ ),
+ migrations.AddField(
+ model_name='audiobook',
+ name='youtube_tags',
+ field=models.TextField(editable=False, null=True),
+ ),
+ migrations.AddField(
+ model_name='audiobook',
+ name='youtube_task',
+ field=models.CharField(editable=False, max_length=64, null=True),
+ ),
+ ]
--- /dev/null
+# Generated by Django 3.0.4 on 2020-04-01 13:17
+
+import archive.utils
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('archive', '0003_auto_20200401_1154'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='audiobook',
+ name='youtube_file',
+ field=models.FileField(editable=False, null=True, storage=archive.utils.OverwriteStorage(), upload_to='archive/final'),
+ ),
+ ]
--- /dev/null
+# Generated by Django 3.0.4 on 2020-04-06 11:35
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('archive', '0004_audiobook_youtube_file'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='audiobook',
+ name='youtube_id',
+ field=models.CharField(blank=True, default='', max_length=255),
+ ),
+ ]
ogg_published_tags = models.TextField(null=True, editable=False)
ogg_published = models.DateTimeField(null=True, editable=False)
+ youtube_status = models.SmallIntegerField(null=True, editable=False, choices=status.choices)
+ youtube_task = models.CharField(max_length=64, null=True, editable=False)
+ youtube_tags = models.TextField(null=True, editable=False)
+ youtube_file = models.FileField(null=True, upload_to='archive/final', storage=OverwriteStorage(), editable=False)
+ 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='')
+
class Meta:
verbose_name = _("audiobook")
verbose_name_plural = _("audiobooks")
@classmethod
def set_status(cls, aid, status):
Audiobook.objects.filter(pk=aid).update(
- **{'%s_status' % cls.ext: status})
+ **{'%s_status' % cls.prefix: status})
@staticmethod
def encode(in_path, out_path):
@classmethod
def set_tags(cls, audiobook, file_name):
- tags = getattr(audiobook, "get_%s_tags" % cls.ext)()['tags']
+ tags = getattr(audiobook, "get_%s_tags" % cls.prefix)()['tags']
if not tags.get('flac_sha1'):
tags['flac_sha1'] = audiobook.get_source_sha1()
audio = File(file_name)
@classmethod
def save(cls, audiobook, file_name):
- field = "%s_file" % cls.ext
+ field = "%s_file" % cls.prefix
getattr(audiobook, field).save(
"%d.%s" % (audiobook.pk, cls.ext),
ExistingFile(file_name),
@classmethod
def published(cls, aid):
kwargs = {
- "%s_published_tags" % cls.ext: F("%s_tags" % cls.ext),
- "%s_tags" % cls.ext: None,
- "%s_published" % cls.ext: datetime.now(),
- '%s_status' % cls.ext: None,
+ "%s_published_tags" % cls.prefix: F("%s_tags" % cls.prefix),
+ "%s_tags" % cls.prefix: None,
+ "%s_published" % cls.prefix: datetime.now(),
+ '%s_status' % cls.prefix: None,
}
Audiobook.objects.filter(pk=aid).update(**kwargs)
@classmethod
def put(cls, user, audiobook, path):
- tags = getattr(audiobook, "get_%s_tags" % cls.ext)()
+ tags = getattr(audiobook, "get_%s_tags" % cls.prefix)()
data = {
'book': tags['url'],
'type': cls.ext,
class Mp3Task(AudioFormatTask):
- ext = 'mp3'
+ prefix = ext = 'mp3'
# these shouldn't be staticmethods
def id3_text(tag, text):
class OggTask(AudioFormatTask):
- ext = 'ogg'
+ prefix = ext = 'ogg'
@staticmethod
def encode(in_path, out_path):
<h2>{% trans "Publishing" %}</h2>
</div>
<div class="card-body">
- {% if audiobook.mp3_status or audiobook.ogg_status %}
+ {% 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 %}
<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>
- {% 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 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">
+ <form method="post" action="{% url 'youtube_publish' audiobook.id %}">
+ {% csrf_token %}
+ <input class="btn btn-primary" type="submit" value="{% trans "Publish on YouTube" %}" />
+ </form>
+
+ <form class="mt-3" method="post" action="{% url 'youtube_convert' audiobook.id %}">
+ {% csrf_token %}
+ <input class="btn btn-secondary" type="submit" value="{% trans "Convert without publishing" %}" />
+ </form>
+
+ </div>
+ </div>
</td></tr>
</table>
{% endif %}
<div class="row">
- <div class="col-xl-6">
+ <div class="col-xl-4">
<div class="card mt-4">
<div class="card-header">
<h2>{% trans "MP3 file" %}</h2>
</div>
</div>
- <div class="col-xl-6">
+ <div class="col-xl-4">
<div class="card mt-4">
<div class="card-header">
<h2>{% trans "Ogg Vorbis file" %}</h2>
</div>
</div>
+ <div class="col-xl-4">
+ <div class="card mt-4">
+ <div class="card-header">
+ <h2>{% trans "YouTube" %}</h2>
+ </div>
+ <div class="card-body">
+ {% if audiobook.youtube_file %}
+ <p><a href="{% url 'download' audiobook.id 'mkv' %}">{% trans "Download YouTube file." %}</a></p>
+ {% if audiobook.youtube_published %}
+ <p>{% trans "Published:" %} {{ audiobook.youtube_published }}</a></p>
+ {% if audiobook.get_youtube_published_tags.tags %}
+ {% tags_table audiobook.get_youtube_published_tags.tags %}
+ {% endif %}
+ {% else %}
+ <p>{% trans "Not published yet." %}</p>
+ {% endif %}
+ {% else %}
+ <p>{% trans "YouTube file hasn't been generated yet." %}</p>
+ {% endif %}
+ </div>
+ </div>
+ </div>
+
<div class="col-xl-6">
<div class="card mt-4">
<div class="card-header">
url(r'^publish/(\d+)/$', views.publish, name="publish"),
url(r'^convert/(\d+)/$', views.publish, {'publish': False}, name="convert"),
url(r'^download/(\d+)/$', views.download, name="download"),
- url(r'^download/(\d+)\.(mp3|ogg)$', views.download, name="download"),
+ 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"),
from hashlib import sha1
import os
import os.path
+import subprocess
from django.core.files.storage import FileSystemStorage
from django.core.files.uploadedfile import UploadedFile
class ExistingFile(UploadedFile):
-
def __init__(self, path, *args, **kwargs):
self.path = path
return super(ExistingFile, self).__init__(*args, **kwargs)
class OverwriteStorage(FileSystemStorage):
-
def _save(self, name, content):
if self.exists(name):
self.delete(name)
def sha1_file(f):
sha = sha1()
- for piece in iter(lambda: f.read(1024*1024), b''):
+ for piece in iter(lambda: f.read(1024 * 1024), b""):
sha.update(piece)
return sha.hexdigest()
root_len = len(root_path)
for path, dirs, files in os.walk(root_path):
for fname in files:
- yield os.path.join(path, fname)[root_len:].lstrip('/')
-
+ yield os.path.join(path, fname)[root_len:].lstrip("/")
# TODO: cancel tasks
audiobook.mp3_status = None
audiobook.ogg_status = None
+ audiobook.youtube_status = None
audiobook.save()
return redirect(file_managed, aid)
def download(request, aid, which="source"):
- if which not in ("source", "mp3", "ogg"):
+ if which not in ("source", "mp3", "ogg", 'mkv'):
raise Http404
audiobook = get_object_or_404(models.Audiobook, id=aid)
- file_ = getattr(audiobook, "%s_file" % which)
+ field = which
+ if which == 'mkv':
+ field = 'youtube'
+ file_ = getattr(audiobook, "%s_file" % field)
if not file_:
raise Http404
ext = file_.path.rsplit('.', 1)[-1]
response['Content-Disposition'] = "attachment; filename*=UTF-8''%s.%s" % (
quote(audiobook.title.encode('utf-8'), safe=''), ext)
- response['X-Sendfile'] = file_.path.encode('utf-8')
+ with open(file_.path, 'rb') as f:
+ response.write(f.read())
+ #response['X-Sendfile'] = file_.path.encode('utf-8')
return response
'apiclient',
'archive',
+ 'youtube',
)
# A sample logging configuration. The only tangible logging
urlpatterns = [
url(r'^$', RedirectView.as_view(url='archive/', permanent=False)),
url(r'^archive/', include('archive.urls')),
+ url(r'^youtube/', include('youtube.urls')),
url(r'^publish/', include('apiclient.urls')),
url(r'^admin/', admin.site.urls),
--- /dev/null
+from django.contrib import admin
+from . import models
+
+
+admin.site.register(models.YouTube)
--- /dev/null
+from django.apps import AppConfig
+
+
+class YoutubeConfig(AppConfig):
+ name = 'youtube'
--- /dev/null
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: \n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2020-04-01 14:18+0200\n"
+"PO-Revision-Date: 2020-04-01 14:18+0200\n"
+"Language: pl\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n"
+"%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n"
+"%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n"
+"Last-Translator: \n"
+"Language-Team: \n"
+"X-Generator: Poedit 2.2.4\n"
+
+#: models.py:23
+msgid "YouTube configuration"
+msgstr "konfiguracja YouTube"
+
+#: models.py:24
+msgid "YouTube configurations"
+msgstr "konfiguracje YouTube"
--- /dev/null
+# Generated by Django 3.0.4 on 2020-03-30 17:58
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='YouTube',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('client_secrets', models.TextField(blank=True)),
+ ('oauth2_storage', models.TextField(blank=True, editable=False)),
+ ('title_template', models.CharField(blank=True, max_length=1024)),
+ ('description_template', models.TextField(blank=True)),
+ ('category', models.IntegerField(null=True)),
+ ('intro_card', models.FileField(blank=True, upload_to='')),
+ ('intro_card_length', models.FloatField(null=True)),
+ ('card', models.FileField(blank=True, upload_to='')),
+ ('loop_video', models.FileField(blank=True, upload_to='')),
+ ('outro_card', models.FileField(blank=True, upload_to='')),
+ ('outro_card_length', models.FloatField(null=True)),
+ ],
+ options={
+ 'verbose_name': 'YouTube configuration',
+ 'verbose_name_plural': 'YouTube configurations',
+ },
+ ),
+ ]
--- /dev/null
+# Generated by Django 3.0.4 on 2020-03-30 18:00
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('youtube', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='youtube',
+ name='category',
+ field=models.IntegerField(blank=True, null=True),
+ ),
+ migrations.AlterField(
+ model_name='youtube',
+ name='intro_card_length',
+ field=models.FloatField(blank=True, null=True),
+ ),
+ migrations.AlterField(
+ model_name='youtube',
+ name='outro_card_length',
+ field=models.FloatField(blank=True, null=True),
+ ),
+ ]
--- /dev/null
+# Generated by Django 3.0.4 on 2020-03-31 14:04
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('youtube', '0002_auto_20200330_1800'),
+ ]
+
+ operations = [
+ migrations.RenameField(
+ model_name='youtube',
+ old_name='intro_card_length',
+ new_name='intro_card_duration',
+ ),
+ migrations.RenameField(
+ model_name='youtube',
+ old_name='outro_card_length',
+ new_name='outro_card_duration',
+ ),
+ ]
--- /dev/null
+from os import unlink
+from tempfile import NamedTemporaryFile
+from django.db import models
+from django.utils.translation import gettext_lazy as _
+from django.template import Template, Context
+from apiclient import youtube_call
+from .utils import (
+ video_from_image,
+ cut_video,
+ concat_videos,
+ get_duration,
+ get_framerate,
+ mux,
+)
+
+
+class YouTube(models.Model):
+ title_template = models.CharField(max_length=1024, blank=True)
+ description_template = models.TextField(blank=True)
+ category = models.IntegerField(null=True, blank=True) # get categories
+ intro_card = models.FileField(blank=True)
+ intro_card_duration = models.FloatField(null=True, blank=True)
+ card = models.FileField(blank=True)
+ loop_video = models.FileField(blank=True)
+ outro_card = models.FileField(blank=True)
+ outro_card_duration = models.FloatField(null=True, blank=True)
+
+ class Meta:
+ verbose_name = _("YouTube configuration")
+ verbose_name_plural = _("YouTube configurations")
+
+ def publish(self, audiobook, path):
+ ctx = Context(dict(audiobook=audiobook))
+ description = Template(self.description_template).render(ctx)
+ title = Template(self.title_template).render(ctx)
+ privacy = 'private'
+
+ data = dict(
+ snippet=dict(
+ title=title,
+ description=description,
+ # tags=tags,
+ # categoryId=category,
+ # defaultLanguage
+ ),
+ status=dict(
+ privacyStatus=privacy,
+ # license
+ # selfDeclaredMadeForKids
+ ),
+ # recordingDetails=dict(
+ # recordingDate
+ # ),
+ )
+ part = ",".join(data.keys())
+
+ with open(path, "rb") as f:
+ response = youtube_call(
+ "POST",
+ "https://www.googleapis.com/upload/youtube/v3/videos",
+ params={'part': part},
+ data=data,
+ media_data=f.read(),
+ )
+ data = response.json()
+ audiobook.youtube_id = data['id']
+ audiobook.save(update_fields=['youtube_id'])
+ return response
+
+ def prepare_file(self, input_path, output_path=None):
+ duration = get_duration(input_path)
+ video = self.prepare_video(duration)
+ output = mux([video, input_path], output_path=output_path)
+ unlink(video)
+ return output
+
+ def prepare_video(self, duration):
+ concat = []
+ delete = []
+
+ if self.loop_video:
+ fps = get_framerate(self.loop_video.path)
+ else:
+ fps = 25
+
+ loop_duration = duration
+ if self.intro_card and self.intro_card_duration:
+ loop_duration -= self.intro_card_duration
+ intro = video_from_image(
+ self.intro_card.path, self.intro_card_duration, fps=fps
+ )
+ concat.append(intro)
+ delete.append(intro)
+
+ if self.outro_card and self.outro_card_duration:
+ loop_duration -= self.outro_card_duration
+ outro = video_from_image(
+ self.outro_card.path, self.outro_card_duration, fps=fps
+ )
+ concat.append(outro)
+ delete.append(outro)
+
+ if self.loop_video:
+ loop_video_duration = get_duration(self.loop_video.path)
+ times_loop = int(loop_duration // loop_video_duration)
+
+ leftover_duration = loop_duration % loop_video_duration
+ leftover = cut_video(self.loop_video.path, leftover_duration)
+ concat[1:1] = [self.loop_video.path] * times_loop + [leftover]
+ delete.append(leftover)
+ else:
+ leftover = video_from_image(self.card.path, loop_duration)
+ concat.insert(1, video_from_image(self.card.path, loop_duration, fps=fps))
+ delete.append(leftover)
+
+ output = concat_videos(concat)
+ for p in delete:
+ unlink(p)
+ return output
+
+ # tags
+ # license
+ # selfDeclaredMadeForKids
--- /dev/null
+from archive.tasks import AudioFormatTask
+from .models import YouTube
+
+
+class YouTubeTask(AudioFormatTask):
+ ext = 'mkv'
+ prefix = 'youtube'
+
+ def encode(self, in_path, out_path):
+ YouTube.objects.first().prepare_file(in_path, out_path)
+
+ def set_tags(self, audiobook, filename):
+ pass
+
+ def put(self, user, audiobook, filename):
+ YouTube.objects.first().publish(audiobook, filename)
--- /dev/null
+from django.test import TestCase
+
+# Create your tests here.
--- /dev/null
+from django.conf.urls import url
+from . import views
+
+urlpatterns = [
+ url(r'^publish/(\d+)/$', views.publish, name="youtube_publish"),
+ url(r'^convert/(\d+)/$', views.publish, {'publish': False}, name="youtube_convert"),
+]
--- /dev/null
+import subprocess
+from tempfile import NamedTemporaryFile
+
+
+def video_from_image(img_path, duration, fps=25):
+ tmp = NamedTemporaryFile(prefix='image', suffix='.mkv', delete=False)
+ tmp.close()
+ subprocess.run(
+ ['ffmpeg', '-y', '-framerate', f'1/{duration}', '-i', img_path, '-c:v', 'libx264', '-vf', f'fps={fps},format=yuv420p', tmp.name], check=True)
+ return tmp.name
+
+
+def cut_video(video_path, duration):
+ tmp = NamedTemporaryFile(prefix='cut', suffix='.mkv', delete=False)
+ tmp.close()
+ subprocess.run(
+ ['ffmpeg', '-y', '-i', video_path, '-t', str(duration), tmp.name], check=True)
+ return tmp.name
+
+
+def concat_videos(paths):
+ filelist = NamedTemporaryFile(prefix='concat', suffix='.txt')
+ for path in paths:
+ filelist.write(f"file '{path}'\n".encode('utf-8'))
+ filelist.flush()
+
+ output = NamedTemporaryFile(prefix='concat', suffix='.mkv', delete=False)
+ output.close()
+
+ subprocess.run(
+ ['ffmpeg', '-y', '-safe', '0', '-f', 'concat', '-i', filelist.name, '-c', 'copy', output.name],
+ check=True)
+
+ filelist.close()
+ return output.name
+
+
+def mux(channels, output_path=None):
+ if not output_path:
+ output = NamedTemporaryFile(prefix='concat', suffix='.mkv', delete=False)
+ output.close()
+ output_path = output.name
+ args = ['ffmpeg']
+ for c in channels:
+ args.extend(['-i', c])
+ args.extend(['-shortest', '-y', output_path])
+ subprocess.run(args, check=True)
+ return output_path
+
+
+def get_duration(path):
+ return float(
+ subprocess.run(
+ [
+ "ffprobe",
+ "-i",
+ path,
+ "-show_entries",
+ "format=duration",
+ "-v",
+ "quiet",
+ "-of",
+ "csv=p=0",
+ ],
+ capture_output=True,
+ text=True,
+ check=True,
+ ).stdout
+ )
+
+
+def get_framerate(path):
+ rates = subprocess.run(
+ [
+ "ffprobe",
+ "-i",
+ path,
+ "-show_entries",
+ "stream=r_frame_rate",
+ "-v",
+ "quiet",
+ "-of",
+ "csv=p=0",
+ ],
+ capture_output=True,
+ text=True,
+ check=True,
+ ).stdout.strip().split('\n')
+ for rate in rates:
+ a, b = rate.split('/')
+ if b == '1':
+ return int(a)
--- /dev/null
+from django.contrib.auth.decorators import permission_required
+from django.shortcuts import redirect, get_object_or_404
+from django.urls import reverse
+from django.views.decorators.http import require_POST
+from archive.constants import status
+from archive.models import Audiobook
+from . import tasks
+
+
+
+@require_POST
+@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'])
+ return redirect(reverse('file', args=[aid]))