2026-03-19 13:39:59 +01:00
|
|
|
from django.core.management.base import BaseCommand
|
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
|
|
|
from django.utils import timezone
|
2026-03-19 13:39:59 +01:00
|
|
|
|
|
|
|
|
from podcasts.models import PodcastFeed
|
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
|
|
|
from podcasts.views import _in_backoff_q, _refresh_feeds
|
2026-03-19 13:39:59 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class Command(BaseCommand):
|
|
|
|
|
help = 'Refresh podcast feeds (fetch new episodes from RSS)'
|
|
|
|
|
|
|
|
|
|
def add_arguments(self, parser):
|
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
|
|
|
parser.add_argument('--feed-id', type=int, help='Refresh only this feed ID (even when backing off)')
|
2026-03-19 13:39:59 +01:00
|
|
|
parser.add_argument('--limit', type=int, default=0, help='Max number of feeds to refresh')
|
|
|
|
|
|
|
|
|
|
def handle(self, *args, **options):
|
|
|
|
|
qs = PodcastFeed.objects.all().order_by('last_refreshed_at')
|
|
|
|
|
|
|
|
|
|
if options['feed_id']:
|
|
|
|
|
qs = qs.filter(pk=options['feed_id'])
|
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
|
|
|
else:
|
|
|
|
|
# A feed failing 3+ times in a row is tried every 2, 4, 8, 16,
|
|
|
|
|
# at most 24 hours instead of every hour.
|
|
|
|
|
qs = qs.exclude(_in_backoff_q(timezone.now()))
|
2026-03-19 13:39:59 +01:00
|
|
|
|
|
|
|
|
if options['limit']:
|
|
|
|
|
qs = qs[: options['limit']]
|
|
|
|
|
|
|
|
|
|
if not qs.exists():
|
|
|
|
|
self.stdout.write('No feeds to refresh.')
|
|
|
|
|
return
|
|
|
|
|
|
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
|
|
|
# Parallel downloads, one per distinct feed URL (see _refresh_feeds).
|
|
|
|
|
refreshed, failed, new = _refresh_feeds(qs)
|
|
|
|
|
self.stdout.write(f'{refreshed} feeds refreshed, {failed} failed, +{new} episodes')
|