rssd/cmd/probe/main.go
Greg Pomerantz bc26e73a90 Add YouTube feed support via yt-dlp, MIME-based extensions, robust state
- ytdlp: thin wrapper around yt-dlp CLI for channel/playlist discovery
  (--flat-playlist) and per-video metadata (--print-json); defaults to
  player_client=web_embedded to avoid SABR-only format restrictions.
- downloader: YouTube path extracts audio via yt-dlp -x, probes duration
  with ffprobe and rejects clips far shorter than the expected length.
- main: YouTube channel/playlist feeds, incremental discovery with
  pre-start-date boundary for newest-first channels, two-phase
  record-then-enqueue so discovered jobs survive crashes.
- poller: propagate enclosure MIME type; computeDestPath appends the
  matching file extension.
- state: jobs carry expected duration; AddJobWithStatus for skipped items.
- cmd/probe: dry-run discovery/date-resolution diagnostics.
2026-09-23 09:06:36 -04:00

121 lines
2.7 KiB
Go

// Command probe performs a dry run of the YouTube discovery + date-resolution
// logic for one or more channel/playlist URLs. It resolves every video's
// upload date and reports how many fall before/after a start date, without
// downloading anything.
//
// Usage:
//
// probe <start-date YYYY-MM-DD> <url> [url ...]
package main
import (
"context"
"fmt"
"os"
"time"
"rssd/ytdlp"
)
func main() {
if len(os.Args) < 3 {
fmt.Fprintln(os.Stderr, "usage: probe <start-date YYYY-MM-DD> <url> [url ...]")
os.Exit(2)
}
start, err := time.Parse("2006-01-02", os.Args[1])
if err != nil {
fmt.Fprintln(os.Stderr, "bad start date:", err)
os.Exit(2)
}
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Hour)
defer cancel()
s := ytdlp.Settings{}
for _, u := range os.Args[2:] {
probeOne(ctx, s, u, start)
}
}
func probeOne(ctx context.Context, s ytdlp.Settings, url string, start time.Time) {
fmt.Printf("\n=== %s ===\n", url)
vids, err := ytdlp.Discovery(ctx, s, url)
if err != nil {
fmt.Fprintf(os.Stderr, " discovery error: %v\n", err)
return
}
fmt.Printf(" discovered %d videos\n", len(vids))
newestFirst := true
for _, tok := range []string{"list="} {
if contains(url, tok) {
newestFirst = false
}
}
fmt.Printf(" assumed newest-first: %v\n", newestFirst)
total := 0
inRange, beforeStart, noDate, failed := 0, 0, 0, 0
var oldest, newest time.Time
for _, v := range vids {
total++
info, err := ytdlp.Info(ctx, s, v.ID)
if err != nil {
failed++
if failed <= 5 {
fmt.Fprintf(os.Stderr, " resolve warn %s: %v\n", v.ID, err)
}
continue
}
if info.UploadDate.IsZero() {
noDate++
continue
}
if oldest.IsZero() || info.UploadDate.Before(oldest) {
oldest = info.UploadDate
}
if info.UploadDate.After(newest) {
newest = info.UploadDate
}
if info.UploadDate.Before(start) {
beforeStart++
if newestFirst {
// Channel is newest-first: everything after this is older, so stop.
fmt.Printf(" reached pre-start-date boundary, stopping\n")
break
}
continue
}
inRange++
if inRange <= 5 {
fmt.Printf(" IN %s %s (%s)\n", info.UploadDate.Format("2006-01-02"), v.ID, trunc(v.Title, 60))
}
}
fmt.Printf(" resolved=%d inRange(>=%s)=%d beforeStart=%d noDate=%d failed=%d\n",
total, start.Format("2006-01-02"), inRange, beforeStart, noDate, failed)
fmt.Printf(" oldest=%s newest=%s\n", fmtDate(oldest), fmtDate(newest))
}
func contains(s, sub string) bool {
for i := 0; i+len(sub) <= len(s); i++ {
if s[i:i+len(sub)] == sub {
return true
}
}
return false
}
func trunc(s string, n int) string {
if len(s) <= n {
return s
}
return s[:n] + "…"
}
func fmtDate(t time.Time) string {
if t.IsZero() {
return "-"
}
return t.Format("2006-01-02")
}