package service import ( "testing" "vidarchive/internal/models" ) func TestPresetServiceBuildArgs(t *testing.T) { // Create a mock preset preset := &models.Preset{ FormatMode: "preset", Format: "best", Quality: "1080", ExtractAudio: true, AudioFormat: "mp3", EmbedSubs: true, SubLangs: "en,de", EmbedThumbnail: true, EmbedMetadata: true, WriteInfoJSON: true, CustomFlags: "--no-playlist", } svc := &PresetService{} args := svc.BuildArgs(preset, "", "") // Check that expected args are present hasFormat := false hasAudio := false hasSubs := false hasOutput := false for i := 0; i < len(args); i++ { switch args[i] { case "-f": if i+1 < len(args) { if args[i+1] == "best[height<=1080]/best" { hasFormat = true } } case "--extract-audio": hasAudio = true case "--embed-subs": hasSubs = true case "-o": if i+1 < len(args) { if args[i+1] == "item-%(autonumber)05d/%(title)s.%(ext)s" { hasOutput = true } } } } if !hasFormat { t.Error("expected format arg with quality limit") } if !hasAudio { t.Error("expected --extract-audio flag") } if !hasSubs { t.Error("expected --embed-subs flag") } if hasOutput { t.Error("output template should not be added by BuildArgs; it is enforced by the download service") } } func TestPresetServiceBuildArgsWithOverride(t *testing.T) { preset := &models.Preset{ Format: "best", } svc := &PresetService{} args := svc.BuildArgs(preset, "22", "--verbose") // Format override should take precedence hasOverride := false hasCustom := false for i := 0; i < len(args); i++ { if args[i] == "-f" && i+1 < len(args) && args[i+1] == "22" { hasOverride = true } if args[i] == "--verbose" { hasCustom = true } } if !hasOverride { t.Error("expected format override to be present") } if !hasCustom { t.Error("expected custom flags to be present") } } // yt-dlp separates extractor arguments with ";" — a "," separates the values of // one argument, so joining with it makes max_comments part of the sort value. func TestPresetServiceBuildArgsCommentExtractorArgs(t *testing.T) { cases := []struct { name string preset *models.Preset wantExtractor []string }{ { name: "comment args use a semicolon", preset: &models.Preset{WriteComments: true, CommentSort: "top", MaxComments: 25}, wantExtractor: []string{"youtube:comment_sort=top;max_comments=25"}, }, { // Two --extractor-args for the same key would make the second replace // the first, so youtube arguments must be merged into one flag. name: "youtube extra args are merged", preset: &models.Preset{ WriteComments: true, CommentSort: "top", MaxComments: 25, CommentExtractorArgs: "youtube:player_client=web", }, wantExtractor: []string{"youtube:comment_sort=top;max_comments=25;player_client=web"}, }, { name: "another extractor stays separate", preset: &models.Preset{ WriteComments: true, CommentSort: "top", CommentExtractorArgs: "vimeo:foo=bar", }, wantExtractor: []string{"youtube:comment_sort=top", "vimeo:foo=bar"}, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { args := (&PresetService{}).BuildArgs(tc.preset, "", "") var got []string for i, arg := range args { if arg == "--extractor-args" && i+1 < len(args) { got = append(got, args[i+1]) } } if len(got) != len(tc.wantExtractor) { t.Fatalf("--extractor-args values = %v, want %v", got, tc.wantExtractor) } for i, want := range tc.wantExtractor { if got[i] != want { t.Errorf("--extractor-args[%d] = %q, want %q", i, got[i], want) } } }) } } // A quoted value must survive as a single argument instead of being split on // spaces, which would hand yt-dlp three broken arguments. func TestPresetServiceBuildArgsKeepsQuotedCustomFlag(t *testing.T) { args := (&PresetService{}).BuildArgs(&models.Preset{}, "", `--match-filter "duration > 60"`) want := []string{"--match-filter", "duration > 60"} if len(args) != len(want) { t.Fatalf("args = %v, want %v", args, want) } for i := range want { if args[i] != want[i] { t.Errorf("args[%d] = %q, want %q", i, args[i], want[i]) } } } func TestPresetServiceIgnoresWhitespaceCommentSort(t *testing.T) { preset := &models.Preset{WriteComments: true, CommentSort: " "} flags := (&PresetService{}).EffectiveFlags(preset, "", "") if contains(flags, "comment_sort=") { t.Fatalf("whitespace-only comment sort emitted an empty argument: %q", flags) } } func TestPresetServiceEffectiveFlags(t *testing.T) { preset := &models.Preset{ FormatMode: "preset", Format: "best", Quality: "720", } svc := &PresetService{} flags := svc.EffectiveFlags(preset, "", "") if flags == "" { t.Error("expected non-empty flags string") } if flags != "-f best[height<=720]/best" { t.Errorf("expected '-f best[height<=720]/best', got %q", flags) } } func TestPresetServiceEffectiveFlagsWithCustom(t *testing.T) { preset := &models.Preset{ FormatMode: "preset", Format: "best", CustomFlags: "--no-warnings", } svc := &PresetService{} flags := svc.EffectiveFlags(preset, "", "--verbose") if flags == "" { t.Error("expected non-empty flags string") } // Should contain both preset custom flags and passed custom flags if !contains(flags, "--no-warnings") { t.Errorf("expected flags to contain '--no-warnings', got %q", flags) } if !contains(flags, "--verbose") { t.Errorf("expected flags to contain '--verbose', got %q", flags) } } func contains(s, substr string) bool { return len(s) >= len(substr) && (s == substr || len(s) > 0 && containsHelper(s, substr)) } func containsHelper(s, substr string) bool { for i := 0; i <= len(s)-len(substr); i++ { if s[i:i+len(substr)] == substr { return true } } return false }