Zum Hauptinhalt springen

YouTube Commands

Spezifische Commands für YouTube-Datensammlung, Video-Statistiken und Related Channel Discovery.


youtube:sync-video-stats

Nightly Delta-Sync für YouTube Video-Statistiken. Nur Profile mit aktiviertem Auto-Sync (YouTubeVideoSync.auto_sync_enabled = true) werden berücksichtigt.

youtube:sync-video-stats
{--profile-id= : Nur für eine bestimmte Social-Profile-ID}
{--max-profiles= : Maximale Anzahl Profile pro Durchlauf (überschreibt Config)}

Beispiele

# Alle Auto-Sync Profile synchronisieren
php artisan youtube:sync-video-stats

# Einzelnes Profil synchronisieren
php artisan youtube:sync-video-stats --profile-id=123

# Maximal 50 Profile pro Lauf (Quota-Schutz)
php artisan youtube:sync-video-stats --max-profiles=50

Optionen

OptionTypDefaultBeschreibung
--profile-id=intEinzelne Social-Profile-ID für One-Off Sync
--max-profiles=intConfig-WertMaximale Profile pro Lauf, überschreibt postbox.youtube_video_sync.max_profiles_per_run (Default: 0 = unbegrenzt)

Interne Schritte

  1. YouTubeVideoSync Records mit auto_sync_enabled = true laden
  2. Optional nach --profile-id filtern
  3. Bei --max-profiles > 0: Profile nach last_sync_at ASC sortieren und limitieren (älteste zuerst)
  4. Pro Profil einen SyncYouTubeVideoStats Job dispatchen
  5. Jobs landen auf der imports-youtube-video Queue
  6. Health-Marker in Cache setzen (health:youtube_video_sync_ran:{date}) mit Anzahl Jobs und Zeitstempel — wird von health:check-daily-scrapes und /up_system gelesen

Quota-Schutz via --max-profiles

Wenn --max-profiles gesetzt oder via Config konfiguriert ist, werden Profile nach last_sync_at (älteste zuerst) sortiert und limitiert. So rotieren alle Profile über mehrere Tage, ohne das tägliche YouTube API-Quota zu überlasten.

Schedule

Täglich um 14:00 UTC mit .withoutOverlapping(). Cron-Heartbeat: youtube_video_sync.

Location: app/Console/Commands/SyncYouTubeVideoStats.php


youtube:auto-activate-pro

Default-deaktiviert seit 2026-05-25 via YOUTUBE_PRO_AUTO_ACTIVATION_ENABLED=false. Code-Pfad bleibt, jederzeit reaktivierbar.

Aktiviert PRO automatisch für YouTube-Profile oberhalb einer Follower-Schwelle (Default 1 Mio.). Setzt pro_enabled=true, erstellt YouTubeVideoSync-Row mit auto_sync_enabled=true + video_tracking_start_date=today + requested_by=NULL (kennzeichnet auto-Aktivierung), dispatcht initialen SyncYouTubeVideoStats-Job + FindRelatedYouTubeChannels-Job.

youtube:auto-activate-pro
{--dry-run : Vorschau ohne Aenderungen}
{--min-followers= : Override Follower-Schwelle fuer diesen Run}

Beispiele

# Vorschau (zeigt "disabled" wenn ENV-Flag false):
php artisan youtube:auto-activate-pro --dry-run

# Mit anderer Schwelle (nur wenn ENV-Flag true):
php artisan youtube:auto-activate-pro --min-followers=500000

Schedule

Täglich um 01:00 UTC. Cron-Heartbeat: youtube_pro_auto_activate.

ENV-Konfiguration:

  • YOUTUBE_PRO_AUTO_ACTIVATION_ENABLED (default: true, Production: false seit 2026-05-25)
  • YOUTUBE_PRO_MIN_FOLLOWERS (default: 1000000)
  • YOUTUBE_PRO_MAX_NEW_PER_RUN (default: 500 — verhindert Kapazitäts-Schocks)

Location: app/Console/Commands/AutoActivateYouTubePro.php


youtube:repair-pro-sync

Heilt PRO-Profile (pro_enabled=true), denen der youtube_video_syncs.auto_sync_enabled-Flag fehlt oder auf false steht (z.B. nach Fail-Streak-Reset, Migration-Bug, manuellem Tinker ohne Sync-Schritt). Respektiert den Plan-67-Lock — Profile mit pro_auto_activation_blocked_at IS NOT NULL werden bewusst ignoriert. Seit 2026-05-25 aus youtube:auto-activate-pro (Phase B) als eigener Command extrahiert.

youtube:repair-pro-sync
{--dry-run : Vorschau ohne Aenderungen}

Pro-Treffer-Aktionen

  1. auto_sync_enabled = true auf youtube_video_syncs setzen
  2. video_tracking_start_date = today (falls null)
  3. requested_by = NULL (Repair-Marker)
  4. Initialer SyncYouTubeVideoStats-Job dispatchen (falls noch keine Videos da)
  5. FindRelatedYouTubeChannels-Job dispatchen (falls noch nicht recherchiert)

Schedule

Täglich um 01:30 UTC (zwischen Auto-Activate 01:00 und Sync 14:00). Cron-Heartbeat: youtube_pro_repair.

ENV-Konfiguration:

  • YOUTUBE_PRO_REPAIR_ENABLED (default: true)

Location: app/Console/Commands/RepairYouTubeProSync.php


youtube:auto-fill-related-channels

Dispatcht FindRelatedYouTubeChannels Jobs wenn Research-Quota verfügbar ist. Füllt Related Channels für YouTube-Profile automatisch auf.

youtube:auto-fill-related-channels
{--dry-run : Vorschau ohne Job-Dispatch}
{--force : Quota-Check ignorieren}

Beispiele

# Standard-Lauf (prüft Quota, dispatcht 1 Job)
php artisan youtube:auto-fill-related-channels

# Vorschau
php artisan youtube:auto-fill-related-channels --dry-run

# Quota-Check umgehen
php artisan youtube:auto-fill-related-channels --force

Optionen

OptionBeschreibung
--dry-runZeigt welches Profil gewählt würde, ohne zu dispatchen
--forceIgnoriert Quota-Prüfung und dispatcht den Job

Quota-Prüfung (Zwei-Stufen)

Stufe 1 — Dashboard-Quota: Der Command dispatcht nur wenn > 25% der Research-Quota verfügbar ist. Die Quota wird via ResearchQuotaService geprüft, der google_api_quota_usages ausliest (stündlich aktualisiert).

Stufe 2 — Live Redis-Check: Zusätzlich prüft YouTubeQuotaGuard::canMakeCall('research') in Echtzeit, ob mindestens ein API-Key nicht exhausted ist. Die Dashboard-Daten können bis zu 1 Stunde veraltet sein, der Redis-Cache wird bei jedem API-Call aktualisiert.

Exponential Backoff

Wenn ein Lauf wegen Quota übersprungen wird, aktiviert sich ein Backoff-Mechanismus:

Consecutive SkipsBackoff-DauerNächster Check frühestens
115 MinutenNormal: 5min → Effektiv: 15min
230 MinutenNormal: 5min → Effektiv: 30min
3+60 MinutenNormal: 5min → Effektiv: 60min

Nach einem erfolgreichen Dispatch wird der Backoff zurückgesetzt. Der Backoff-Counter verfällt nach 6 Stunden. --force umgeht sowohl Quota-Prüfung als auch Backoff.

Priority-Logik

PrioritätKriteriumSortierung
1Profile ohne Related Channels (related_channels_searched_at IS NULL)Followers DESC
2Profile mit stale Related Channels (> 30 Tage alt)related_channels_searched_at ASC

Interne Schritte

  1. Backoff prüfen: Falls im Cooldown, sofort abbrechen (außer --force)
  2. Quota Stufe 1: Research-Quota via ResearchQuotaService::getStatus() prüfen (forceRefresh)
  3. Quota Stufe 2: Live Redis-Check via YouTubeQuotaGuard::canMakeCall('research')
  4. Nächstes Profil nach Priority-Logik finden
  5. Profile mit Status running oder pending ausschließen (Duplikat-Schutz)
  6. related_channels_status = 'pending' setzen
  7. FindRelatedYouTubeChannels Job auf youtube-related-channels Queue dispatchen
  8. Backoff zurücksetzen nach erfolgreichem Dispatch

Schedule

Alle 5 Minuten mit .withoutOverlapping().

Location: app/Console/Commands/AutoFillRelatedYouTubeChannels.php


youtube-research:prune

Löscht YouTube Research Queries die älter als 30 Tage sind. Dies ist ein Schedule-Call (kein Artisan Command), definiert in routes/console.php.

youtube-research:prune

Beispiel

# Wird automatisch via Scheduler ausgeführt
# Manuelle Ausführung nicht direkt möglich (Schedule::call)

Interne Schritte

  1. YouTubeResearchQuery Records mit created_at < now()->subDays(30) selektieren
  2. Alle gefundenen Records löschen
  3. Research Queries entstehen durch FindRelatedYouTubeChannels Jobs

Retention

  • 30 Tage Aufbewahrung für Research Queries
  • Danach werden die Queries gelöscht, die gefundenen Related Channels bleiben erhalten

Schedule

Täglich um 01:30 UTC mit .withoutOverlapping().

Location: routes/console.php (Schedule::call)


youtube:calculate-video-scores

Berechnet tägliche Video Performance Scores (VPS) für YouTube-Videos. Tier-normalisierte Bewertung (0-100) basierend auf Subscriber-Zahlen des Channels.

youtube:calculate-video-scores
{--date= : Score für ein bestimmtes Datum berechnen (Y-m-d)}
{--from= : Start-Datum für Range-Berechnung (Y-m-d)}
{--to= : End-Datum für Range-Berechnung (Y-m-d)}
{--backfill : Scores für alle Daten mit vorhandenen Video-Metriken berechnen}
{--profile-id= : Nur Videos eines bestimmten Profils verarbeiten}
{--force : Bestehende Scores überschreiben}
{--dry-run : Vorschau ohne Speicherung}

Optionen

OptionTypDefaultBeschreibung
--date=stringScore für ein spezifisches Datum (Format: Y-m-d)
--from=stringStart-Datum für Range-Berechnung
--to=stringEnd-Datum für Range-Berechnung
--backfillflagAlle Daten mit vorhandenen Metriken abarbeiten
--profile-id=intNur Videos dieses Profils verarbeiten
--forceflagBestehende Scores überschreiben (Default: überspringen)
--dry-runflagNur Vorschau, keine Daten speichern

Beispiele

# Standard: Score für gestern berechnen
php artisan youtube:calculate-video-scores

# Score für bestimmtes Datum
php artisan youtube:calculate-video-scores --date=2026-02-10

# Range-Berechnung (z.B. nach Ausfall)
php artisan youtube:calculate-video-scores --from=2026-02-01 --to=2026-02-10

# Backfill für alle verfügbaren Daten
php artisan youtube:calculate-video-scores --backfill

# Nur Videos eines Profils, bestehende überschreiben
php artisan youtube:calculate-video-scores --profile-id=12345 --force

# Vorschau
php artisan youtube:calculate-video-scores --dry-run

Interne Logik

  1. Videos mit genügend Datenpunkten (MIN_DATA_POINTS aus VideoScoreCalculator) identifizieren
  2. Lookback-Window (METRICS_LOOKBACK_DAYS) für relevante Daily-Metriken
  3. Channel-Tier pro Profil cachen (verhindert wiederholte Follower-Lookups)
  4. Tier-normalisierter Score (0-100): Micro-Channels werden nicht unfair mit Mega-Channels verglichen
  5. Bestehende Scores werden übersprungen (es sei denn --force)

Schedule

Täglich um 21:00 UTC.

Location: app/Console/Commands/CalculateVideoScores.php


youtube:find-stuck-related

Findet YouTube-Profile mit hängengebliebenen Related-Channel-Suchen. Debugging-Tool wenn Jobs im Status running oder pending_imports steckenbleiben.

youtube:find-stuck-related
{--hours=1 : Ab wie vielen Stunden gilt ein Job als "stuck"}
{--reset : Stuck-Profile zurücksetzen für Retry}

Optionen

OptionTypDefaultBeschreibung
--hours=int1Schwellwert in Stunden ab dem ein Job als "stuck" gilt
--resetflagStatus auf null zurücksetzen (ermöglicht Retry)

Beispiele

# Stuck-Jobs finden (Default: > 1 Stunde)
php artisan youtube:find-stuck-related

# Großzügigerer Zeitraum (> 6 Stunden)
php artisan youtube:find-stuck-related --hours=6

# Stuck-Jobs finden UND zurücksetzen
php artisan youtube:find-stuck-related --reset

Interne Logik

  1. Profile mit related_channels_status IN (pending, running, pending_imports) suchen
  2. Nur Profile deren Status-Timestamp älter als --hours ist
  3. Tabelle mit Profil-ID, Titel, Status und Stuck-Dauer anzeigen
  4. Bei --reset: Interaktive Bestätigung → Status auf null setzen, Fehlermeldung loggen

Schedule

Manuell (Admin-Debugging-Tool).

Location: app/Console/Commands/FindStuckRelatedChannels.php


youtube:manage-websub

Verwaltet WebSub (PubSubHubbub) Subscriptions fuer YouTube-Channel-Push-Notifications. Subscriptions ermoeglichen Echtzeit-Erkennung neuer Videos ohne API-Quota.

youtube:manage-websub
{--renew : Ablaufende Subscriptions erneuern}
{--sync : Fehlende Subscriptions fuer Auto-Sync-Profile erstellen}
{--cleanup : Subscriptions fuer nicht-mehr-aktive Profile entfernen}
{--all : Alle drei Aktionen ausfuehren}

Beispiele

# Alle Aktionen ausfuehren (Standard via Schedule)
php artisan youtube:manage-websub --all

# Nur ablaufende Subscriptions erneuern
php artisan youtube:manage-websub --renew

# Neue Subscriptions fuer Profile ohne Subscription erstellen
php artisan youtube:manage-websub --sync

Optionen

OptionBeschreibung
--renewErneuert Subscriptions die innerhalb des konfigurierten Zeitfensters (Default: 48h) ablaufen. Profile mit 5+ aufeinanderfolgenden Renewal-Fehlern werden uebersprungen.
--syncErstellt fehlende Subscriptions fuer auto_sync_enabled-Profile ohne aktive Subscription.
--cleanupEntfernt Subscriptions fuer Profile ohne auto_sync_enabled. Sendet Unsubscribe an Hub und loescht DB-Record.
--allFuehrt --renew, --sync und --cleanup nacheinander aus.

Schedule

Taeglich um 06:00 UTC mit .withoutOverlapping(30). Cron-Heartbeat: websub_manage.

Location: app/Console/Commands/ManageWebSubSubscriptions.php


youtube:poll-rss-feeds

RSS-Fallback-Polling fuer YouTube-Channels mit Auto-Sync. Erkennt neue Videos wenn WebSub-Notifications ausbleiben. Nutzt ETag/If-Modified-Since-Caching.

youtube:poll-rss-feeds
{--batch-size= : Anzahl Channels pro Batch (ueberschreibt Config)}
{--profile-id= : Nur ein bestimmtes Profil pollen}

Beispiele

# Standard-Lauf (alle Auto-Sync-Profile, aelteste zuerst)
php artisan youtube:poll-rss-feeds

# Kleiner Batch fuer Testing
php artisan youtube:poll-rss-feeds --batch-size=10

# Einzelnes Profil pollen
php artisan youtube:poll-rss-feeds --profile-id=123

Optionen

OptionTypDefaultBeschreibung
--batch-size=intConfig-Wert (100)Anzahl Channels pro Durchlauf
--profile-id=intEinzelne Social-Profile-ID pollen

Interne Schritte

  1. youtube_video_syncs mit auto_sync_enabled = true laden, JOIN auf social_profiles fuer channel_id
  2. Sortiert nach rss_last_poll_at ASC (aelteste zuerst)
  3. Batch-Polling via Http::pool() mit konfigurierbarer Concurrency (Default: 10)
  4. ETag/If-Modified-Since Header fuer jede Anfrage (304 = keine neuen Videos)
  5. Atom-XML parsen und Video-IDs extrahieren
  6. Batch-Check gegen DB: nur neue Video-IDs dispatchen als FetchNewYouTubeVideo-Jobs
  7. rss_etag, rss_last_modified, rss_last_poll_at pro Channel aktualisieren
  8. Memory-Cleanup nach jedem Chunk: unset() + gc_collect_cycles() — SimpleXML DOM-Trees und HTTP-Response-Objekte halten zirkulaere Referenzen die PHPs Refcount-GC nicht automatisch freigibt

Schedule

Alle 30 Minuten mit .withoutOverlapping(25). Cron-Heartbeat: youtube_rss_poll.

Location: app/Console/Commands/PollYouTubeRssFeeds.php


youtube:aggregate-publishing-stats

Aggregiert YouTube Publishing-Analytics pro Kanal und Wochentag (Plan 13). Berechnet Video-Anzahl, VFR-Median, Engagement-Rate, Like-to-View-Ratio, Stunden-Verteilung, beste Uploadstunde und Konfidenzintervalle.

youtube:aggregate-publishing-stats
{--profile= : Nur fuer eine bestimmte Social-Profile-ID}
{--dry-run : Vorschau ohne Speicherung}

Beispiele

# Alle YouTube-Profile aggregieren
php artisan youtube:aggregate-publishing-stats

# Einzelnes Profil
php artisan youtube:aggregate-publishing-stats --profile=123

# Vorschau
php artisan youtube:aggregate-publishing-stats --dry-run

Optionen

OptionTypDefaultBeschreibung
--profile=intEinzelne Social-Profile-ID aggregieren
--dry-runflagNur Vorschau, keine Daten speichern

Berechnete Metriken pro Wochentag

MetrikBeschreibung
video_countAnzahl regulaere Videos (keine Shorts)
shorts_countAnzahl YouTube Shorts (admin-markiert oder auto-detected ≤60s)
vfr_medianMedian Views/Follower-Ratio
vfr_p25 / vfr_p7525./75. Perzentil fuer Konfidenzintervall
engagement_rate_medianMedian (Likes + Comments) / Followers
like_to_view_ratio_medianMedian Likes / Views
best_hour_utcBeste Uploadstunde (UTC) basierend auf VFR
hour_distributionJSON: Verteilung der Uploads pro Stunde (0-23)
prev_video_count / prev_vfr_medianVorherige Werte fuer Trend-Vergleich

Interne Schritte

  1. YouTube-Profile mit tracking_enabled, ohne blocked_at/sanitized_at, mit min. 3 Videos laden
  2. Verarbeitung in Chunks von 50 Profilen (Memory: 128 MB Limit)
  3. Pro Profil: Videos der letzten 365 Tage nach ISO-Wochentag (1=Mo, 7=So) gruppieren
  4. Shorts-Trennung: isEffectiveShort() (Admin-Markierung > Auto-Erkennung ≤60s)
  5. VFR, Engagement, L2V fuer regulaere Videos berechnen (Median)
  6. Stunden-Verteilung und beste Stunde (basierend auf VFR pro Stunde, min. 2 Videos)
  7. Trend-Daten: Vorherige Werte speichern bevor Upsert
  8. Upsert via updateOrCreate (eine Zeile pro Profil pro Wochentag)

Schedule

Woechentlich Montag 03:00 UTC mit .withoutOverlapping(). Cron-Heartbeat: youtube_publishing_stats.

Location: app/Console/Commands/AggregateYouTubePublishingStats.php


youtube:aggregate-video-stats-tracking

Aggregiert tägliche Video-Stats-Tracking-Abdeckung in youtube_video_stats_daily_aggregations. Berechnet wie viele Videos pro Tag frische Metriken erhalten haben, aufgeschlüsselt nach Gründen für fehlende Metriken (entfernt, Sync deaktiviert, über Limit, vor Cutoff).

youtube:aggregate-video-stats-tracking
{--date= : Aggregation für ein bestimmtes Datum (Y-m-d)}
{--backfill : Aggregation für alle Daten mit vorhandenen Metriken}
{--backfill-from= : Re-Aggregation ab spezifischem Datum bis gestern (UPSERT, überschreibt bestehende Einträge)}
{--enforce-limits : Überschüssige Video-Metriken rückwirkend löschen}
{--dry-run : Vorschau ohne Speicherung/Löschung}

Optionen

OptionTypDefaultBeschreibung
--date=stringgesternAggregation für ein spezifisches Datum
--backfillflagAlle Daten mit vorhandenen Metriken abarbeiten (überspringt existierende)
--backfill-from=stringPlan 75: Re-Aggregation ab Datum bis gestern, überschreibt bestehende Einträge via UPSERT
--enforce-limitsflagLöscht youtube_video_daily_metrics die über dem per-Profile max_videos-Limit liegen
--dry-runflagNur Vorschau, keine Daten speichern oder löschen

Beispiele

# Standard: Aggregation für gestern
php artisan youtube:aggregate-video-stats-tracking

# Bestimmtes Datum
php artisan youtube:aggregate-video-stats-tracking --date=2026-03-01

# Backfill für alle verfügbaren Daten
php artisan youtube:aggregate-video-stats-tracking --backfill

# Überschüssige Metriken entfernen (Vorschau)
php artisan youtube:aggregate-video-stats-tracking --enforce-limits --dry-run

# Überschüssige Metriken tatsächlich löschen
php artisan youtube:aggregate-video-stats-tracking --enforce-limits

Coverage-Berechnung

Die Coverage nutzt ROW_NUMBER() OVER (PARTITION BY social_profile_id ORDER BY published_at DESC) mit per-Profile-Limits:

  • Standard-Profile: max. 50 Videos (postbox.youtube_video_sync.max_videos)
  • PRO-Profile: max. 100 Videos (postbox.youtube_video_detection.pro_max_videos)

Nur Videos innerhalb dieses Limits zählen zum Denominator. Dies spiegelt exakt die Logik des SyncYouTubeVideoStats-Jobs wider.

PRO/Standard Breakdown

Pro Tag werden aufgeschlüsselt:

  • Anzahl PRO- und Standard-Profile mit Videos
  • Anzahl eligible Videos je Profil-Typ
  • Gesamtzahl Profile mit aktiviertem Auto-Sync

Shorts-Tracking

YouTube Shorts werden separat gezählt (shorts_total, shorts_with_metrics). Erkennung via isEffectiveShort()-Logik: is_short-Feld hat Vorrang (Admin-Markierung), Fallback auf duration_seconds <= 60.

--enforce-limits (Prune)

Löscht youtube_video_daily_metrics für Videos die über dem per-Profile max_videos-Limit liegen. Batched Deletes (1000 Video-IDs pro Batch) für Memory-Safety innerhalb des 128MB-Limits.

Performance (seit 2026-05-26)

Die fünf Reason-Counts (removed, sync_disabled, tracking_disabled, before_cutoff, over_limit) materialisieren ihre Kandidaten einmal in einer TEMP-Table tmp_reason_candidates_X mit vorberechneten Profil-Flags (has_active_sync, has_tracking_enabled, is_youtube). Die Counts danach sind triviale Aggregationen gegen die kleine Materialisierung — keine Joins, keine korrelierten Subqueries. Plus ANALYZE auf allen drei TEMP-Tables (tmp_eligible_videos_X, tmp_metrics_video_ids_X, tmp_reason_candidates_X) damit der PG-Planner echte Stats statt Default-Estimates nutzt.

Hintergrund: bei grossen Backfills (--backfill-from=2026-01-01) lief die alte Implementierung — 5 separate COUNT-Queries gegen die 5M+ Zeilen grosse youtube_videos-Tabelle, jede mit eigenem Profil-Join — wiederholt in 15-Minuten-Postgres-statement_timeout. Details: docs/04-integrationen/youtube-video-stats-tier-aware.md → Performance.

Schedule

Täglich um 22:00 UTC mit .withoutOverlapping(). Cron-Heartbeat: video_stats_tracking.

Location: app/Console/Commands/AggregateVideoStatsTracking.php


youtube:prune-inactive-video-metrics

Einmaliger Cleanup-Command (seit 2026-05-27, Folge-Massnahme zu Plan 67) — löscht youtube_video_daily_metrics-Rows für Videos, deren YouTube-Profil HEUTE nicht mehr Video-Stats-aktiv ist. Hintergrund: Nach Deaktivierung von ~13.000 Auto-PRO-Profilen sind die historischen Metriken nur noch Datenmüll und belegen ~20 GB Storage.

youtube:prune-inactive-video-metrics
{--dry-run : Nur Diagnose, kein DELETE}
{--batch-size=50000 : Anzahl Rows pro DELETE-Batch (eigene Transaction pro Batch)}
{--min-active-profiles=800 : Notbremse — minimale Anzahl aktive Profile im Schutz-Set}
{--max-active-profiles=3000 : Notbremse — maximale Anzahl aktive Profile im Schutz-Set}

Triple-Lock-Schutz

Ein Profil bleibt nur dann vor der Löschung verschont, wenn ALLE vier Bedingungen erfüllt sind:

TabelleSpalteErwartung
social_profilesplatform'youtube'
social_profilestracking_enabledtrue
social_profilespro_enabledtrue
youtube_video_syncsauto_sync_enabledtrue

Wenn nur eines der Felder false oder null ist, gelten alle Videos dieses Profils als Lösch-Kandidaten. Defensive Logik: lieber eine inaktive Profil-Spur stehen lassen als eine aktive löschen.

Pipeline

  1. TEMP-Table cleanup_active_profiles mit dem Triple-Lock-Schutz-Set (alle vier Bedingungen erfüllt)
  2. Sanity-Check — Schutz-Set muss zwischen --min-active-profiles und --max-active-profiles liegen. Stand 2026-05-27 sind das ~1.200 Profile. Wenn außerhalb: Abbruch vor dem DELETE.
  3. TEMP-Table cleanup_target_videos mit allen Videos der Nicht-Schutz-Profile
  4. Vor-Diagnose: Anzahl betroffener Daily-Metric-Rows + pg_total_relation_size('youtube_video_daily_metrics')
  5. Batched DELETE in Häppchen à batch-size (default 50.000) Rows, jeder Batch eine eigene Transaction → keine Lock-Eskalation auf der Tabelle, andere Schreiber werden nicht blockiert. EXISTS gegen die TEMP-Table → Hash-Anti-Join via PRIMARY KEY auf video_id.
  6. Heartbeat-Logs alle 20 Batches (1M Rows) in Log::info
  7. Post-Diagnose + Hinweis auf optionales VACUUM ANALYZE (kein Lock, Reuse-Pool intern) oder VACUUM FULL ANALYZE (lockt Tabelle, gibt Speicher ans OS zurück)

Beispiele

# Diagnose ohne Löschung (Phase 1 — Schutz-Set-Size + Lösch-Menge prüfen)
php artisan youtube:prune-inactive-video-metrics --dry-run

# Apply mit Default-Batches (50k Rows, Notbremse 800-3000 aktive Profile)
php artisan youtube:prune-inactive-video-metrics

# Konservativere Batches bei stark belasteter DB
php artisan youtube:prune-inactive-video-metrics --batch-size=10000

Sicherheits-Eigenschaften

  • Idempotent: Wiederholtes Ausführen löscht nichts mehr (Target-Set ist leer)
  • Pro Batch eigene Transaction: keine grossen Lock-Hold-Zeiten, andere Operationen auf youtube_video_daily_metrics werden nicht behindert
  • Notbremse bei Schutz-Set-Anomalien — wenn die Triple-Lock-Query versehentlich 0 oder 100k Profile findet, bricht der Command vor dem DELETE ab
  • Diagnose vor DELETE: Tabellen-Grösse + Lösch-Menge werden geloggt, sodass bei Anomalien gestoppt werden kann

Erwartete Performance

Mit EXISTS gegen die PRIMARY-KEY-indizierte TEMP-Table läuft Postgres bei ~50-100k Rows/Sekunde. 20 GB ≈ ~200M Daily-Metric-Rows; bei 50k/s = ~67 Minuten Gesamtlaufzeit.

Location: app/Console/Commands/Youtube/PruneInactiveVideoMetrics.php


youtube:prune-inactive-video-records

Folge-Cleanup zu youtube:prune-inactive-video-metrics (seit 2026-05-28). Beim ersten Durchlauf wurden nur die youtube_video_daily_metrics-Rows der inaktiven Profile gelöscht — die zugehörigen youtube_videos-Rows samt ihren Thumbnails (R2/local Storage-Disk) blieben bestehen. Bei ~13.000 disablten Profilen × ~50 Videos × ~4 Thumbnail-Varianten = bis zu 2,6M nutzlose Files auf dem Storage-Backend.

youtube:prune-inactive-video-records
{--dry-run : Nur Diagnose, kein DELETE}
{--batch-size=5000 : Anzahl Videos pro DELETE-Batch}
{--min-active-profiles=800 : Notbremse — minimale Anzahl aktive Profile im Schutz-Set}
{--max-active-profiles=3000 : Notbremse — maximale Anzahl aktive Profile im Schutz-Set}

Triple-Lock-Schutz

Identisch zu youtube:prune-inactive-video-metrics:

TabelleSpalteErwartung
social_profilesplatform'youtube'
social_profilestracking_enabledtrue
social_profilespro_enabledtrue
youtube_video_syncsauto_sync_enabledtrue

Pipeline

  1. TEMP-Table cleanup_active_profiles — Schutz-Set
  2. Sanity-Check — gleiche Notbremse wie Metrics-Cleanup
  3. TEMP-Table cleanup_target_videos — Videos aller Nicht-Schutz-Profile
  4. Vor-Diagnose — Anzahl Videos + geschätzte Anzahl Storage-Files (4 Spalten: thumbnail_path, thumbnail_path_large, thumbnail_path_medium, thumbnail_path_thumbnail)
  5. Per Batch (default 5.000 Videos):
    • Thumbnail-Paths sammeln (Null-Werte filtern)
    • Storage::disk('public')->delete([...]) — Batch-DELETE auf R2/local (best-effort; 404er ignoriert)
    • DELETE FROM youtube_videos WHERE video_id IN (chunk) — Cascade entfernt restliche youtube_video_daily_metrics + youtube_video_scores
  6. Post-Diagnose — gelöschte Files + DB-Tabellen-Grössen + Hinweis auf optionales VACUUM

Beispiele

# Diagnose ohne Löschung (Phase 1)
php artisan youtube:prune-inactive-video-records --dry-run

# Apply mit Default-Batches
php artisan youtube:prune-inactive-video-records

# Kleinere Batches bei langsamer Storage-API
php artisan youtube:prune-inactive-video-records --batch-size=1000

Sicherheits-Eigenschaften

  • Idempotent: Wiederholtes Ausführen löscht nichts mehr
  • Storage-Delete vor DB-Delete: vermeidet Waisen-Files (falls Crash zwischen den Schritten würde nur ein Batch wiederholt → erneutes Storage-Delete ist idempotent)
  • Storage-Delete best-effort: R2/local-Driver wirft nicht bei 404, der DB-Delete läuft trotzdem
  • Cascade-Schutz: youtube_video_scores und übrige youtube_video_daily_metrics werden via FK cascadeOnDelete() mitgelöscht — keine Waisen-Rows
  • Notbremse bei Schutz-Set-Anomalien (identisch zu Metrics-Cleanup)

Erwartete Performance

Pro Batch (5.000 Videos × ~4 Thumbnails ≈ 20.000 Storage-Deletes) dauert R2-API-seitig ~5-15 Sekunden + ~0,5s DB-Delete. Bei ~650.000 Lösch-Kandidaten = ~130 Batches = ~15-35 Minuten Gesamtlaufzeit.

Location: app/Console/Commands/Youtube/PruneInactiveVideoRecords.php


Übersicht: YouTube Queue Workers

Die YouTube-Jobs werden auf verschiedenen Queues verarbeitet:

QueueTimeoutTriesZweck
imports-youtube120s3Channel Updates (regulär)
imports-youtube-priority120s3Channel Updates (Leaders, PRO)
imports-youtube-video120s3Video-Statistiken Sync
imports-youtube-video-priority120s3Video-Statistiken (Priorität)
youtube-related-channels300s2Related Channel Discovery

Worker-Konfiguration (Forge)

# Channel Updates
php8.4 artisan queue:work database --sleep=5 --daemon --quiet --timeout=120 --tries=3 --queue=imports-youtube

# Priority Updates
php8.4 artisan queue:work database --sleep=5 --daemon --quiet --timeout=120 --tries=3 --queue=imports-youtube-priority

# Video Stats
php8.4 artisan queue:work database --sleep=5 --daemon --quiet --timeout=120 --tries=3 --queue=imports-youtube-video

# Related Channels (längerer Timeout wegen API-Calls)
php8.4 artisan queue:work database --sleep=5 --daemon --quiet --timeout=300 --tries=2 --queue=youtube-related-channels