implement native Deezer download/decrypt pipeline

Replace Deezer yt-dlp usage with native ARL session + media.get_url resolution, add BF_CBC_STRIPE decryption in downloader, and wire cipher-aware Deezer downloads through the main rip pipeline. Includes validation hardening and metadata/source-id improvements used by tagging flows.
This commit is contained in:
2026-04-21 00:48:07 +02:00
parent 0ba8faa943
commit 26c9d50fac
10 changed files with 569 additions and 260 deletions

View File

@@ -3,7 +3,6 @@ package deezer
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"strings"
@@ -14,12 +13,11 @@ import (
func TestSearchTrack(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/search/track":
if r.URL.Path == "/search/track" {
_ = json.NewEncoder(w).Encode(map[string]any{"data": []any{map[string]any{"id": 1, "title": "Dreams", "artist": map[string]any{"name": "Fleetwood Mac"}}}})
default:
w.WriteHeader(http.StatusNotFound)
return
}
w.WriteHeader(http.StatusNotFound)
}))
defer ts.Close()
@@ -27,9 +25,9 @@ func TestSearchTrack(t *testing.T) {
c := New(&config.Config{File: cfgData, Session: cfgData})
c.loggedIn = true
orig := baseURL
origBase := baseURL
baseURL = ts.URL
defer func() { baseURL = orig }()
defer func() { baseURL = origBase }()
pages, err := c.Search(context.Background(), "track", "dreams", 5)
if err != nil {
@@ -40,11 +38,13 @@ func TestSearchTrack(t *testing.T) {
}
}
func TestGetDownloadableUsesPreview(t *testing.T) {
func TestGetDownloadableNativeCipher(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/track/42":
_ = json.NewEncoder(w).Encode(map[string]any{"id": 42, "title": "X", "preview": "https://cdn.example/p.mp3"})
_ = json.NewEncoder(w).Encode(map[string]any{"id": 42, "title": "X", "track_token": "tt"})
case "/media":
_ = json.NewEncoder(w).Encode(map[string]any{"data": []any{map[string]any{"errors": []any{}, "media": []any{map[string]any{"cipher": map[string]any{"type": "BF_CBC_STRIPE"}, "format": "FLAC", "sources": []any{map[string]any{"url": "https://cdn.example/file"}}}}}}})
default:
w.WriteHeader(http.StatusNotFound)
}
@@ -52,31 +52,48 @@ func TestGetDownloadableUsesPreview(t *testing.T) {
defer ts.Close()
cfgData := config.DefaultConfigData()
cfgData.Deezer.ARL = "arl"
c := New(&config.Config{File: cfgData, Session: cfgData})
c.loggedIn = true
orig := baseURL
baseURL = ts.URL
defer func() { baseURL = orig }()
c.arl = "arl"
c.license = "license"
d, err := c.GetDownloadable(context.Background(), "42", 0)
origBase := baseURL
origMedia := mediaURL
baseURL = ts.URL
mediaURL = ts.URL + "/media"
defer func() {
baseURL = origBase
mediaURL = origMedia
}()
d, err := c.GetDownloadable(context.Background(), "42", 2)
if err != nil {
t.Fatalf("GetDownloadable() error = %v", err)
}
if d.URL != "https://cdn.example/p.mp3" || d.Extension != "mp3" {
if d.Cipher != "BF_CBC_STRIPE" || d.Extension != "flac" || d.TrackID != "42" {
t.Fatalf("unexpected downloadable: %+v", d)
}
}
func TestGetMetadataSetsExplicitFromBool(t *testing.T) {
func TestGetDownloadableRequiresARL(t *testing.T) {
cfgData := config.DefaultConfigData()
cfgData.Deezer.ARL = ""
c := New(&config.Config{File: cfgData, Session: cfgData})
c.loggedIn = true
_, err := c.GetDownloadable(context.Background(), "42", 2)
if err == nil || !strings.Contains(strings.ToLower(err.Error()), "arl") {
t.Fatalf("expected arl requirement error, got %v", err)
}
}
func TestGetDownloadableDRMError(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/track/9":
_ = json.NewEncoder(w).Encode(map[string]any{
"id": 9,
"title": "X",
"explicit_lyrics": true,
"artist": map[string]any{"name": "Artist"},
})
case "/track/42":
_ = json.NewEncoder(w).Encode(map[string]any{"id": 42, "title": "X", "track_token": "tt"})
case "/media":
_ = json.NewEncoder(w).Encode(map[string]any{"data": []any{map[string]any{"errors": []any{map[string]any{"code": 403, "message": "DRM required"}}, "media": []any{}}}})
default:
w.WriteHeader(http.StatusNotFound)
}
@@ -84,69 +101,23 @@ func TestGetMetadataSetsExplicitFromBool(t *testing.T) {
defer ts.Close()
cfgData := config.DefaultConfigData()
cfgData.Deezer.ARL = "arl"
c := New(&config.Config{File: cfgData, Session: cfgData})
c.loggedIn = true
orig := baseURL
c.arl = "arl"
c.license = "license"
origBase := baseURL
origMedia := mediaURL
baseURL = ts.URL
defer func() { baseURL = orig }()
meta, err := c.GetMetadata(context.Background(), "9", "track")
if err != nil {
t.Fatalf("GetMetadata() error = %v", err)
}
if explicit, _ := meta["explicit"].(bool); !explicit {
t.Fatalf("expected explicit=true, got %#v", meta["explicit"])
}
}
func TestSearchReturnsStructuredAPIError(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/search/track" {
_ = json.NewEncoder(w).Encode(map[string]any{"error": map[string]any{"message": "invalid query"}})
return
}
w.WriteHeader(http.StatusNotFound)
}))
defer ts.Close()
cfgData := config.DefaultConfigData()
c := New(&config.Config{File: cfgData, Session: cfgData})
c.loggedIn = true
orig := baseURL
baseURL = ts.URL
defer func() { baseURL = orig }()
_, err := c.Search(context.Background(), "track", "", 5)
if err == nil || !strings.Contains(err.Error(), "invalid query") {
t.Fatalf("expected structured deezer error, got %v", err)
}
}
func TestGetDownloadableErrorsWhenFullQualityFailsAndFallbackDisabled(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/track/42" {
_ = json.NewEncoder(w).Encode(map[string]any{"id": 42, "title": "X", "preview": "https://cdn.example/p.mp3"})
return
}
w.WriteHeader(http.StatusNotFound)
}))
defer ts.Close()
cfgData := config.DefaultConfigData()
cfgData.Deezer.UseDeezloader = true
cfgData.Deezer.LowerQualityIfNotAvailable = false
c := New(&config.Config{File: cfgData, Session: cfgData})
c.loggedIn = true
c.bin = "definitely-not-a-real-yt-dlp-bin"
c.run = func(context.Context, string, ...string) ([]byte, error) {
return nil, fmt.Errorf("unexpected run call")
}
orig := baseURL
baseURL = ts.URL
defer func() { baseURL = orig }()
mediaURL = ts.URL + "/media"
defer func() {
baseURL = origBase
mediaURL = origMedia
}()
_, err := c.GetDownloadable(context.Background(), "42", 2)
if err == nil || !strings.Contains(err.Error(), "full-quality mode failed") {
t.Fatalf("expected full-quality failure error, got %v", err)
if err == nil || !strings.Contains(strings.ToLower(err.Error()), "drm") {
t.Fatalf("expected drm error, got %v", err)
}
}