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, err := e.svc.ListFormats(t.Context(), videoURL)
67 if err != nil {
68 t.Fatalf("ListFormats: %v", err)
69 }
70 if len(formats) == 0 {
71 t.Fatal("ListFormats returned no formats")
72 }
73 for _, f := range formats {
74 if f.ID == "" || f.Ext == "" {
75 t.Fatalf("format with empty id or ext: %+v", f)
76 }
77 }
78}
79
80// Checks --flat-playlist --print %(id)s, which subscription pruning relies on.
81func TestLiveEnumeratePlaylistIDs(t *testing.T) {
82 e, videoURL, videoID := liveEnv(t)
83
84 ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
85 defer cancel()
86
87 ids, err := e.svc.enumeratePlaylistIDs(ctx, videoURL)
88 if err != nil {
89 t.Fatalf("enumeratePlaylistIDs: %v", err)
90 }
91 if !ids[videoID] {
92 t.Errorf("ids = %v, want them to contain %q", ids, videoID)
93 }
94}
95
96// Checks the full download path: --newline, -P/-o with %(autonumber), the
97// info.json sidecar, and the import that turns it into a library item.
98func TestLiveExecuteDownload(t *testing.T) {
99 e, videoURL, videoID := liveEnv(t)
100
101 preset := &models.Preset{Name: "Live", WriteInfoJSON: true}
102 if err := e.svc.presetSvc.Create(preset); err != nil {
103 t.Fatalf("create preset: %v", err)
104 }
105
106 // "wa/w" is worst audio, else worst overall: the smallest thing yt-dlp can
107 // give us, so the test stays cheap.
108 d := e.queue(t, &models.Download{
109 URL: videoURL,
110 PresetID: sqlNullInt64(preset.ID),
111 FormatOverride: "wa/w",
112 })
113
114 ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute)
115 defer cancel()
116
117 processed, err := e.svc.ExecuteDownload(ctx, d)
118 if err != nil {
119 t.Fatalf("ExecuteDownload: %v", err)
120 }
121 if !processed {
122 t.Error("processed = false, want true")
123 }
124 if got := e.status(t, d.ID); got.Status != "completed" {
125 t.Fatalf("status = %q, want completed; logs:\n%s", got.Status, got.Logs.String)
126 }
127
128 names := libraryEntries(t, e.cfg.LibraryDir)
129 if len(names) != 1 {
130 t.Fatalf("library = %v, want exactly one item", names)
131 }
132 itemDir := filepath.Join(e.cfg.LibraryDir, names[0])
133
134 if _, err := os.Stat(filepath.Join(itemDir, itemMarkerName)); err != nil {
135 t.Errorf("item marker missing: %v", err)
136 }
137 infoPath := filepath.Join(itemDir, "info.json")
138 if _, err := os.Stat(infoPath); err != nil {
139 t.Fatalf("info.json missing: %v", err)
140 }
141
142 info := readInfoJSON(infoPath)
143 if info.ID != videoID {
144 t.Errorf("info.json id = %q, want %q", info.ID, videoID)
145 }
146 if info.Title == "" {
147 t.Error("info.json title is empty")
148 }
149 if info.WebpageURL == "" {
150 t.Error("info.json webpage_url is empty")
151 }
152}
153