2026-03-19 13:39:59 +01:00
|
|
|
from django.contrib.auth.models import User
|
|
|
|
|
from django.db import models
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class PodcastFeed(models.Model):
|
Podcasts: Einstellungen pro Podcast auf dem Server, Regeln für neue Folgen, Warteschlange sortiert halten
- PodcastFeed (Migration 0007_feed_settings): playback_speed (null = Tempo
des Geräts), skip_intro, skip_outro, volume (0.2/0.5/1.0/1.5/2.0/2.5),
new_episodes (inbox/queue/ignore), include_words, exclude_words,
min_duration. Abos mit auto_queue bekommen new_episodes='queue';
auto_queue bleibt gleich (new_episodes == 'queue'), auch über
set-auto-queue/ und beim Speichern im Code.
- POST feeds/<id>/settings/: beliebige Teilmenge, prüft jeden Wert (400 mit
Feldname, dann wird nichts geändert), nur eigene Abos (sonst 404);
Antwort sind alle Einstellungen des Abos. feeds/ und state/ liefern sie
für jedes Abo mit.
- Neue Folgen beim Aktualisieren: was am Filter scheitert (keins der
Wörter, ein Ausschlusswort im Titel, kürzer als die Mindestlänge) und
bei 'ignore' alles verlässt sofort „Neu“ (nicht gehört); 'queue' reiht
an der auto_queue_position ein; 'inbox' wie bisher. Beim ersten Abruf
zählt weiter nur die neueste Folge.
- PodcastSettings.keep_sorted ('', new, old, short, long, feed): danach
wird die ganze Warteschlange nach jedem Einreihen, jedem Umsortieren und
beim Einschalten neu sortiert; settings/ und state/ liefern den Wert.
- Tests für alles davon.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 09:47:49 +02:00
|
|
|
# What happens to a new episode of this feed (one that passed the filter).
|
|
|
|
|
NEW_INBOX = 'inbox'
|
|
|
|
|
NEW_QUEUE = 'queue'
|
|
|
|
|
NEW_IGNORE = 'ignore'
|
|
|
|
|
NEW_EPISODE_ACTIONS = [
|
|
|
|
|
(NEW_INBOX, 'In „Neu“'),
|
|
|
|
|
(NEW_QUEUE, 'In die Warteschlange'),
|
|
|
|
|
(NEW_IGNORE, 'Nicht anzeigen'),
|
|
|
|
|
]
|
|
|
|
|
# Volume factors: below 1 quieter, above 1 louder (the apps boost those).
|
|
|
|
|
VOLUMES = (0.2, 0.5, 1.0, 1.5, 2.0, 2.5)
|
|
|
|
|
|
2026-03-19 13:39:59 +01:00
|
|
|
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='podcast_feeds')
|
|
|
|
|
rss_url = models.URLField(max_length=1000)
|
|
|
|
|
title = models.CharField(max_length=300)
|
|
|
|
|
description = models.TextField(blank=True)
|
|
|
|
|
artwork_url = models.URLField(max_length=1000, blank=True)
|
|
|
|
|
author = models.CharField(max_length=300, blank=True)
|
|
|
|
|
link = models.URLField(max_length=1000, blank=True)
|
|
|
|
|
last_refreshed_at = models.DateTimeField(null=True, blank=True)
|
|
|
|
|
added_at = models.DateTimeField(auto_now_add=True)
|
Podcasts: Einstellungen pro Podcast auf dem Server, Regeln für neue Folgen, Warteschlange sortiert halten
- PodcastFeed (Migration 0007_feed_settings): playback_speed (null = Tempo
des Geräts), skip_intro, skip_outro, volume (0.2/0.5/1.0/1.5/2.0/2.5),
new_episodes (inbox/queue/ignore), include_words, exclude_words,
min_duration. Abos mit auto_queue bekommen new_episodes='queue';
auto_queue bleibt gleich (new_episodes == 'queue'), auch über
set-auto-queue/ und beim Speichern im Code.
- POST feeds/<id>/settings/: beliebige Teilmenge, prüft jeden Wert (400 mit
Feldname, dann wird nichts geändert), nur eigene Abos (sonst 404);
Antwort sind alle Einstellungen des Abos. feeds/ und state/ liefern sie
für jedes Abo mit.
- Neue Folgen beim Aktualisieren: was am Filter scheitert (keins der
Wörter, ein Ausschlusswort im Titel, kürzer als die Mindestlänge) und
bei 'ignore' alles verlässt sofort „Neu“ (nicht gehört); 'queue' reiht
an der auto_queue_position ein; 'inbox' wie bisher. Beim ersten Abruf
zählt weiter nur die neueste Folge.
- PodcastSettings.keep_sorted ('', new, old, short, long, feed): danach
wird die ganze Warteschlange nach jedem Einreihen, jedem Umsortieren und
beim Einschalten neu sortiert; settings/ und state/ liefern den Wert.
- Tests für alles davon.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 09:47:49 +02:00
|
|
|
# Always the same as new_episodes == 'queue' (see save()); kept for the
|
|
|
|
|
# web UI's toggle and older clients.
|
Add podcast enhancements: AntennaPod parity features + inbox management
- Auto-play next episode from queue when current episode ends
- Sleep timer (N minutes or end-of-episode) with countdown in button
- In-feed episode filter (client-side search)
- Auto-queue new episodes per feed (⚡Q toggle, inserts at top of queue)
- More playback speeds: 1¾× and 2½× added
- Progress bars + structured meta line in all episode list views (feed, inbox, queue)
- Queue drag-and-drop reorder
- Feed list search filter and sort options (A–Z, Z–A, recently added/refreshed)
- DB migration: PodcastFeed.auto_queue, EpisodeProgress.dismissed
- Inbox: dismiss episodes without marking played, checkboxes for multi-select,
bulk actions (add to queue, mark played, download, dismiss), load-more pagination
- Refresh button in single feed view header
- Hourly background refresh of all subscribed feeds
- Full Media Session API for radio and podcast: Windows taskbar thumbnail buttons
(play/pause/stop/next/seek) now work correctly for both modes
- Playing an episode auto-adds it to the queue if not already there
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 08:55:11 +01:00
|
|
|
auto_queue = models.BooleanField(default=False)
|
Podcasts: Feeds bedingt abrufen, Fehler speichern, Umzüge folgen, Backoff
- Bedingter Abruf: ETag/Last-Modified werden nach einer 200 gespeichert und
als If-None-Match/If-Modified-Since mitgeschickt. Bei 304 wird nichts
geparst und keine Folge geschrieben, nur last_refreshed_at aktualisiert und
der Fehler gelöscht. Teilen sich mehrere Abos eine URL, gehen die Validatoren
nur mit, wenn alle schon einmal abgerufen wurden und dieselben haben.
- Fehler (Netz, HTTP >= 400, kein Feed) landen in last_error, last_error_at
und error_count; ein erfolgreicher Abruf setzt sie zurück. Feed-Liste und
/podcasts/state/ liefern die drei Felder mit.
- Dauerhafte Weiterleitungen (301/308 am Anfang der Kette) ändern rss_url,
außer der Nutzer hat die neue URL schon abonniert; 302/307 ändern nichts.
- Backoff im stündlichen Job: ab 3 Fehlern in Folge nur alle 2, 4, 8, 16,
höchstens 24 Stunden. „Alle aktualisieren“ und --feed-id versuchen es
trotzdem (max_age gilt weiter).
- _fetch_feed liefert jetzt ein FeedFetch-Objekt bzw. wirft FeedFetchError.
- Migration 0007_feed_http_cache.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 09:45:38 +02:00
|
|
|
# HTTP validators of the last full (200) response, sent back as
|
|
|
|
|
# If-None-Match / If-Modified-Since so an unchanged feed costs a 304.
|
|
|
|
|
http_etag = models.CharField(max_length=500, blank=True)
|
|
|
|
|
http_last_modified = models.CharField(max_length=100, blank=True)
|
|
|
|
|
# The last failed refresh (network, HTTP status, not a feed); cleared on
|
|
|
|
|
# success. error_count counts failures in a row and drives the backoff.
|
|
|
|
|
last_error = models.CharField(max_length=500, blank=True)
|
|
|
|
|
last_error_at = models.DateTimeField(null=True, blank=True)
|
|
|
|
|
error_count = models.PositiveIntegerField(default=0)
|
2026-03-19 13:39:59 +01:00
|
|
|
|
Podcasts: Einstellungen pro Podcast auf dem Server, Regeln für neue Folgen, Warteschlange sortiert halten
- PodcastFeed (Migration 0007_feed_settings): playback_speed (null = Tempo
des Geräts), skip_intro, skip_outro, volume (0.2/0.5/1.0/1.5/2.0/2.5),
new_episodes (inbox/queue/ignore), include_words, exclude_words,
min_duration. Abos mit auto_queue bekommen new_episodes='queue';
auto_queue bleibt gleich (new_episodes == 'queue'), auch über
set-auto-queue/ und beim Speichern im Code.
- POST feeds/<id>/settings/: beliebige Teilmenge, prüft jeden Wert (400 mit
Feldname, dann wird nichts geändert), nur eigene Abos (sonst 404);
Antwort sind alle Einstellungen des Abos. feeds/ und state/ liefern sie
für jedes Abo mit.
- Neue Folgen beim Aktualisieren: was am Filter scheitert (keins der
Wörter, ein Ausschlusswort im Titel, kürzer als die Mindestlänge) und
bei 'ignore' alles verlässt sofort „Neu“ (nicht gehört); 'queue' reiht
an der auto_queue_position ein; 'inbox' wie bisher. Beim ersten Abruf
zählt weiter nur die neueste Folge.
- PodcastSettings.keep_sorted ('', new, old, short, long, feed): danach
wird die ganze Warteschlange nach jedem Einreihen, jedem Umsortieren und
beim Einschalten neu sortiert; settings/ und state/ liefern den Wert.
- Tests für alles davon.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 09:47:49 +02:00
|
|
|
# Playback, for every device: null = the device's own speed.
|
|
|
|
|
playback_speed = models.FloatField(null=True, blank=True)
|
|
|
|
|
skip_intro = models.PositiveIntegerField(default=0) # seconds
|
|
|
|
|
skip_outro = models.PositiveIntegerField(default=0) # seconds
|
|
|
|
|
volume = models.FloatField(default=1.0) # one of VOLUMES
|
|
|
|
|
# New episodes: what happens to them, and which ones count at all.
|
|
|
|
|
new_episodes = models.CharField(max_length=10, choices=NEW_EPISODE_ACTIONS, default=NEW_INBOX)
|
|
|
|
|
# Comma-separated words, case-insensitive, matched against the title.
|
|
|
|
|
include_words = models.CharField(max_length=500, blank=True)
|
|
|
|
|
exclude_words = models.CharField(max_length=500, blank=True)
|
|
|
|
|
min_duration = models.PositiveIntegerField(default=0) # minutes, 0 = any
|
AntennaPod-Import und automatischer Download pro Podcast
- Neuer Befehl `manage.py import_antennapod <backup.db> --user <name> [--dry-run]
[--global-auto-download on|off] [--backup-date ...]`: übernimmt aus einer
AntennaPod-Datenbanksicherung die Abos (Abgleich über normalisierte URL,
fehlende werden angelegt, lokale übersprungen), die Podcast-Einstellungen
(Tempo, Intro/Outro, Lautstärke, Filter mit Phrasen in Anführungszeichen,
Mindestlänge, neue Folgen, „nicht aktualisieren“ -> ignorieren, automatischer
Download), den Zustand jeder Folge mit Zustand (gehört, neu, Position,
Favorit, Zeitpunkt), die Warteschlange (AntennaPods Reihenfolge zuerst) und
nimmt bekannte, dort nicht neue Folgen aus „Neu“. Neuere Änderungen in diora
bleiben, Favoriten werden vereinigt. Alles in einer Transaktion; ein zweiter
Lauf ändert nichts mehr.
- PodcastFeed.auto_download (Einstellung pro Podcast, in feeds/, state/ und
feeds/<id>/settings/) und PodcastSettings.auto_download_default (in settings/
und state/), mit dem neue Abos (Hinzufügen, OPML, gpodder, Import) starten.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 11:41:04 +02:00
|
|
|
# Devices download this feed's new episodes (those in the inbox) on
|
|
|
|
|
# their own, for listening offline.
|
|
|
|
|
auto_download = models.BooleanField(default=False)
|
Podcasts: Einstellungen pro Podcast auf dem Server, Regeln für neue Folgen, Warteschlange sortiert halten
- PodcastFeed (Migration 0007_feed_settings): playback_speed (null = Tempo
des Geräts), skip_intro, skip_outro, volume (0.2/0.5/1.0/1.5/2.0/2.5),
new_episodes (inbox/queue/ignore), include_words, exclude_words,
min_duration. Abos mit auto_queue bekommen new_episodes='queue';
auto_queue bleibt gleich (new_episodes == 'queue'), auch über
set-auto-queue/ und beim Speichern im Code.
- POST feeds/<id>/settings/: beliebige Teilmenge, prüft jeden Wert (400 mit
Feldname, dann wird nichts geändert), nur eigene Abos (sonst 404);
Antwort sind alle Einstellungen des Abos. feeds/ und state/ liefern sie
für jedes Abo mit.
- Neue Folgen beim Aktualisieren: was am Filter scheitert (keins der
Wörter, ein Ausschlusswort im Titel, kürzer als die Mindestlänge) und
bei 'ignore' alles verlässt sofort „Neu“ (nicht gehört); 'queue' reiht
an der auto_queue_position ein; 'inbox' wie bisher. Beim ersten Abruf
zählt weiter nur die neueste Folge.
- PodcastSettings.keep_sorted ('', new, old, short, long, feed): danach
wird die ganze Warteschlange nach jedem Einreihen, jedem Umsortieren und
beim Einschalten neu sortiert; settings/ und state/ liefern den Wert.
- Tests für alles davon.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 09:47:49 +02:00
|
|
|
|
2026-03-19 13:39:59 +01:00
|
|
|
class Meta:
|
|
|
|
|
unique_together = ('user', 'rss_url')
|
|
|
|
|
ordering = ['title']
|
|
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
|
return self.title
|
|
|
|
|
|
Podcasts: Einstellungen pro Podcast auf dem Server, Regeln für neue Folgen, Warteschlange sortiert halten
- PodcastFeed (Migration 0007_feed_settings): playback_speed (null = Tempo
des Geräts), skip_intro, skip_outro, volume (0.2/0.5/1.0/1.5/2.0/2.5),
new_episodes (inbox/queue/ignore), include_words, exclude_words,
min_duration. Abos mit auto_queue bekommen new_episodes='queue';
auto_queue bleibt gleich (new_episodes == 'queue'), auch über
set-auto-queue/ und beim Speichern im Code.
- POST feeds/<id>/settings/: beliebige Teilmenge, prüft jeden Wert (400 mit
Feldname, dann wird nichts geändert), nur eigene Abos (sonst 404);
Antwort sind alle Einstellungen des Abos. feeds/ und state/ liefern sie
für jedes Abo mit.
- Neue Folgen beim Aktualisieren: was am Filter scheitert (keins der
Wörter, ein Ausschlusswort im Titel, kürzer als die Mindestlänge) und
bei 'ignore' alles verlässt sofort „Neu“ (nicht gehört); 'queue' reiht
an der auto_queue_position ein; 'inbox' wie bisher. Beim ersten Abruf
zählt weiter nur die neueste Folge.
- PodcastSettings.keep_sorted ('', new, old, short, long, feed): danach
wird die ganze Warteschlange nach jedem Einreihen, jedem Umsortieren und
beim Einschalten neu sortiert; settings/ und state/ liefern den Wert.
- Tests für alles davon.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 09:47:49 +02:00
|
|
|
@classmethod
|
|
|
|
|
def from_db(cls, *args, **kwargs):
|
|
|
|
|
obj = super().from_db(*args, **kwargs)
|
|
|
|
|
obj._remember_sync()
|
|
|
|
|
return obj
|
|
|
|
|
|
|
|
|
|
def refresh_from_db(self, *args, **kwargs):
|
|
|
|
|
super().refresh_from_db(*args, **kwargs)
|
|
|
|
|
self._remember_sync()
|
|
|
|
|
|
|
|
|
|
def _remember_sync(self):
|
|
|
|
|
self._synced = (self.__dict__.get('auto_queue'), self.__dict__.get('new_episodes'))
|
|
|
|
|
|
|
|
|
|
def _sync_auto_queue(self):
|
|
|
|
|
"""Keep auto_queue == (new_episodes == 'queue'). Whichever of the two
|
|
|
|
|
was changed since loading (or, for a new feed, set away from its
|
|
|
|
|
default) wins; new_episodes if both were."""
|
|
|
|
|
auto_before, new_before = getattr(self, '_synced', (False, self.NEW_INBOX))
|
|
|
|
|
if self.new_episodes != new_before or self.auto_queue == auto_before:
|
|
|
|
|
self.auto_queue = self.new_episodes == self.NEW_QUEUE
|
|
|
|
|
elif self.auto_queue:
|
|
|
|
|
self.new_episodes = self.NEW_QUEUE
|
|
|
|
|
elif self.new_episodes == self.NEW_QUEUE:
|
|
|
|
|
self.new_episodes = self.NEW_INBOX
|
|
|
|
|
|
|
|
|
|
def save(self, *args, **kwargs):
|
|
|
|
|
update_fields = kwargs.get('update_fields')
|
|
|
|
|
if update_fields is not None and not {'auto_queue', 'new_episodes'} & set(update_fields):
|
|
|
|
|
return super().save(*args, **kwargs)
|
|
|
|
|
self._sync_auto_queue()
|
|
|
|
|
if update_fields is not None:
|
|
|
|
|
kwargs['update_fields'] = set(update_fields) | {'auto_queue', 'new_episodes'}
|
|
|
|
|
super().save(*args, **kwargs)
|
|
|
|
|
self._remember_sync()
|
|
|
|
|
|
|
|
|
|
def matches_filter(self, title, duration_seconds):
|
|
|
|
|
"""Whether a new episode passes this feed's filter: an include word
|
|
|
|
|
(if any are given), no exclude word, and long enough (if known)."""
|
|
|
|
|
title = (title or '').casefold()
|
|
|
|
|
include = _words(self.include_words)
|
|
|
|
|
if include and not any(w in title for w in include):
|
|
|
|
|
return False
|
|
|
|
|
if any(w in title for w in _words(self.exclude_words)):
|
|
|
|
|
return False
|
|
|
|
|
if self.min_duration and duration_seconds and duration_seconds < self.min_duration * 60:
|
|
|
|
|
return False
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _words(raw):
|
|
|
|
|
return [w.strip().casefold() for w in (raw or '').split(',') if w.strip()]
|
|
|
|
|
|
2026-03-19 13:39:59 +01:00
|
|
|
|
|
|
|
|
class PodcastEpisode(models.Model):
|
|
|
|
|
feed = models.ForeignKey(PodcastFeed, on_delete=models.CASCADE, related_name='episodes')
|
|
|
|
|
guid = models.CharField(max_length=1000)
|
|
|
|
|
title = models.CharField(max_length=500)
|
|
|
|
|
description = models.TextField(blank=True)
|
|
|
|
|
audio_url = models.URLField(max_length=1000)
|
|
|
|
|
duration_seconds = models.IntegerField(default=0)
|
|
|
|
|
pub_date = models.DateTimeField(null=True, blank=True)
|
|
|
|
|
episode_number = models.IntegerField(null=True, blank=True)
|
|
|
|
|
season_number = models.IntegerField(null=True, blank=True)
|
|
|
|
|
artwork_url = models.URLField(max_length=1000, blank=True)
|
|
|
|
|
discovered_at = models.DateTimeField(auto_now_add=True)
|
Podcasts: Kapitel, Shownotes und Transkripte für die Apps
- Feeds liefern jetzt Podlove-Kapitel (psc:chapters), Podcasting-2.0-Kapitel
(podcast:chapters, JSON-URL) und Transkripte (podcast:transcript, auch
mehrere); als Beschreibung wird content:encoded bevorzugt.
- Bekannte Folgen werden beim Aktualisieren nachgezogen, aber nur die Zeilen,
die sich wirklich geändert haben.
- Neuer Endpunkt GET /podcasts/episodes/<id>/ mit bereinigten Shownotes,
Zeitmarken, Kapiteln, Transkripten und Fortschritt. JSON-Kapitel werden
einmal serverseitig geholt (nur öffentliche Adressen, max. 1 MB) und an
der Folge gespeichert.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 07:56:44 +02:00
|
|
|
# Podcasting 2.0 <podcast:chapters url=…>: a JSON file, loaded on demand.
|
|
|
|
|
chapters_url = models.URLField(max_length=1000, blank=True)
|
|
|
|
|
# [{start: seconds, title, url?, img?}], from Podlove Simple Chapters in
|
Podcasts: Kapitel aus dem ID3-Tag der Audiodatei, Transkripte
- Ohne Kapitel im Feed (kein PSC, keine chapters_url oder dort nichts)
liest die Folgen-Detailansicht einmalig den ID3v2-Tag am Anfang der
Audiodatei (Range-Anfrage, höchstens 4 MB, Weiterleitungen mit
SSRF-Prüfung) und übernimmt dessen CHAP-Frames (v2.3/v2.4,
Unsynchronisation, erweiterter Header, Synchsafe-Größen, CTOC-Reihenfolge).
Eigener Parser nach der ID3-Spezifikation; Ergebnis wird wie die
JSON-Kapitel zwischengespeichert, vorübergehende Fehler nicht.
- Neu: GET /podcasts/episodes/<id>/transcript/ holt das beste Transkript
(JSON vor VTT vor SRT, Deutsch bzw. ohne Sprachangabe vor Englisch),
zerlegt es in Abschnitte {start, end, speaker, text}, fasst kurze
Schnipsel desselben Sprechers bis zum Satzende bzw. ~30 s zusammen und
speichert das Ergebnis (eigene Tabelle EpisodeTranscript). HTML: 404
{"error": "unsupported"}.
- Folgen-Detail liefert zusätzlich has_transcript.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 09:54:03 +02:00
|
|
|
# the feed, or once fetched: the JSON at chapters_url, else (or if that
|
|
|
|
|
# has none) the CHAP frames of the audio file's ID3v2 tag.
|
Podcasts: Kapitel, Shownotes und Transkripte für die Apps
- Feeds liefern jetzt Podlove-Kapitel (psc:chapters), Podcasting-2.0-Kapitel
(podcast:chapters, JSON-URL) und Transkripte (podcast:transcript, auch
mehrere); als Beschreibung wird content:encoded bevorzugt.
- Bekannte Folgen werden beim Aktualisieren nachgezogen, aber nur die Zeilen,
die sich wirklich geändert haben.
- Neuer Endpunkt GET /podcasts/episodes/<id>/ mit bereinigten Shownotes,
Zeitmarken, Kapiteln, Transkripten und Fortschritt. JSON-Kapitel werden
einmal serverseitig geholt (nur öffentliche Adressen, max. 1 MB) und an
der Folge gespeichert.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 07:56:44 +02:00
|
|
|
chapters = models.JSONField(default=list, blank=True)
|
Podcasts: Kapitel aus dem ID3-Tag der Audiodatei, Transkripte
- Ohne Kapitel im Feed (kein PSC, keine chapters_url oder dort nichts)
liest die Folgen-Detailansicht einmalig den ID3v2-Tag am Anfang der
Audiodatei (Range-Anfrage, höchstens 4 MB, Weiterleitungen mit
SSRF-Prüfung) und übernimmt dessen CHAP-Frames (v2.3/v2.4,
Unsynchronisation, erweiterter Header, Synchsafe-Größen, CTOC-Reihenfolge).
Eigener Parser nach der ID3-Spezifikation; Ergebnis wird wie die
JSON-Kapitel zwischengespeichert, vorübergehende Fehler nicht.
- Neu: GET /podcasts/episodes/<id>/transcript/ holt das beste Transkript
(JSON vor VTT vor SRT, Deutsch bzw. ohne Sprachangabe vor Englisch),
zerlegt es in Abschnitte {start, end, speaker, text}, fasst kurze
Schnipsel desselben Sprechers bis zum Satzende bzw. ~30 s zusammen und
speichert das Ergebnis (eigene Tabelle EpisodeTranscript). HTML: 404
{"error": "unsupported"}.
- Folgen-Detail liefert zusätzlich has_transcript.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 09:54:03 +02:00
|
|
|
# When chapters were fetched into `chapters` (from chapters_url or the
|
|
|
|
|
# audio file); null = not yet.
|
Podcasts: Kapitel, Shownotes und Transkripte für die Apps
- Feeds liefern jetzt Podlove-Kapitel (psc:chapters), Podcasting-2.0-Kapitel
(podcast:chapters, JSON-URL) und Transkripte (podcast:transcript, auch
mehrere); als Beschreibung wird content:encoded bevorzugt.
- Bekannte Folgen werden beim Aktualisieren nachgezogen, aber nur die Zeilen,
die sich wirklich geändert haben.
- Neuer Endpunkt GET /podcasts/episodes/<id>/ mit bereinigten Shownotes,
Zeitmarken, Kapiteln, Transkripten und Fortschritt. JSON-Kapitel werden
einmal serverseitig geholt (nur öffentliche Adressen, max. 1 MB) und an
der Folge gespeichert.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 07:56:44 +02:00
|
|
|
chapters_fetched_at = models.DateTimeField(null=True, blank=True)
|
|
|
|
|
# [{url, type, language}] from <podcast:transcript>.
|
|
|
|
|
transcripts = models.JSONField(default=list, blank=True)
|
2026-03-19 13:39:59 +01:00
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
|
unique_together = ('feed', 'guid')
|
|
|
|
|
ordering = ['-pub_date']
|
|
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
|
return self.title
|
|
|
|
|
|
|
|
|
|
|
Podcasts: Kapitel aus dem ID3-Tag der Audiodatei, Transkripte
- Ohne Kapitel im Feed (kein PSC, keine chapters_url oder dort nichts)
liest die Folgen-Detailansicht einmalig den ID3v2-Tag am Anfang der
Audiodatei (Range-Anfrage, höchstens 4 MB, Weiterleitungen mit
SSRF-Prüfung) und übernimmt dessen CHAP-Frames (v2.3/v2.4,
Unsynchronisation, erweiterter Header, Synchsafe-Größen, CTOC-Reihenfolge).
Eigener Parser nach der ID3-Spezifikation; Ergebnis wird wie die
JSON-Kapitel zwischengespeichert, vorübergehende Fehler nicht.
- Neu: GET /podcasts/episodes/<id>/transcript/ holt das beste Transkript
(JSON vor VTT vor SRT, Deutsch bzw. ohne Sprachangabe vor Englisch),
zerlegt es in Abschnitte {start, end, speaker, text}, fasst kurze
Schnipsel desselben Sprechers bis zum Satzende bzw. ~30 s zusammen und
speichert das Ergebnis (eigene Tabelle EpisodeTranscript). HTML: 404
{"error": "unsupported"}.
- Folgen-Detail liefert zusätzlich has_transcript.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 09:54:03 +02:00
|
|
|
class EpisodeTranscript(models.Model):
|
|
|
|
|
"""An episode's transcript, fetched and parsed once (transcripts.py).
|
|
|
|
|
|
|
|
|
|
A table of its own rather than fields on PodcastEpisode: a parsed
|
|
|
|
|
transcript runs to hundreds of KB, and episode lists load whole rows.
|
|
|
|
|
"""
|
|
|
|
|
episode = models.OneToOneField(PodcastEpisode, on_delete=models.CASCADE, related_name='transcript_cache')
|
|
|
|
|
# URLs of the transcripts it was made from, best first. When the feed
|
|
|
|
|
# offers different ones, the cache is stale.
|
|
|
|
|
sources = models.JSONField(default=list)
|
|
|
|
|
# {"segments": [...], "source": {...}}, or null with an error.
|
|
|
|
|
data = models.JSONField(null=True, blank=True)
|
|
|
|
|
# '' when data is there; else why there's none for good: 'unsupported'
|
|
|
|
|
# (no format with timing), 'invalid' (missing, broken, too large).
|
|
|
|
|
error = models.CharField(max_length=20, blank=True)
|
|
|
|
|
fetched_at = models.DateTimeField(auto_now=True)
|
|
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
|
return f'Transcript of {self.episode}'
|
|
|
|
|
|
|
|
|
|
|
2026-10-02 08:26:28 +02:00
|
|
|
# An episode counts as played this close to its end (AntennaPod's default),
|
|
|
|
|
# the same rule as the apps' "als gehört zählen".
|
|
|
|
|
PLAYED_BEFORE_END_SECONDS = 30
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def counts_as_played(position_seconds, duration_seconds):
|
|
|
|
|
return bool(duration_seconds) and duration_seconds > 0 and \
|
|
|
|
|
position_seconds >= max(duration_seconds - PLAYED_BEFORE_END_SECONDS, 1)
|
|
|
|
|
|
|
|
|
|
|
2026-03-19 13:39:59 +01:00
|
|
|
class EpisodeProgress(models.Model):
|
|
|
|
|
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='episode_progress')
|
|
|
|
|
episode = models.ForeignKey(PodcastEpisode, on_delete=models.CASCADE, related_name='progress')
|
|
|
|
|
position_seconds = models.IntegerField(default=0)
|
|
|
|
|
played = models.BooleanField(default=False)
|
Add podcast enhancements: AntennaPod parity features + inbox management
- Auto-play next episode from queue when current episode ends
- Sleep timer (N minutes or end-of-episode) with countdown in button
- In-feed episode filter (client-side search)
- Auto-queue new episodes per feed (⚡Q toggle, inserts at top of queue)
- More playback speeds: 1¾× and 2½× added
- Progress bars + structured meta line in all episode list views (feed, inbox, queue)
- Queue drag-and-drop reorder
- Feed list search filter and sort options (A–Z, Z–A, recently added/refreshed)
- DB migration: PodcastFeed.auto_queue, EpisodeProgress.dismissed
- Inbox: dismiss episodes without marking played, checkboxes for multi-select,
bulk actions (add to queue, mark played, download, dismiss), load-more pagination
- Refresh button in single feed view header
- Hourly background refresh of all subscribed feeds
- Full Media Session API for radio and podcast: Windows taskbar thumbnail buttons
(play/pause/stop/next/seek) now work correctly for both modes
- Playing an episode auto-adds it to the queue if not already there
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 08:55:11 +01:00
|
|
|
dismissed = models.BooleanField(default=False)
|
Podcasts: Favoriten, Folgenliste mit Filtern und Verlauf, Hörstatistik, OPML-Export
- EpisodeProgress.favorite (Migration 0006) und POST episodes/<id>/favorite/
(setzen oder umschalten; ändert updated_at nicht, damit Verlauf und
Statistik nicht verfälscht werden)
- GET episodes/: alle Folgen aller Abos, Filter all/unplayed/in_progress/
played/favorites/history, feed_id, q, offset/limit/total; zwei Abfragen
pro Seite, ohne Shownotes
- Folgendetails liefern zusätzlich favorite und last_played_at
- GET stats/: Hörzeit gesamt und pro Podcast, optional nur die letzten N Tage
- GET feeds/export.opml: Abos als OPML 2.0 (lässt sich wieder importieren)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 08:12:27 +02:00
|
|
|
favorite = models.BooleanField(default=False)
|
2026-03-19 13:39:59 +01:00
|
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
|
unique_together = ('user', 'episode')
|
|
|
|
|
ordering = ['-updated_at']
|
|
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
|
return f'{self.user} - {self.episode}'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class PodcastQueue(models.Model):
|
|
|
|
|
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='podcast_queue')
|
|
|
|
|
episode = models.ForeignKey(PodcastEpisode, on_delete=models.CASCADE, related_name='queued_by')
|
|
|
|
|
position = models.PositiveIntegerField(default=0)
|
|
|
|
|
added_at = models.DateTimeField(auto_now_add=True)
|
|
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
|
unique_together = ('user', 'episode')
|
|
|
|
|
ordering = ['position']
|
|
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
|
return f'{self.user} queue pos {self.position}: {self.episode}'
|
Podcasts: Position in der Warteschlange einstellbar; Feeds nur noch auf dem Server holen (SW v51)
Warteschlange
- PodcastSettings: wohin neue Folgen kommen, getrennt für Hinzufügen von
Hand (auch beim Abspielen einer Folge außerhalb der Warteschlange) und
für automatisch eingereihte neue Folgen: ans Ende, an den Anfang oder
nach die laufende Folge. Voreinstellung wie bisher (Hand: Ende,
automatisch: Anfang). GET/POST /podcasts/settings/.
- queue/add nimmt auch mehrere Folgen (episode_ids), eine Position
(where) und die laufende Folge (current_episode_id); ohne Angabe gilt
die zuletzt angehörte, nicht fertige Folge als laufende.
- Positionen werden lückenlos neu durchnummeriert.
Feeds
- POST /podcasts/feeds/refresh-all/ aktualisiert alle Feeds des Nutzers
auf dem Server: Downloads parallel, jede Feed-Adresse nur einmal (auch
über Nutzer hinweg), mit Timeout; kürzlich aktualisierte werden
übersprungen. Der stündliche Cron-Job nutzt dasselbe.
- GET /podcasts/state/ liefert Abos, Warteschlange, Inbox und
Einstellungen in einer Antwort (für die Apps).
- Web: „↻ All“ ist eine Anfrage statt einer pro Feed; die stündliche
Schleife im Browser lädt nur noch die Listen neu, statt jeden Feed
selbst anzustoßen.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 07:26:16 +02:00
|
|
|
|
|
|
|
|
class PodcastSettings(models.Model):
|
|
|
|
|
"""A user's podcast preferences. Where episodes go in the queue: when added
|
Podcasts: Einstellungen pro Podcast auf dem Server, Regeln für neue Folgen, Warteschlange sortiert halten
- PodcastFeed (Migration 0007_feed_settings): playback_speed (null = Tempo
des Geräts), skip_intro, skip_outro, volume (0.2/0.5/1.0/1.5/2.0/2.5),
new_episodes (inbox/queue/ignore), include_words, exclude_words,
min_duration. Abos mit auto_queue bekommen new_episodes='queue';
auto_queue bleibt gleich (new_episodes == 'queue'), auch über
set-auto-queue/ und beim Speichern im Code.
- POST feeds/<id>/settings/: beliebige Teilmenge, prüft jeden Wert (400 mit
Feldname, dann wird nichts geändert), nur eigene Abos (sonst 404);
Antwort sind alle Einstellungen des Abos. feeds/ und state/ liefern sie
für jedes Abo mit.
- Neue Folgen beim Aktualisieren: was am Filter scheitert (keins der
Wörter, ein Ausschlusswort im Titel, kürzer als die Mindestlänge) und
bei 'ignore' alles verlässt sofort „Neu“ (nicht gehört); 'queue' reiht
an der auto_queue_position ein; 'inbox' wie bisher. Beim ersten Abruf
zählt weiter nur die neueste Folge.
- PodcastSettings.keep_sorted ('', new, old, short, long, feed): danach
wird die ganze Warteschlange nach jedem Einreihen, jedem Umsortieren und
beim Einschalten neu sortiert; settings/ und state/ liefern den Wert.
- Tests für alles davon.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 09:47:49 +02:00
|
|
|
by hand (also when played from outside the queue), and when a feed that
|
|
|
|
|
queues its new episodes brings some; and whether the queue stays sorted."""
|
Podcasts: Position in der Warteschlange einstellbar; Feeds nur noch auf dem Server holen (SW v51)
Warteschlange
- PodcastSettings: wohin neue Folgen kommen, getrennt für Hinzufügen von
Hand (auch beim Abspielen einer Folge außerhalb der Warteschlange) und
für automatisch eingereihte neue Folgen: ans Ende, an den Anfang oder
nach die laufende Folge. Voreinstellung wie bisher (Hand: Ende,
automatisch: Anfang). GET/POST /podcasts/settings/.
- queue/add nimmt auch mehrere Folgen (episode_ids), eine Position
(where) und die laufende Folge (current_episode_id); ohne Angabe gilt
die zuletzt angehörte, nicht fertige Folge als laufende.
- Positionen werden lückenlos neu durchnummeriert.
Feeds
- POST /podcasts/feeds/refresh-all/ aktualisiert alle Feeds des Nutzers
auf dem Server: Downloads parallel, jede Feed-Adresse nur einmal (auch
über Nutzer hinweg), mit Timeout; kürzlich aktualisierte werden
übersprungen. Der stündliche Cron-Job nutzt dasselbe.
- GET /podcasts/state/ liefert Abos, Warteschlange, Inbox und
Einstellungen in einer Antwort (für die Apps).
- Web: „↻ All“ ist eine Anfrage statt einer pro Feed; die stündliche
Schleife im Browser lädt nur noch die Listen neu, statt jeden Feed
selbst anzustoßen.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 07:26:16 +02:00
|
|
|
BACK = 'back'
|
|
|
|
|
FRONT = 'front'
|
|
|
|
|
AFTER_CURRENT = 'after_current'
|
|
|
|
|
POSITIONS = [
|
|
|
|
|
(BACK, 'Ans Ende'),
|
|
|
|
|
(FRONT, 'An den Anfang'),
|
|
|
|
|
(AFTER_CURRENT, 'Nach der laufenden Folge'),
|
|
|
|
|
]
|
|
|
|
|
|
Podcasts: Einstellungen pro Podcast auf dem Server, Regeln für neue Folgen, Warteschlange sortiert halten
- PodcastFeed (Migration 0007_feed_settings): playback_speed (null = Tempo
des Geräts), skip_intro, skip_outro, volume (0.2/0.5/1.0/1.5/2.0/2.5),
new_episodes (inbox/queue/ignore), include_words, exclude_words,
min_duration. Abos mit auto_queue bekommen new_episodes='queue';
auto_queue bleibt gleich (new_episodes == 'queue'), auch über
set-auto-queue/ und beim Speichern im Code.
- POST feeds/<id>/settings/: beliebige Teilmenge, prüft jeden Wert (400 mit
Feldname, dann wird nichts geändert), nur eigene Abos (sonst 404);
Antwort sind alle Einstellungen des Abos. feeds/ und state/ liefern sie
für jedes Abo mit.
- Neue Folgen beim Aktualisieren: was am Filter scheitert (keins der
Wörter, ein Ausschlusswort im Titel, kürzer als die Mindestlänge) und
bei 'ignore' alles verlässt sofort „Neu“ (nicht gehört); 'queue' reiht
an der auto_queue_position ein; 'inbox' wie bisher. Beim ersten Abruf
zählt weiter nur die neueste Folge.
- PodcastSettings.keep_sorted ('', new, old, short, long, feed): danach
wird die ganze Warteschlange nach jedem Einreihen, jedem Umsortieren und
beim Einschalten neu sortiert; settings/ und state/ liefern den Wert.
- Tests für alles davon.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 09:47:49 +02:00
|
|
|
# Keep the queue sorted: after every change the whole queue is sorted
|
|
|
|
|
# again by this rule ('' = off, the user's own order).
|
|
|
|
|
SORT_ORDERS = [
|
|
|
|
|
('', 'Aus'),
|
|
|
|
|
('new', 'Neueste zuerst'),
|
|
|
|
|
('old', 'Älteste zuerst'),
|
|
|
|
|
('short', 'Kürzeste zuerst'),
|
|
|
|
|
('long', 'Längste zuerst'),
|
|
|
|
|
('feed', 'Nach Podcast'),
|
|
|
|
|
]
|
|
|
|
|
|
Podcasts: Position in der Warteschlange einstellbar; Feeds nur noch auf dem Server holen (SW v51)
Warteschlange
- PodcastSettings: wohin neue Folgen kommen, getrennt für Hinzufügen von
Hand (auch beim Abspielen einer Folge außerhalb der Warteschlange) und
für automatisch eingereihte neue Folgen: ans Ende, an den Anfang oder
nach die laufende Folge. Voreinstellung wie bisher (Hand: Ende,
automatisch: Anfang). GET/POST /podcasts/settings/.
- queue/add nimmt auch mehrere Folgen (episode_ids), eine Position
(where) und die laufende Folge (current_episode_id); ohne Angabe gilt
die zuletzt angehörte, nicht fertige Folge als laufende.
- Positionen werden lückenlos neu durchnummeriert.
Feeds
- POST /podcasts/feeds/refresh-all/ aktualisiert alle Feeds des Nutzers
auf dem Server: Downloads parallel, jede Feed-Adresse nur einmal (auch
über Nutzer hinweg), mit Timeout; kürzlich aktualisierte werden
übersprungen. Der stündliche Cron-Job nutzt dasselbe.
- GET /podcasts/state/ liefert Abos, Warteschlange, Inbox und
Einstellungen in einer Antwort (für die Apps).
- Web: „↻ All“ ist eine Anfrage statt einer pro Feed; die stündliche
Schleife im Browser lädt nur noch die Listen neu, statt jeden Feed
selbst anzustoßen.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 07:26:16 +02:00
|
|
|
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='podcast_settings')
|
|
|
|
|
enqueue_position = models.CharField(max_length=20, choices=POSITIONS, default=BACK)
|
|
|
|
|
# front = how auto-queue always worked before this setting existed
|
|
|
|
|
auto_queue_position = models.CharField(max_length=20, choices=POSITIONS, default=FRONT)
|
Podcasts: Einstellungen pro Podcast auf dem Server, Regeln für neue Folgen, Warteschlange sortiert halten
- PodcastFeed (Migration 0007_feed_settings): playback_speed (null = Tempo
des Geräts), skip_intro, skip_outro, volume (0.2/0.5/1.0/1.5/2.0/2.5),
new_episodes (inbox/queue/ignore), include_words, exclude_words,
min_duration. Abos mit auto_queue bekommen new_episodes='queue';
auto_queue bleibt gleich (new_episodes == 'queue'), auch über
set-auto-queue/ und beim Speichern im Code.
- POST feeds/<id>/settings/: beliebige Teilmenge, prüft jeden Wert (400 mit
Feldname, dann wird nichts geändert), nur eigene Abos (sonst 404);
Antwort sind alle Einstellungen des Abos. feeds/ und state/ liefern sie
für jedes Abo mit.
- Neue Folgen beim Aktualisieren: was am Filter scheitert (keins der
Wörter, ein Ausschlusswort im Titel, kürzer als die Mindestlänge) und
bei 'ignore' alles verlässt sofort „Neu“ (nicht gehört); 'queue' reiht
an der auto_queue_position ein; 'inbox' wie bisher. Beim ersten Abruf
zählt weiter nur die neueste Folge.
- PodcastSettings.keep_sorted ('', new, old, short, long, feed): danach
wird die ganze Warteschlange nach jedem Einreihen, jedem Umsortieren und
beim Einschalten neu sortiert; settings/ und state/ liefern den Wert.
- Tests für alles davon.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 09:47:49 +02:00
|
|
|
keep_sorted = models.CharField(max_length=10, choices=SORT_ORDERS, default='', blank=True)
|
AntennaPod-Import und automatischer Download pro Podcast
- Neuer Befehl `manage.py import_antennapod <backup.db> --user <name> [--dry-run]
[--global-auto-download on|off] [--backup-date ...]`: übernimmt aus einer
AntennaPod-Datenbanksicherung die Abos (Abgleich über normalisierte URL,
fehlende werden angelegt, lokale übersprungen), die Podcast-Einstellungen
(Tempo, Intro/Outro, Lautstärke, Filter mit Phrasen in Anführungszeichen,
Mindestlänge, neue Folgen, „nicht aktualisieren“ -> ignorieren, automatischer
Download), den Zustand jeder Folge mit Zustand (gehört, neu, Position,
Favorit, Zeitpunkt), die Warteschlange (AntennaPods Reihenfolge zuerst) und
nimmt bekannte, dort nicht neue Folgen aus „Neu“. Neuere Änderungen in diora
bleiben, Favoriten werden vereinigt. Alles in einer Transaktion; ein zweiter
Lauf ändert nichts mehr.
- PodcastFeed.auto_download (Einstellung pro Podcast, in feeds/, state/ und
feeds/<id>/settings/) und PodcastSettings.auto_download_default (in settings/
und state/), mit dem neue Abos (Hinzufügen, OPML, gpodder, Import) starten.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 11:41:04 +02:00
|
|
|
# What PodcastFeed.auto_download starts as for a new subscription.
|
|
|
|
|
auto_download_default = models.BooleanField(default=False)
|
2026-10-02 12:01:42 +02:00
|
|
|
# Episodes that were downloaded in AntennaPod when its backup was
|
|
|
|
|
# imported: [{episode_id, size (bytes, 0 = unknown)}]. The app offers once
|
|
|
|
|
# to download them again, then clears the list.
|
|
|
|
|
import_downloads = models.JSONField(default=list, blank=True)
|
Podcasts: Position in der Warteschlange einstellbar; Feeds nur noch auf dem Server holen (SW v51)
Warteschlange
- PodcastSettings: wohin neue Folgen kommen, getrennt für Hinzufügen von
Hand (auch beim Abspielen einer Folge außerhalb der Warteschlange) und
für automatisch eingereihte neue Folgen: ans Ende, an den Anfang oder
nach die laufende Folge. Voreinstellung wie bisher (Hand: Ende,
automatisch: Anfang). GET/POST /podcasts/settings/.
- queue/add nimmt auch mehrere Folgen (episode_ids), eine Position
(where) und die laufende Folge (current_episode_id); ohne Angabe gilt
die zuletzt angehörte, nicht fertige Folge als laufende.
- Positionen werden lückenlos neu durchnummeriert.
Feeds
- POST /podcasts/feeds/refresh-all/ aktualisiert alle Feeds des Nutzers
auf dem Server: Downloads parallel, jede Feed-Adresse nur einmal (auch
über Nutzer hinweg), mit Timeout; kürzlich aktualisierte werden
übersprungen. Der stündliche Cron-Job nutzt dasselbe.
- GET /podcasts/state/ liefert Abos, Warteschlange, Inbox und
Einstellungen in einer Antwort (für die Apps).
- Web: „↻ All“ ist eine Anfrage statt einer pro Feed; die stündliche
Schleife im Browser lädt nur noch die Listen neu, statt jeden Feed
selbst anzustoßen.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 07:26:16 +02:00
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def for_user(cls, user):
|
|
|
|
|
obj, _ = cls.objects.get_or_create(user=user)
|
|
|
|
|
return obj
|
|
|
|
|
|
AntennaPod-Import und automatischer Download pro Podcast
- Neuer Befehl `manage.py import_antennapod <backup.db> --user <name> [--dry-run]
[--global-auto-download on|off] [--backup-date ...]`: übernimmt aus einer
AntennaPod-Datenbanksicherung die Abos (Abgleich über normalisierte URL,
fehlende werden angelegt, lokale übersprungen), die Podcast-Einstellungen
(Tempo, Intro/Outro, Lautstärke, Filter mit Phrasen in Anführungszeichen,
Mindestlänge, neue Folgen, „nicht aktualisieren“ -> ignorieren, automatischer
Download), den Zustand jeder Folge mit Zustand (gehört, neu, Position,
Favorit, Zeitpunkt), die Warteschlange (AntennaPods Reihenfolge zuerst) und
nimmt bekannte, dort nicht neue Folgen aus „Neu“. Neuere Änderungen in diora
bleiben, Favoriten werden vereinigt. Alles in einer Transaktion; ein zweiter
Lauf ändert nichts mehr.
- PodcastFeed.auto_download (Einstellung pro Podcast, in feeds/, state/ und
feeds/<id>/settings/) und PodcastSettings.auto_download_default (in settings/
und state/), mit dem neue Abos (Hinzufügen, OPML, gpodder, Import) starten.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 11:41:04 +02:00
|
|
|
@classmethod
|
|
|
|
|
def new_feed_defaults(cls, user):
|
|
|
|
|
"""Field values a new PodcastFeed of the user starts with."""
|
|
|
|
|
auto_download = cls.objects.filter(user=user).values_list('auto_download_default', flat=True).first()
|
|
|
|
|
return {'auto_download': bool(auto_download)}
|
|
|
|
|
|
Podcasts: Position in der Warteschlange einstellbar; Feeds nur noch auf dem Server holen (SW v51)
Warteschlange
- PodcastSettings: wohin neue Folgen kommen, getrennt für Hinzufügen von
Hand (auch beim Abspielen einer Folge außerhalb der Warteschlange) und
für automatisch eingereihte neue Folgen: ans Ende, an den Anfang oder
nach die laufende Folge. Voreinstellung wie bisher (Hand: Ende,
automatisch: Anfang). GET/POST /podcasts/settings/.
- queue/add nimmt auch mehrere Folgen (episode_ids), eine Position
(where) und die laufende Folge (current_episode_id); ohne Angabe gilt
die zuletzt angehörte, nicht fertige Folge als laufende.
- Positionen werden lückenlos neu durchnummeriert.
Feeds
- POST /podcasts/feeds/refresh-all/ aktualisiert alle Feeds des Nutzers
auf dem Server: Downloads parallel, jede Feed-Adresse nur einmal (auch
über Nutzer hinweg), mit Timeout; kürzlich aktualisierte werden
übersprungen. Der stündliche Cron-Job nutzt dasselbe.
- GET /podcasts/state/ liefert Abos, Warteschlange, Inbox und
Einstellungen in einer Antwort (für die Apps).
- Web: „↻ All“ ist eine Anfrage statt einer pro Feed; die stündliche
Schleife im Browser lädt nur noch die Listen neu, statt jeden Feed
selbst anzustoßen.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 07:26:16 +02:00
|
|
|
def __str__(self):
|
|
|
|
|
return f'Podcast settings of {self.user}'
|