ytdlp_live_test.go
⎇
Raw
1// These tests run the real yt-dlp against a real video. They exist to catch
2// yt-dlp version drift in the few places VidArchive depends on its behaviour:
3// the -J JSON field names, --flat-playlist printing, and the output layout
4// produced by -P/-o plus --write-info-json.
5//
6// They are opt-in because they need network access and take time. Run them with:
7//
8// VIDARCHIVE_ONLINE_TESTS=1 go test ./internal/service -run Live -v
9//
10// Override the video with VIDARCHIVE_TEST_VIDEO_URL.
11
12package service
13
14import (
15 "context"
16 "net/url"
17 "os"
18 "os/exec"
19 "path/filepath"
20 "testing"
21 "time"
22
23 "vidarchive/internal/models"
24)
25
26// defaultLiveVideoURL is a 19 second public video, small enough to download.
27const defaultLiveVideoURL = "https://www.youtube.com/watch?v=jNQXAC9IVRw"
28
29// liveEnv returns an execEnv wired to the real yt-dlp, the test video URL, and
30// the video id expected for it. It skips the test unless the online tests are
31// enabled and yt-dlp is installed.
32func liveEnv(t *testing.T) (*execEnv, string, string) {
33 t.Helper()
34
35 if os.Getenv("VIDARCHIVE_ONLINE_TESTS") != "1" {
36 t.Skip("online test: set VIDARCHIVE_ONLINE_TESTS=1 to run it")
37 }
38 ytdlp, err := exec.LookPath("yt-dlp")
39 if err != nil {
40 t.Skipf("online test: yt-dlp not found on PATH: %v", err)
41 }
42
43 videoURL := os.Getenv("VIDARCHIVE_TEST_VIDEO_URL")
44 if videoURL == "" {
45 videoURL = defaultLiveVideoURL
46 }
47 // The id comes from the URL, so an override still knows what to expect.
48 parsed, err := url.Parse(videoURL)
49 if err != nil {
50 t.Fatalf("parse test video URL %q: %v", videoURL, err)
51 }
52 videoID := parsed.Query().Get("v")
53 if videoID == "" {
54 t.Fatalf("test video URL %q has no v parameter", videoURL)
55 }
56
57 e := newExecEnv(t)
58 e.cfg.YTDLPPath = ytdlp
59 return e, videoURL, videoID
60}
61
62// Checks the -J JSON field names the format picker reads.
63func TestLiveListFormats(t *testing.T) {
64 e, videoURL, _ := liveEnv(t)
65
66 formats, entry, err := e.svc.ListFormats(t.Context(), videoURL)
67 if err != nil {
68 t.Fatalf("ListFormats: %v", err)
69 }
70 if entry != "" {
71 t.Errorf("single video URL reported playlist entry %q", entry)
72 }
73 if len(formats) == 0 {
74 t.Fatal("ListFormats returned no formats")
75 }
76 for _, f := range formats {
77 if f.ID == "" || f.Ext == "" {
78 t.Fatalf("format with empty id or ext: %+v", f)
79 }
80 }
81}
82
83// Checks --flat-playlist --print %(id)s, which subscription pruning relies on.
84func TestLiveEnumeratePlaylistIDs(t *testing.T) {
85 e, videoURL, videoID := liveEnv(t)
86
87 ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
88 defer cancel()
89
90 ids, err := e.svc.enumeratePlaylistIDs(ctx, videoURL)
91 if err != nil {
92 t.Fatalf("enumeratePlaylistIDs: %v", err)
93 }
94 if !ids[videoID] {
95 t.Errorf("ids = %v, want them to contain %q", ids, videoID)
96 }
97}
98
99// Checks the full download path: --newline, -P/-o with %(autonumber), the
100// info.json sidecar, and the import that turns it into a library item.
101func TestLiveExecuteDownload(t *testing.T) {
102 e, videoURL, videoID := liveEnv(t)
103
104 preset := &models.Preset{Name: "Live", WriteInfoJSON: true}
105 if err := e.svc.presetSvc.Save(preset); err != nil {
106 t.Fatalf("create preset: %v", err)
107 }
108
109 // "wa/w" is worst audio, else worst overall: the smallest thing yt-dlp can
110 // give us, so the test stays cheap.
111 d := e.queue(t, &models.Download{
112 URL: videoURL,
113 PresetID: sqlNullInt64(preset.ID),
114 FormatOverride: "wa/w",
115 })
116
117 ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute)
118 defer cancel()
119
120 processed, err := e.svc.ExecuteDownload(ctx, d)
121 if err != nil {
122 t.Fatalf("ExecuteDownload: %v", err)
123 }
124 if !processed {
125 t.Error("processed = false, want true")
126 }
127 if got := e.status(t, d.ID); got.Status != "completed" {
128 t.Fatalf("status = %q, want completed; logs:\n%s", got.Status, got.Logs.String)
129 }
130
131 names := libraryEntries(t, e.cfg.LibraryDir)
132 if len(names) != 1 {
133 t.Fatalf("library = %v, want exactly one item", names)
134 }
135 itemDir := filepath.Join(e.cfg.LibraryDir, names[0])
136
137 if _, err := os.Stat(filepath.Join(itemDir, itemMarkerName)); err != nil {
138 t.Errorf("item marker missing: %v", err)
139 }
140 infoPath := filepath.Join(itemDir, "info.json")
141 if _, err := os.Stat(infoPath); err != nil {
142 t.Fatalf("info.json missing: %v", err)
143 }
144
145 info := readInfoJSON(infoPath)
146 if info.ID != videoID {
147 t.Errorf("info.json id = %q, want %q", info.ID, videoID)
148 }
149 if info.Title == "" {
150 t.Error("info.json title is empty")
151 }
152 if info.WebpageURL == "" {
153 t.Error("info.json webpage_url is empty")
154 }
155}
156