diff --git a/internal/download/downloader.go b/internal/download/downloader.go
index 7a548c2..cf29574 100644
--- a/internal/download/downloader.go
+++ b/internal/download/downloader.go
@@ -386,7 +386,7 @@ func isManifestResponse(contentType string, peek []byte) bool {
return true
}
s := strings.TrimSpace(strings.ToLower(string(peek)))
- if strings.HasPrefix(s, "")) {
t.Fatalf("expected MPD XML body to be manifest")
}
+ if !isManifestResponse("application/octet-stream", []byte("")) {
+ t.Fatalf("expected MPD body without xml prolog to be manifest")
+ }
if !isManifestResponse("text/plain", []byte("#EXTM3U\n#EXT-X-VERSION:3")) {
t.Fatalf("expected HLS body to be manifest")
}
@@ -158,3 +167,67 @@ func TestFileDeezerEncryptedTruncatedResponseRemovesPartialFile(t *testing.T) {
t.Fatalf("expected no partial file, stat err=%v", statErr)
}
}
+
+func TestFileDeezerEncryptedBadStatus(t *testing.T) {
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
+ w.WriteHeader(http.StatusForbidden)
+ }))
+ defer ts.Close()
+
+ d := NewWithOptions(true, false)
+ out := filepath.Join(t.TempDir(), "x", "a.flac")
+ err := d.FileDeezerEncrypted(context.Background(), ts.URL, out, "3135556")
+ if err == nil || !strings.Contains(err.Error(), "status=403") {
+ t.Fatalf("expected status error, got %v", err)
+ }
+}
+
+func TestDownloaderFileContextCancellationRemovesPartialFile(t *testing.T) {
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "application/octet-stream")
+ _, _ = w.Write([]byte("partial-data"))
+ if f, ok := w.(http.Flusher); ok {
+ f.Flush()
+ }
+ <-r.Context().Done()
+ }))
+ defer ts.Close()
+
+ d := NewWithOptions(true, false)
+ out := filepath.Join(t.TempDir(), "x", "cancel.bin")
+ ctx, cancel := context.WithTimeout(context.Background(), 60*time.Millisecond)
+ defer cancel()
+ err := d.File(ctx, ts.URL, out)
+ if err == nil {
+ t.Fatalf("expected context cancellation error")
+ }
+ if _, statErr := os.Stat(out); !errors.Is(statErr, os.ErrNotExist) {
+ t.Fatalf("expected no partial file, stat err=%v", statErr)
+ }
+}
+
+func TestStreamManifestWithFFmpegMissing(t *testing.T) {
+ d := NewWithOptions(true, false)
+ t.Setenv("PATH", "")
+ err := d.streamManifestWithFFmpeg(context.Background(), "https://example.com/live.m3u8", filepath.Join(t.TempDir(), "out.m4a"), false)
+ if err == nil || !strings.Contains(strings.ToLower(err.Error()), "ffmpeg not found") {
+ t.Fatalf("expected ffmpeg missing error, got %v", err)
+ }
+}
+
+func TestStreamManifestWithFFmpegFailureRemovesPartialFile(t *testing.T) {
+ if _, err := exec.LookPath("ffmpeg"); err != nil {
+ t.Skip("ffmpeg not installed")
+ }
+ d := NewWithOptions(true, false)
+ out := filepath.Join(t.TempDir(), "out.m4a")
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+ err := d.streamManifestWithFFmpeg(ctx, "https://127.0.0.1:1/unreachable.m3u8", out, false)
+ if err == nil || !strings.Contains(err.Error(), "ffmpeg stream copy failed") {
+ t.Fatalf("expected ffmpeg failure error, got %v", err)
+ }
+ if _, statErr := os.Stat(out); !errors.Is(statErr, os.ErrNotExist) {
+ t.Fatalf("expected no partial file after ffmpeg failure, stat err=%v", statErr)
+ }
+}