All checks were successful
CI / build (push) Successful in 52s
- Add MediaTypeAsk (0) as the default, mapping old video_audio to Ask - Show Video/Audio/Ask type picker when no default media type is set - Auto-select matching format and skip picker when defaults are configured - Add autoSelectVideoFormat / autoSelectAudioFormat helpers with closest-match - Add buildFilteredFormatList for type-filtered format lists - Add Media Type setting in settings UI (Ask/Video/Audio) - Add --merge-output-format mp4 for video downloads via yt-dlp - Fix formatLabelForDisplay to show Height fallback and Extension - Add cookies volume mount to docker-compose.yml - Document cookies setup and new features in README
420 lines
11 KiB
Go
420 lines
11 KiB
Go
package handler
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"uptodownBot/internal/domain"
|
|
)
|
|
|
|
func TestBuildFormatList(t *testing.T) {
|
|
formats := []domain.Format{
|
|
{ID: "1", HasVideo: true, HasAudio: false, Resolution: "1920x1080"},
|
|
{ID: "2", HasVideo: false, HasAudio: true, Bitrate: "128k"},
|
|
{ID: "3", HasVideo: true, HasAudio: false, Resolution: "1280x720"},
|
|
{ID: "4", HasVideo: true, HasAudio: true, Resolution: "640x480"},
|
|
}
|
|
ids, err := buildFormatList(formats)
|
|
if err != nil {
|
|
t.Fatalf("buildFormatList() error: %v", err)
|
|
}
|
|
if len(ids) == 0 {
|
|
t.Fatal("buildFormatList() returned empty list")
|
|
}
|
|
if ids[0] != "best" {
|
|
t.Errorf("first ID = %q, want best", ids[0])
|
|
}
|
|
// Best, then all video formats, then all audio-only
|
|
wantVideo := map[string]bool{"1": true, "3": true, "4": true}
|
|
wantAudio := map[string]bool{"2": true}
|
|
seenAudio := false
|
|
for i := 1; i < len(ids); i++ {
|
|
if wantVideo[ids[i]] {
|
|
if seenAudio {
|
|
t.Errorf("video format %q found after audio-only formats", ids[i])
|
|
}
|
|
}
|
|
if wantAudio[ids[i]] {
|
|
seenAudio = true
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestFindFormatByID(t *testing.T) {
|
|
formats := []domain.Format{
|
|
{ID: "1", Resolution: "1920x1080"},
|
|
{ID: "2", Bitrate: "128k"},
|
|
}
|
|
f := findFormatByID(formats, "1")
|
|
if f == nil {
|
|
t.Fatal("findFormatByID(1) = nil, want non-nil")
|
|
}
|
|
if f.ID != "1" {
|
|
t.Errorf("findFormatByID(1).ID = %q, want 1", f.ID)
|
|
}
|
|
f = findFormatByID(formats, "nonexistent")
|
|
if f != nil {
|
|
t.Errorf("findFormatByID(nonexistent) = %+v, want nil", f)
|
|
}
|
|
}
|
|
|
|
func TestStepStateDetermineMediaType(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
ss *stepState
|
|
want domain.MediaType
|
|
}{
|
|
{
|
|
name: "nil main format",
|
|
ss: &stepState{},
|
|
want: domain.MediaTypeVideoAudio,
|
|
},
|
|
{
|
|
name: "audio only no secondary",
|
|
ss: &stepState{MainFormat: &domain.Format{IsAudioOnly: true}},
|
|
want: domain.MediaTypeAudio,
|
|
},
|
|
{
|
|
name: "audio only with secondary",
|
|
ss: &stepState{MainFormat: &domain.Format{IsAudioOnly: true}, SecondaryFmt: &domain.Format{HasVideo: true}},
|
|
want: domain.MediaTypeVideoAudio,
|
|
},
|
|
{
|
|
name: "has video, has audio",
|
|
ss: &stepState{MainFormat: &domain.Format{HasVideo: true, HasAudio: true}},
|
|
want: domain.MediaTypeVideoAudio,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := tt.ss.determineMediaType()
|
|
if got != tt.want {
|
|
t.Errorf("determineMediaType() = %v, want %v", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestStepStateBuildFormatString(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
ss *stepState
|
|
want string
|
|
}{
|
|
{
|
|
name: "best format",
|
|
ss: &stepState{MainFormatID: "best"},
|
|
want: "",
|
|
},
|
|
{
|
|
name: "single format",
|
|
ss: &stepState{MainFormatID: "137"},
|
|
want: "137",
|
|
},
|
|
{
|
|
name: "combined format",
|
|
ss: &stepState{MainFormatID: "137", SecondaryID: "140", SecondaryFmt: &domain.Format{ID: "140"}},
|
|
want: "137+140",
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := tt.ss.buildFormatString()
|
|
if got != tt.want {
|
|
t.Errorf("buildFormatString() = %q, want %q", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFormatBytes(t *testing.T) {
|
|
tests := []struct {
|
|
input int64
|
|
want string
|
|
}{
|
|
{0, "0B"},
|
|
{1, "1B"},
|
|
{1023, "1023B"},
|
|
{1024, "1.0KB"},
|
|
{1536, "1.5KB"},
|
|
{1048576, "1.0MB"},
|
|
{1073741824, "1.0GB"},
|
|
{1099511627776, "1.0TB"},
|
|
}
|
|
for _, tt := range tests {
|
|
got := formatBytes(tt.input)
|
|
if got != tt.want {
|
|
t.Errorf("formatBytes(%d) = %q, want %q", tt.input, got, tt.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestFormatDuration(t *testing.T) {
|
|
tests := []struct {
|
|
input float64
|
|
want string
|
|
}{
|
|
{0, "0:00"},
|
|
{5, "0:05"},
|
|
{60, "1:00"},
|
|
{65, "1:05"},
|
|
{3600, "1:00:00"},
|
|
{3661, "1:01:01"},
|
|
{86400, "24:00:00"},
|
|
}
|
|
for _, tt := range tests {
|
|
got := formatDuration(tt.input)
|
|
if got != tt.want {
|
|
t.Errorf("formatDuration(%f) = %q, want %q", tt.input, got, tt.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestBuildFilteredFormatList(t *testing.T) {
|
|
formats := []domain.Format{
|
|
{ID: "1", HasVideo: true, HasAudio: false, Resolution: "1920x1080"},
|
|
{ID: "2", HasVideo: false, HasAudio: true, IsAudioOnly: true, Bitrate: "128k"},
|
|
{ID: "3", HasVideo: true, HasAudio: false, Resolution: "1280x720"},
|
|
{ID: "4", HasVideo: true, HasAudio: true, Resolution: "640x480"},
|
|
{ID: "5", HasVideo: false, HasAudio: true, IsAudioOnly: true, Bitrate: "320k"},
|
|
}
|
|
|
|
t.Run("ask type returns all formats", func(t *testing.T) {
|
|
ids, err := buildFilteredFormatList(formats, "ask")
|
|
if err != nil {
|
|
t.Fatalf("buildFilteredFormatList(ask) error: %v", err)
|
|
}
|
|
if ids[0] != "best" {
|
|
t.Errorf("first ID = %q, want best", ids[0])
|
|
}
|
|
// Should include all non-best IDs
|
|
expectedIDs := map[string]bool{"1": true, "2": true, "3": true, "4": true, "5": true}
|
|
for _, id := range ids[1:] {
|
|
if !expectedIDs[id] {
|
|
t.Errorf("unexpected ID %q in ask list", id)
|
|
}
|
|
delete(expectedIDs, id)
|
|
}
|
|
if len(expectedIDs) > 0 {
|
|
t.Errorf("missing IDs in ask list: %v", expectedIDs)
|
|
}
|
|
})
|
|
|
|
t.Run("video type only returns video formats", func(t *testing.T) {
|
|
ids, err := buildFilteredFormatList(formats, "video")
|
|
if err != nil {
|
|
t.Fatalf("buildFilteredFormatList(video) error: %v", err)
|
|
}
|
|
if ids[0] != "best" {
|
|
t.Errorf("first ID = %q, want best", ids[0])
|
|
}
|
|
for _, id := range ids[1:] {
|
|
if id == "2" || id == "5" {
|
|
t.Errorf("video list contains audio-only ID %q", id)
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("audio type only returns audio-only formats", func(t *testing.T) {
|
|
ids, err := buildFilteredFormatList(formats, "audio")
|
|
if err != nil {
|
|
t.Fatalf("buildFilteredFormatList(audio) error: %v", err)
|
|
}
|
|
if ids[0] != "best" {
|
|
t.Errorf("first ID = %q, want best", ids[0])
|
|
}
|
|
for _, id := range ids[1:] {
|
|
if id == "1" || id == "3" || id == "4" {
|
|
t.Errorf("audio list contains video ID %q", id)
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("default (empty) returns all formats", func(t *testing.T) {
|
|
ids, err := buildFilteredFormatList(formats, "")
|
|
if err != nil {
|
|
t.Fatalf("buildFilteredFormatList('') error: %v", err)
|
|
}
|
|
if len(ids) != 6 { // best + 5 formats
|
|
t.Errorf("got %d IDs, want 6", len(ids))
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestAutoSelectVideoFormat(t *testing.T) {
|
|
formats := []domain.Format{
|
|
{ID: "137", HasVideo: true, HasAudio: false, Height: 1080, Width: 1920, Resolution: "1920x1080"},
|
|
{ID: "136", HasVideo: true, HasAudio: false, Height: 720, Width: 1280, Resolution: "1280x720"},
|
|
{ID: "135", HasVideo: true, HasAudio: false, Height: 480, Width: 854, Resolution: "854x480"},
|
|
{ID: "247", HasVideo: true, HasAudio: true, Height: 1080, Width: 1920, Resolution: "1920x1080"},
|
|
{ID: "140", HasVideo: false, HasAudio: true, IsAudioOnly: true, Bitrate: "128k"},
|
|
}
|
|
|
|
t.Run("best returns best, no secondary", func(t *testing.T) {
|
|
mainID, secondaryID := autoSelectVideoFormat(formats, "best")
|
|
if mainID != "best" {
|
|
t.Errorf("mainID = %q, want best", mainID)
|
|
}
|
|
if secondaryID != "" {
|
|
t.Errorf("secondaryID = %q, want empty", secondaryID)
|
|
}
|
|
})
|
|
|
|
t.Run("empty returns best", func(t *testing.T) {
|
|
mainID, _ := autoSelectVideoFormat(formats, "")
|
|
if mainID != "best" {
|
|
t.Errorf("mainID = %q, want best", mainID)
|
|
}
|
|
})
|
|
|
|
t.Run("exact match 1080p", func(t *testing.T) {
|
|
mainID, _ := autoSelectVideoFormat(formats, "1080p")
|
|
if mainID != "137" && mainID != "247" {
|
|
t.Errorf("mainID = %q, want 137 or 247", mainID)
|
|
}
|
|
})
|
|
|
|
t.Run("prefer above 360p", func(t *testing.T) {
|
|
mainID, _ := autoSelectVideoFormat(formats, "360p")
|
|
// 480p (135) is the closest above 360p; below is none
|
|
if mainID != "135" {
|
|
t.Errorf("mainID = %q, want 135 (480p is closest above 360p)", mainID)
|
|
}
|
|
})
|
|
|
|
t.Run("exact match 720p", func(t *testing.T) {
|
|
mainID, _ := autoSelectVideoFormat(formats, "720p")
|
|
// 720p (136) is an exact match
|
|
if mainID != "136" {
|
|
t.Errorf("mainID = %q, want 136 (exact match for 720p)", mainID)
|
|
}
|
|
})
|
|
|
|
t.Run("above 1080p takes 1080p as closest below", func(t *testing.T) {
|
|
mainID, _ := autoSelectVideoFormat(formats, "2160p")
|
|
// No 4K format, should fall back to 1080p (closest below)
|
|
if mainID != "137" && mainID != "247" {
|
|
t.Errorf("mainID = %q, want 137 or 247 (1080p is closest below 2160p)", mainID)
|
|
}
|
|
})
|
|
|
|
t.Run("video-only picks best audio secondary", func(t *testing.T) {
|
|
formats := []domain.Format{
|
|
{ID: "137", HasVideo: true, HasAudio: false, Height: 1080},
|
|
{ID: "140", HasVideo: false, HasAudio: true, IsAudioOnly: true, Bitrate: "128k"},
|
|
}
|
|
_, secondaryID := autoSelectVideoFormat(formats, "1080p")
|
|
if secondaryID != "140" {
|
|
t.Errorf("secondaryID = %q, want 140", secondaryID)
|
|
}
|
|
})
|
|
|
|
t.Run("no match returns best", func(t *testing.T) {
|
|
formats := []domain.Format{
|
|
{ID: "140", HasVideo: false, HasAudio: true, IsAudioOnly: true},
|
|
}
|
|
mainID, _ := autoSelectVideoFormat(formats, "1080p")
|
|
if mainID != "best" {
|
|
t.Errorf("mainID = %q, want best", mainID)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestAutoSelectAudioFormat(t *testing.T) {
|
|
formats := []domain.Format{
|
|
{ID: "140", HasVideo: false, HasAudio: true, IsAudioOnly: true, Bitrate: "128k"},
|
|
{ID: "256", HasVideo: false, HasAudio: true, IsAudioOnly: true, Bitrate: "256k"},
|
|
{ID: "320", HasVideo: false, HasAudio: true, IsAudioOnly: true, Bitrate: "320k"},
|
|
}
|
|
|
|
t.Run("best returns best", func(t *testing.T) {
|
|
id := autoSelectAudioFormat(formats, "best")
|
|
if id != "best" {
|
|
t.Errorf("got %q, want best", id)
|
|
}
|
|
})
|
|
|
|
t.Run("empty returns best", func(t *testing.T) {
|
|
id := autoSelectAudioFormat(formats, "")
|
|
if id != "best" {
|
|
t.Errorf("got %q, want best", id)
|
|
}
|
|
})
|
|
|
|
t.Run("exact match 256k", func(t *testing.T) {
|
|
id := autoSelectAudioFormat(formats, "256k")
|
|
if id != "256" {
|
|
t.Errorf("got %q, want 256", id)
|
|
}
|
|
})
|
|
|
|
t.Run("prefer above 192k", func(t *testing.T) {
|
|
id := autoSelectAudioFormat(formats, "192k")
|
|
if id != "256" {
|
|
t.Errorf("got %q, want 256 (closest above 192k)", id)
|
|
}
|
|
})
|
|
|
|
t.Run("above highest takes highest below", func(t *testing.T) {
|
|
id := autoSelectAudioFormat(formats, "384k")
|
|
if id != "320" {
|
|
t.Errorf("got %q, want 320 (closest below 384k)", id)
|
|
}
|
|
})
|
|
|
|
t.Run("no match returns best", func(t *testing.T) {
|
|
formats := []domain.Format{
|
|
{ID: "1", HasVideo: true, HasAudio: true},
|
|
}
|
|
id := autoSelectAudioFormat(formats, "128k")
|
|
if id != "best" {
|
|
t.Errorf("got %q, want best", id)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestFormatLabelForDisplay(t *testing.T) {
|
|
tests := []struct {
|
|
f domain.Format
|
|
want string
|
|
}{
|
|
{
|
|
f: domain.Format{Resolution: "1920x1080", Filesize: 1048576},
|
|
want: "1920x1080 (1.0MB)",
|
|
},
|
|
{
|
|
f: domain.Format{Resolution: "1920x1080"},
|
|
want: "1920x1080",
|
|
},
|
|
{
|
|
f: domain.Format{Bitrate: "128k", Filesize: 1048576},
|
|
want: "128k (1.0MB)",
|
|
},
|
|
{
|
|
f: domain.Format{Bitrate: "128k"},
|
|
want: "128k",
|
|
},
|
|
{
|
|
f: domain.Format{ID: "137"},
|
|
want: "137",
|
|
},
|
|
{
|
|
f: domain.Format{IsAudioOnly: true, Bitrate: "128k", Filesize: 1048576, Resolution: "audio only"},
|
|
want: "128k (1.0MB)",
|
|
},
|
|
{
|
|
f: domain.Format{IsAudioOnly: true, Bitrate: "128k", Resolution: "audio only"},
|
|
want: "128k",
|
|
},
|
|
{
|
|
f: domain.Format{IsAudioOnly: true, ID: "140", Extension: "m4a", Filesize: 1048576, Resolution: "audio only"},
|
|
want: "140 (m4a) (1.0MB)",
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
got := formatLabelForDisplay(tt.f)
|
|
if got != tt.want {
|
|
t.Errorf("formatLabelForDisplay(%+v) = %q, want %q", tt.f, got, tt.want)
|
|
}
|
|
}
|
|
}
|