Compare commits

...

5 commits

Author SHA1 Message Date
256442abda finished docs, updated compose
All checks were successful
continuous-integration/drone/push Build is passing
2022-07-09 20:29:59 +02:00
17ece4401d Bump version: 0.4.5 → 0.4.6 2022-07-09 02:08:48 +02:00
7d6477425e write docs
improve docker compose
2022-07-09 02:08:39 +02:00
283e3d50c7 fix error deleting cache folder while running job 2022-07-08 22:38:58 +02:00
95cad578d0 dont count filtered videos as pending 2022-07-06 09:50:42 +02:00
8 changed files with 270 additions and 7 deletions

View file

@ -1,5 +1,5 @@
[bumpversion]
current_version = 0.4.5
current_version = 0.4.6
commit = True
tag = True

View file

@ -3,6 +3,7 @@ services:
ucast:
image: thetadev256/ucast
user: 1000:1000
restart: unless-stopped
ports:
- "8001:8001"
volumes:
@ -10,7 +11,11 @@ services:
environment:
UCAST_REDIS_URL: "redis://redis:6379"
UCAST_SECRET_KEY: "django-insecure-Es/+plApGxNBy8+ewB+74zMlmfV2H3whw6gu7i0ESwGrEWAUYRP3HM2EX0PLr3UJ"
UCAST_ALLOWED_HOSTS: ".localhost,127.0.0.1"
UCAST_N_WORKERS: 2
UCAST_TZ: "Europe/Berlin"
redis:
container_name: redis
image: redis:alpine
restart: unless-stopped

View file

@ -1,4 +1,245 @@
Einleitung
##########
Hello World
Bei den meisten YouTube-Videos, die ich mir anschaue, handelt es sich um
Nachrichten oder Kommentarvideos. Da diese Videos sehr textlastig sind,
spiele ich sie oft im Hintergrund ab und arbeite währenddessen an meinen Projekten.
Unterwegs habe ich aber keine Möglichkeit, YouTube-Videos im Hintergrund
abzuspielen, da die YouTube-App im Hintergrund die Wiedergabe unterbricht.
Es ist zwar möglich, YouTube-Videos mit entsprechenden Webdiensten herunterzuladen,
dies ist aber relativ unkomfortabel.
Deshalb höre ich unterwegs häufiger Podcasts, die mit entsprechenden Apps
(ich benutze AntennaPod) sowohl gestreamt als auch offline aufs Handy geladen werden
können.
Ich habe dann überlegt, ob es möglch wäre, YouTube-Kanäle automatisch in Podcasts
umzuwandeln. So kam ich auf die Idee, einen Server zu entwickeln,
der YouTube-Videos automatisch als MP3-Dateien herunterlädt und im Podcast-Format
bereitstellt. Auf diese Weise kann man sich die Audioinhalte von YouTube sowohl
am PC als auch unterwegs mit einer Podcast-App anhören.
Technik
#######
Webframework
************
Ich habe ucast mit dem Webframework Django entwickelt. Django hat den Vorteil,
das es grundlegende Funktionen von Webanwendungen wie ein Login-System bereits
implementiert hat. Dadurch konnte ich mich schneller auf die eigentlichen Features
meiner Anwendung konzentrieren.
YouTube-Downloading
*******************
Zum Herunterladen von Videos wird die Python-Library
`yt-dlp <https://github.com/yt-dlp/yt-dlp>`_ verwendet.
Diese Library kann Videos von YouTube und diversen anderen Videoplattformen
herunterladen und mithilfe von ffmpeg ins MP3-Format konvertieren.
Yt-dlp benötigt den Link oder die YouTube-ID eines Videos, um es herunterladen zu können.
Deswegen wird zusätzlich eine Möglichkeit benötigt, die aktuellen Videos eines
Kanals und dessen Metadaten (Profilbild, Beschreibung) abzurufen.
Hierfür gibt es zwei Möglichkeiten:
erstens Scraping der YouTube-Webseite und zweitens YouTube's eigene RSS-Feeds.
YouTube stellt für jeden Kanal einen RSS-Feed unter der Adresse
``https://www.youtube.com/feeds/videos.xml?channel_id=<Kanal-ID>`` bereit.
Der Feed listet allerdings nur die letzten 15 Videos eines Kanals auf.
Um ältere Videos sowie die Metadaten eines Kanals abrufen
zu können, muss die YouTube-Webseite aufgerufen und geparsed werden. Hierfür habe ich
die ``scrapetube``-Library als Grundlage verwendet und um eine Methode zum Abrufen
von Kanalinformationen erweitert.
Task-Queue
**********
Ucast muss regelmäßig die abonnierten Kanäle abrufen und Videos herunterladen.
Hier kommt eine `Task-Queue <https://python-rq.org>`_
zum Einsatz. Die Webanwendung kann neue Tasks in die
Queue einreihen, die dann im Hintergrund von Workern ausgeführt werden.
Mit einem Scheduler ist es auch möglich, periodisch (bspw. alle 15 Minuten)
Tasks auszuführen.
Die Queue benötigt eine Möglichkeit, Daten zwischen der Anwendung und den Workern
auszutauschen. Hier kommt eine Redis-Datenbank zum Einsatz.
Frontend
********
Da Ucast keine komplexen Funktionen auf der Clientseite bereitstellen muss,
wird das Frontend mithilfe von Django-Templates serverseitig gerendert und es
wurde auf ein Frontend-Framework verzichtet. Als CSS-Framework habe ich Bulma
verwendet, was eine Bibliothek von Komponenten bereitstellt. Bulma ist in Sass
geschrieben, wodurch es einfach an ein gewünschtes Designsthema angepasst werden kann.
Komplett auf Javascript verzichtet habe ich jedoch nicht.
Beispielsweise habe ich ``clipboard.js`` verwendet, um die Feed-URLs mit Klick auf einen
Button kopieren zu können.
Das endlose Scrolling auf den Videoseiten habe ich mit ``htmx`` umgesetzt, einer
JS-Library, mit der man dynamisch Webinhalte nachladen kann, ohne dafür eigenen
JS-Code zu schreiben.
Inbetriebnahme
##############
Docker-Compose
**************
Ucast ist als Docker-Image mit dem Namen
`thetadev256/ucast <https://hub.docker.com/r/thetadev256/ucast>`_ verfügbar.
Eine docker-compose-Datei mit einer Basiskonfiguration befindet sich im
Projektordner unter ``deploy/docker-compose.yml``. Um Ucast zu starten, müssen
die folgenden Befehle ausgeführt werden.
.. code-block:: sh
mkdir _run # Arbeitsverzeichnis erstellen
docker-compose -f deploy/docker-compose.yml up -d # Anwendung starten
docker exec -it ucast-ucast-1 ucast-manage createsuperuser # Benutzerkonto anlegen
Die Weboberfläche ist unter http://127.0.0.1:8001 erreichbar.
Konfiguration
*************
Die Konfiguration erfolgt durch Umgebungsvariablen. Alle Umgebungsvariablen
sind mit dem Präfix ``UCAST_`` zu versehen (z.B. ``UCAST_DEBUG``).
**DEBUG**
`Debug-Modus <https://docs.djangoproject.com/en/4.0/ref/settings/#debug>`_ von Django aktivieren.
Standard: ``false``
**ALLOWED_HOSTS**
Erlaubte `Hosts/Domains <https://docs.djangoproject.com/en/4.0/ref/settings/#allowed-hosts>`_.
Beispiel: ``"ucast.thetadev.de"``
**DB_ENGINE**
Verwendete Datenbanksoftware (``sqlite`` / ``mysql`` / ``postgresql``).
Standard: ``sqlite``
**DB_NAME**
Name der Datenbank. Standard: ``db``
**DB_HOST**
Adresse der Datenbank. Standard: ``127.0.0.1``
**DB_PORT**
Port der Datenbank. Standard: 3306 (mysql), 5432 (postgresql)
**DB_USER**, **DB_PASS**
Benutzername/Passwort für die Datenbank
**WORKDIR**
Hauptverzeichnis für Ucast (Siehe Verzeichnisstruktur).
Standard: aktuelles Arbeitsverzeichnis
**STATIC_ROOT**
Ordner für statische Dateien (``WORKDIR/static``)
**DOWNLOAD_ROOT**
Ordner für heruntergeladene Bilder und Audiodateien (``WORKDIR/data``)
**CACHE_ROOT**
Ordner für temporäre Dateien (``{WORKDIR}/cache``)
**DB_DIR**
Ordner für die SQLite-Datenbankdatei (``{WORKDIR}/db``)
**TZ**
Zeitzone. Standard: Systemeinstellung
**REDIS_URL**
Redis-Addresse. Standard: ``redis://localhost:6379``
**REDIS_QUEUE_TIMEOUT**
Timeout für gestartete Jobs [s]. Standard: 600
**REDIS_QUEUE_RESULT_TTL**
Speicherdauer für abgeschlossene Tasks [s]. Standard: 600
**YT_UPDATE_INTERVAL**
Zeitabstand, in dem die YouTube-Kanäle abgerufen werden [s].
Standard: 900
**FEED_MAX_ITEMS**
Maximale Anzahl Videos, die in den Feeds enthalten sind.
Standard: 50
**N_WORKERS**
Anzahl an Worker-Prozessen, die gestartet werden sollen
(nur im Docker-Container verfügbar).
Standard: 1
Verzeichnisstruktur
*******************
Ucast erstellt in seinem Arbeitsverzeichnis vier Unterordner, in denen die
Daten der Anwendung abgelegt werden.
.. code-block:: txt
- workdir
|_ cache Temporäre Dateien
|_ data Heruntergeladene Medien
|_ db SQLite-Datenbank
|_ static Statische Websitedaten
Bedienung
#########
Nach dem Login kommt man auf die Übersichtsseite, auf der alle abonnierten
Kanäle aufgelistet werden. Um einen neuen Kanal zu abonnieren, muss die YouTube-URL
(z.B. https://youtube.com/channel/UCGiJh0NZ52wRhYKYnuZI08Q)
in das Eingabefeld kopiert werden.
Wurde ein neuer Kanal hinzugefügt, beginnt ucast damit, die neuesten 15 Videos
herunterzuladen. Um zu überprüfen, welche Videos momentan heruntergeladen werden,
kann man auf die *Downloads*-Seite gehen. Auf dieser Seite werden auch fehlgeschlagene
Downloadtasks aufgelistet, die auch manuell wiederholt werden können (bspw. nach einem
Ausfall der Internetverbindung). Es gibt auch eine Suchfunktion, mit der man nach
einem Video mit einem bestimmten Titel suchen kann.
Um die abonnierten Kanäle zu seinem Podcast-Client hinzuzufügen, kann man die
Feed-URL auf der Übersichtsseite einfach kopieren und einfügen.
Die meisten Podcast-Clients bieten zudem eine Funktion zum Import von OPML-Dateien an.
In diesem Fall kann man einfach auf den Link *Download OPML* unten auf der Seite
klicken und die heruntergeladen Datei importieren. Auf diese Weise hat man schnell
alle abonnierten Kanäle zu seinem Podcast-Client hinzugefügt.
Fazit
#####
Ich betreibe Ucast seit einer Woche auf meiner NAS
und verwende es, um mir Videos sowohl am Rechner als auch unterwegs anzuhören.
In den ersten Tagen habe ich noch einige Bugs festgestellt, die beseitigt werden
mussten. Beispielsweise liegen nicht alle YouTube-Thumbnails im 16:9-Format vor,
weswegen sie zugeschnitten werden müssen, um das Layout der Webseite nicht zu
verschieben.
Am Anfang habe ich geplant, `SponsorBlock <https://sponsor.ajay.app>`_ in Ucast
zu integrieren, um Werbeinhalte aus den Videos zu entfernen. Yt-dlp hat dieses
Feature bereits integriert. Allerdings basiert Sponsorblock auf einer von der
Community verwalteten Datenbank, d.h. je nach Beliebtheit des Videos dauert es
zwischen einer halben und mehreren Stunden nach Release, bis Markierungen verfügbar
sind. Damit Sponsorblock zuverlässig funktioniert, müsste Ucast regelmäßig nach dem
Release des Videos die Datenbank abfragen und das Video bei Änderungen erneut
herunterladen und zuschneiden. Dies war mir zunächst zu komplex und ich habe mich
dazu entschieden, das Feature erst in Zukunft umzusetzen.
Ein weiteres Feature, das ich in Zukunft umsetzen werde,
ist die Unterstützung von alternativen Videoplattformen wie Peertube,
Odysee und Bitchute.

View file

@ -1,6 +1,6 @@
[tool.poetry]
name = "ucast"
version = "0.4.5"
version = "0.4.6"
description = "YouTube to Podcast converter"
authors = ["Theta-Dev <t.testboy@gmail.com>"]
packages = [

View file

@ -1,4 +1,4 @@
__version__ = "0.4.5"
__version__ = "0.4.6"
def template_context(request):

View file

@ -70,6 +70,16 @@ class Channel(models.Model):
"download_size__sum"
)
def vfilter_args(self) -> dict:
filter_args = {}
if self.skip_livestreams:
filter_args["is_livestream"] = False
if self.skip_shorts:
filter_args["is_short"] = False
return filter_args
def __str__(self):
return self.name

View file

@ -85,8 +85,12 @@ class Cache:
if dirname == "yt_dlp":
continue
ctime = os.path.getctime(dirname)
try:
ctime = os.path.getctime(dirname)
# Cache folders may get removed by concurrent jobs
except FileNotFoundError:
continue
age = datetime.now() - datetime.fromtimestamp(ctime)
if age > timedelta(days=1):
shutil.rmtree(self.dir_cache / dirname)
shutil.rmtree(self.dir_cache / dirname, ignore_errors=True)

View file

@ -92,7 +92,10 @@ def videos(request: http.HttpRequest, channel: str):
template_name = "ucast/videos_items.html"
n_pending = Video.objects.filter(
channel=chan, downloaded__isnull=True, is_deleted=False
channel=chan,
downloaded__isnull=True,
is_deleted=False,
**chan.vfilter_args(),
).count()
return render(