diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 2ee016f..cb7d16d 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.4.5 +current_version = 0.4.6 commit = True tag = True diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml index 415031e..24d889a 100644 --- a/deploy/docker-compose.yml +++ b/deploy/docker-compose.yml @@ -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 diff --git a/docs/src/0_intro.rst b/docs/src/0_intro.rst index 1fa61b5..471db95 100644 --- a/docs/src/0_intro.rst +++ b/docs/src/0_intro.rst @@ -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 `_ 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=`` 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 `_ +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 `_ 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 `_ von Django aktivieren. + Standard: ``false`` + +**ALLOWED_HOSTS** + Erlaubte `Hosts/Domains `_. + 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 `_ 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. diff --git a/pyproject.toml b/pyproject.toml index 15c1883..78dd21f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "ucast" -version = "0.4.5" +version = "0.4.6" description = "YouTube to Podcast converter" authors = ["Theta-Dev "] packages = [ diff --git a/ucast/__init__.py b/ucast/__init__.py index 367d911..b87db4b 100644 --- a/ucast/__init__.py +++ b/ucast/__init__.py @@ -1,4 +1,4 @@ -__version__ = "0.4.5" +__version__ = "0.4.6" def template_context(request): diff --git a/ucast/models.py b/ucast/models.py index 5c1097c..3075d9a 100644 --- a/ucast/models.py +++ b/ucast/models.py @@ -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 diff --git a/ucast/service/storage.py b/ucast/service/storage.py index e1b6af0..ed31aa7 100644 --- a/ucast/service/storage.py +++ b/ucast/service/storage.py @@ -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) diff --git a/ucast/views.py b/ucast/views.py index 0850a17..230748c 100644 --- a/ucast/views.py +++ b/ucast/views.py @@ -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(