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
| Option | Typ | Default | Beschreibung |
|---|---|---|---|
--profile-id= | int | – | Einzelne Social-Profile-ID für One-Off Sync |
--max-profiles= | int | Config-Wert | Maximale Profile pro Lauf, überschreibt postbox.youtube_video_sync.max_profiles_per_run (Default: 0 = unbegrenzt) |
Interne Schritte
YouTubeVideoSyncRecords mitauto_sync_enabled = trueladen- Optional nach
--profile-idfiltern - Bei
--max-profiles> 0: Profile nachlast_sync_atASC sortieren und limitieren (älteste zuerst) - Pro Profil einen
SyncYouTubeVideoStatsJob dispatchen - Jobs landen auf der
imports-youtube-videoQueue - Health-Marker in Cache setzen (
health:youtube_video_sync_ran:{date}) mit Anzahl Jobs und Zeitstempel — wird vonhealth:check-daily-scrapesund/up_systemgelesen
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:falseseit 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
auto_sync_enabled = trueaufyoutube_video_syncssetzenvideo_tracking_start_date = today(falls null)requested_by = NULL(Repair-Marker)- Initialer
SyncYouTubeVideoStats-Job dispatchen (falls noch keine Videos da) 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
| Option | Beschreibung |
|---|---|
--dry-run | Zeigt welches Profil gewählt würde, ohne zu dispatchen |
--force | Ignoriert 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 Skips | Backoff-Dauer | Nächster Check frühestens |
|---|---|---|
| 1 | 15 Minuten | Normal: 5min → Effektiv: 15min |
| 2 | 30 Minuten | Normal: 5min → Effektiv: 30min |
| 3+ | 60 Minuten | Normal: 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ät | Kriterium | Sortierung |
|---|---|---|
| 1 | Profile ohne Related Channels (related_channels_searched_at IS NULL) | Followers DESC |
| 2 | Profile mit stale Related Channels (> 30 Tage alt) | related_channels_searched_at ASC |
Interne Schritte
- Backoff prüfen: Falls im Cooldown, sofort abbrechen (außer
--force) - Quota Stufe 1: Research-Quota via
ResearchQuotaService::getStatus()prüfen (forceRefresh) - Quota Stufe 2: Live Redis-Check via
YouTubeQuotaGuard::canMakeCall('research') - Nächstes Profil nach Priority-Logik finden
- Profile mit Status
runningoderpendingausschließen (Duplikat-Schutz) related_channels_status = 'pending'setzenFindRelatedYouTubeChannelsJob aufyoutube-related-channelsQueue dispatchen- 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
YouTubeResearchQueryRecords mitcreated_at < now()->subDays(30)selektieren- Alle gefundenen Records löschen
- Research Queries entstehen durch
FindRelatedYouTubeChannelsJobs
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
| Option | Typ | Default | Beschreibung |
|---|---|---|---|
--date= | string | – | Score für ein spezifisches Datum (Format: Y-m-d) |
--from= | string | – | Start-Datum für Range-Berechnung |
--to= | string | – | End-Datum für Range-Berechnung |
--backfill | flag | – | Alle Daten mit vorhandenen Metriken abarbeiten |
--profile-id= | int | – | Nur Videos dieses Profils verarbeiten |
--force | flag | – | Bestehende Scores überschreiben (Default: überspringen) |
--dry-run | flag | – | Nur 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
- Videos mit genügend Datenpunkten (
MIN_DATA_POINTSausVideoScoreCalculator) identifizieren - Lookback-Window (
METRICS_LOOKBACK_DAYS) für relevante Daily-Metriken - Channel-Tier pro Profil cachen (verhindert wiederholte Follower-Lookups)
- Tier-normalisierter Score (0-100): Micro-Channels werden nicht unfair mit Mega-Channels verglichen
- 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
| Option | Typ | Default | Beschreibung |
|---|---|---|---|
--hours= | int | 1 | Schwellwert in Stunden ab dem ein Job als "stuck" gilt |
--reset | flag | – | Status 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
- Profile mit
related_channels_statusIN (pending,running,pending_imports) suchen - Nur Profile deren Status-Timestamp älter als
--hoursist - Tabelle mit Profil-ID, Titel, Status und Stuck-Dauer anzeigen
- Bei
--reset: Interaktive Bestätigung → Status aufnullsetzen, 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
| Option | Beschreibung |
|---|---|
--renew | Erneuert Subscriptions die innerhalb des konfigurierten Zeitfensters (Default: 48h) ablaufen. Profile mit 5+ aufeinanderfolgenden Renewal-Fehlern werden uebersprungen. |
--sync | Erstellt fehlende Subscriptions fuer auto_sync_enabled-Profile ohne aktive Subscription. |
--cleanup | Entfernt Subscriptions fuer Profile ohne auto_sync_enabled. Sendet Unsubscribe an Hub und loescht DB-Record. |
--all | Fuehrt --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
| Option | Typ | Default | Beschreibung |
|---|---|---|---|
--batch-size= | int | Config-Wert (100) | Anzahl Channels pro Durchlauf |
--profile-id= | int | – | Einzelne Social-Profile-ID pollen |
Interne Schritte
youtube_video_syncsmitauto_sync_enabled = trueladen, JOIN aufsocial_profilesfuerchannel_id- Sortiert nach
rss_last_poll_at ASC(aelteste zuerst) - Batch-Polling via
Http::pool()mit konfigurierbarer Concurrency (Default: 10) - ETag/If-Modified-Since Header fuer jede Anfrage (304 = keine neuen Videos)
- Atom-XML parsen und Video-IDs extrahieren
- Batch-Check gegen DB: nur neue Video-IDs dispatchen als
FetchNewYouTubeVideo-Jobs rss_etag,rss_last_modified,rss_last_poll_atpro Channel aktualisieren- 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
| Option | Typ | Default | Beschreibung |
|---|---|---|---|
--profile= | int | – | Einzelne Social-Profile-ID aggregieren |
--dry-run | flag | – | Nur Vorschau, keine Daten speichern |
Berechnete Metriken pro Wochentag
| Metrik | Beschreibung |
|---|---|
video_count | Anzahl regulaere Videos (keine Shorts) |
shorts_count | Anzahl YouTube Shorts (admin-markiert oder auto-detected ≤60s) |
vfr_median | Median Views/Follower-Ratio |
vfr_p25 / vfr_p75 | 25./75. Perzentil fuer Konfidenzintervall |
engagement_rate_median | Median (Likes + Comments) / Followers |
like_to_view_ratio_median | Median Likes / Views |
best_hour_utc | Beste Uploadstunde (UTC) basierend auf VFR |
hour_distribution | JSON: Verteilung der Uploads pro Stunde (0-23) |
prev_video_count / prev_vfr_median | Vorherige Werte fuer Trend-Vergleich |
Interne Schritte
- YouTube-Profile mit
tracking_enabled, ohneblocked_at/sanitized_at, mit min. 3 Videos laden - Verarbeitung in Chunks von 50 Profilen (Memory: 128 MB Limit)
- Pro Profil: Videos der letzten 365 Tage nach ISO-Wochentag (1=Mo, 7=So) gruppieren
- Shorts-Trennung:
isEffectiveShort()(Admin-Markierung > Auto-Erkennung ≤60s) - VFR, Engagement, L2V fuer regulaere Videos berechnen (Median)
- Stunden-Verteilung und beste Stunde (basierend auf VFR pro Stunde, min. 2 Videos)
- Trend-Daten: Vorherige Werte speichern bevor Upsert
- 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
| Option | Typ | Default | Beschreibung |
|---|---|---|---|
--date= | string | gestern | Aggregation für ein spezifisches Datum |
--backfill | flag | – | Alle Daten mit vorhandenen Metriken abarbeiten (überspringt existierende) |
--backfill-from= | string | – | Plan 75: Re-Aggregation ab Datum bis gestern, überschreibt bestehende Einträge via UPSERT |
--enforce-limits | flag | – | Löscht youtube_video_daily_metrics die über dem per-Profile max_videos-Limit liegen |
--dry-run | flag | – | Nur 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:
| Tabelle | Spalte | Erwartung |
|---|---|---|
social_profiles | platform | 'youtube' |
social_profiles | tracking_enabled | true |
social_profiles | pro_enabled | true |
youtube_video_syncs | auto_sync_enabled | true |
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
- TEMP-Table
cleanup_active_profilesmit dem Triple-Lock-Schutz-Set (alle vier Bedingungen erfüllt) - Sanity-Check — Schutz-Set muss zwischen
--min-active-profilesund--max-active-profilesliegen. Stand 2026-05-27 sind das ~1.200 Profile. Wenn außerhalb: Abbruch vor dem DELETE. - TEMP-Table
cleanup_target_videosmit allen Videos der Nicht-Schutz-Profile - Vor-Diagnose: Anzahl betroffener Daily-Metric-Rows +
pg_total_relation_size('youtube_video_daily_metrics') - 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.EXISTSgegen die TEMP-Table → Hash-Anti-Join via PRIMARY KEY aufvideo_id. - Heartbeat-Logs alle 20 Batches (1M Rows) in
Log::info - Post-Diagnose + Hinweis auf optionales
VACUUM ANALYZE(kein Lock, Reuse-Pool intern) oderVACUUM 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_metricswerden 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:
| Tabelle | Spalte | Erwartung |
|---|---|---|
social_profiles | platform | 'youtube' |
social_profiles | tracking_enabled | true |
social_profiles | pro_enabled | true |
youtube_video_syncs | auto_sync_enabled | true |
Pipeline
- TEMP-Table
cleanup_active_profiles— Schutz-Set - Sanity-Check — gleiche Notbremse wie Metrics-Cleanup
- TEMP-Table
cleanup_target_videos— Videos aller Nicht-Schutz-Profile - Vor-Diagnose — Anzahl Videos + geschätzte Anzahl Storage-Files (4 Spalten:
thumbnail_path,thumbnail_path_large,thumbnail_path_medium,thumbnail_path_thumbnail) - 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 restlicheyoutube_video_daily_metrics+youtube_video_scores
- 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_scoresund übrigeyoutube_video_daily_metricswerden via FKcascadeOnDelete()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:
| Queue | Timeout | Tries | Zweck |
|---|---|---|---|
imports-youtube | 120s | 3 | Channel Updates (regulär) |
imports-youtube-priority | 120s | 3 | Channel Updates (Leaders, PRO) |
imports-youtube-video | 120s | 3 | Video-Statistiken Sync |
imports-youtube-video-priority | 120s | 3 | Video-Statistiken (Priorität) |
youtube-related-channels | 300s | 2 | Related 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