feat: add thumbnail preview, audio-only detection, /download command, tests, and bugfixes
- Add thumbnail preview with metadata (title, uploader, duration) before download - Auto-detect audio-only sites (Spotify, SoundCloud, etc.) and skip type selection - Add /download command for feature parity with UI button - Fix yt-dlp defer bug: StatusFailed was overwritten to StatusCompleted - Fix handleURLInput using editText with msgID=0 (now uses sendWithKB) - Fix filename detection: use job ID prefix for reliable file finding - Fix double answerCb between handleBackStep and handleDownloadPrompt - Fix runDownload unused msgID parameter - Fix context for download goroutine (use context.Background) - Remove unused startWebhook h parameter - Remove dead buildToggleKeyboard function - Add formatBytes, formatDuration, formatLabelForDisplay tests - Add IsAudioOnlyContent, MediaType.String tests - Add repo integration tests (users, preferences, quotas, history, delete) - Add cmd/bot tests (parseAllowedUsers, env helpers) - Add edge case tests for progress parser - Add zero/negative safety guard in formatDuration - Add .gitea, TODO.md, opencode.json to gitignore
This commit is contained in:
@@ -45,6 +45,8 @@ type Format struct {
|
||||
type MediaInfo struct {
|
||||
URL string
|
||||
Title string
|
||||
Uploader string
|
||||
Thumbnail string
|
||||
Duration float64
|
||||
Formats []Format
|
||||
Languages []string
|
||||
@@ -54,6 +56,16 @@ type MediaInfo struct {
|
||||
EstimatedSize int64
|
||||
}
|
||||
|
||||
// IsAudioOnlyContent returns true when no format carries a video stream.
|
||||
func (m *MediaInfo) IsAudioOnlyContent() bool {
|
||||
for _, f := range m.Formats {
|
||||
if f.HasVideo {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return len(m.Formats) > 0
|
||||
}
|
||||
|
||||
// PlaylistEntry represents a single entry in a playlist.
|
||||
type PlaylistEntry struct {
|
||||
ID string
|
||||
|
||||
90
internal/domain/types_test.go
Normal file
90
internal/domain/types_test.go
Normal file
@@ -0,0 +1,90 @@
|
||||
package domain
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestMediaTypeString(t *testing.T) {
|
||||
tests := []struct {
|
||||
mt MediaType
|
||||
want string
|
||||
}{
|
||||
{MediaTypeAudio, "audio"},
|
||||
{MediaTypeVideo, "video"},
|
||||
{MediaTypeVideoAudio, "video_audio"},
|
||||
{MediaType(0), "unknown"},
|
||||
{MediaType(99), "unknown"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
if got := tt.mt.String(); got != tt.want {
|
||||
t.Errorf("MediaType(%d).String() = %q, want %q", tt.mt, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsAudioOnlyContent(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
media *MediaInfo
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "empty formats returns false",
|
||||
media: &MediaInfo{Formats: nil},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "all audio formats returns true",
|
||||
media: &MediaInfo{
|
||||
Formats: []Format{
|
||||
{ID: "1", HasVideo: false, HasAudio: true, IsAudioOnly: true},
|
||||
{ID: "2", HasVideo: false, HasAudio: true, IsAudioOnly: true},
|
||||
},
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "mixed formats returns false",
|
||||
media: &MediaInfo{
|
||||
Formats: []Format{
|
||||
{ID: "1", HasVideo: false, HasAudio: true, IsAudioOnly: true},
|
||||
{ID: "2", HasVideo: true, HasAudio: true},
|
||||
},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "single format with no video returns true",
|
||||
media: &MediaInfo{
|
||||
Formats: []Format{
|
||||
{ID: "1", HasVideo: false, HasAudio: true, IsAudioOnly: true},
|
||||
},
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "format with both video and audio returns false",
|
||||
media: &MediaInfo{
|
||||
Formats: []Format{
|
||||
{ID: "1", HasVideo: true, HasAudio: true},
|
||||
},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "only audio and no-video-no-audio formats returns true",
|
||||
media: &MediaInfo{
|
||||
Formats: []Format{
|
||||
{ID: "1", HasVideo: false, HasAudio: true, IsAudioOnly: true},
|
||||
{ID: "2", HasVideo: false, HasAudio: false},
|
||||
},
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := tt.media.IsAudioOnlyContent(); got != tt.want {
|
||||
t.Errorf("IsAudioOnlyContent() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user