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) + } +}