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

@@ -106,6 +106,7 @@ func (c *Client) searchTracks(ctx context.Context, query string, limit int) ([]m
if artist == "" {
artist = strings.TrimSpace(stringFromAny(m["channel"]))
}
artistID := strings.TrimSpace(firstNonEmpty(stringFromAny(m["uploader_id"]), stringFromAny(m["channel_id"])))
item := map[string]any{
"id": id,
"title": stringFromAny(m["title"]),
@@ -113,6 +114,9 @@ func (c *Client) searchTracks(ctx context.Context, query string, limit int) ([]m
"name": artist,
},
}
if artistID != "" {
item["artist"] = map[string]any{"name": artist, "id": artistID}
}
if trackID := strings.TrimSpace(stringFromAny(m["id"])); trackID != "" {
item["source_track_id"] = trackID
}
@@ -164,6 +168,7 @@ func (c *Client) searchPlaylists(ctx context.Context, query string, limit int) (
title = strings.Trim(strings.ReplaceAll(path, "/", " "), " ")
}
artist := strings.TrimSpace(firstNonEmpty(stringFromAny(info["uploader"]), stringFromAny(info["channel"])))
artistID := strings.TrimSpace(firstNonEmpty(stringFromAny(info["uploader_id"]), stringFromAny(info["channel_id"])))
trackCount := 0
if entries := asAnySlice(info["entries"]); len(entries) > 0 {
trackCount = len(entries)
@@ -175,11 +180,14 @@ func (c *Client) searchPlaylists(ctx context.Context, query string, limit int) (
"tracks_count": trackCount,
"artist": map[string]any{"name": artist},
}
if artistID != "" {
item["artist"] = map[string]any{"name": artist, "id": artistID}
}
if pid := strings.TrimSpace(stringFromAny(info["id"])); pid != "" {
item["source_playlist_id"] = pid
}
if thumb := strings.TrimSpace(stringFromAny(info["thumbnail"])); thumb != "" {
item["image"] = map[string]any{"small": thumb, "large": thumb, "extralarge": thumb, "original": thumb}
item["image"] = soundcloudImageMap(thumb)
}
items = append(items, item)
if len(items) >= limit {
@@ -227,7 +235,11 @@ func (c *Client) GetMetadata(ctx context.Context, item, mediaType string) (map[s
track["title"] = title
}
if artist := strings.TrimSpace(firstNonEmpty(stringFromAny(entry["uploader"]), stringFromAny(entry["channel"]))); artist != "" {
track["artist"] = map[string]any{"name": artist}
artistMap := map[string]any{"name": artist}
if artistID := strings.TrimSpace(firstNonEmpty(stringFromAny(entry["uploader_id"]), stringFromAny(entry["channel_id"]))); artistID != "" {
artistMap["id"] = artistID
}
track["artist"] = artistMap
}
track["track_number"] = i + 1
tracks = append(tracks, track)
@@ -249,7 +261,7 @@ func (c *Client) GetMetadata(ctx context.Context, item, mediaType string) (map[s
meta["artist"] = map[string]any{"name": artist}
}
if thumb := strings.TrimSpace(stringFromAny(root["thumbnail"])); thumb != "" {
meta["image"] = map[string]any{"small": thumb, "large": thumb, "extralarge": thumb, "original": thumb}
meta["image"] = soundcloudImageMap(thumb)
}
if entries := asAnySlice(root["entries"]); len(entries) > 0 {
meta["tracks_count"] = len(entries)
@@ -326,17 +338,33 @@ func (c *Client) playlistInfo(ctx context.Context, item string) (map[string]any,
func trackMetadataFromInfo(id string, info map[string]any) map[string]any {
canonicalID := firstNonEmpty(canonicalSoundcloudURL(info), id)
publisher := nestedMap(info, "publisher_metadata")
title := strings.TrimSpace(stringFromAny(info["title"]))
if title == "" {
title = canonicalID
}
albumTitle := strings.TrimSpace(stringFromAny(publisher["album_title"]))
if albumTitle == "" {
albumTitle = strings.TrimSpace(stringFromAny(info["album"]))
}
if albumTitle == "" {
albumTitle = title
}
artistName := strings.TrimSpace(stringFromAny(info["artist"]))
if artistName == "" {
artistName = strings.TrimSpace(stringFromAny(publisher["artist"]))
}
if artistName == "" {
artistName = strings.TrimSpace(stringFromAny(info["uploader"]))
}
if artistName == "" {
artistName = strings.TrimSpace(stringFromAny(info["channel"]))
}
artistID := strings.TrimSpace(firstNonEmpty(
stringFromAny(info["uploader_id"]),
stringFromAny(info["channel_id"]),
stringFromAny(nestedMap(info, "user")["id"]),
))
trackNum := intFromAny(info["track_number"])
if trackNum <= 0 {
@@ -347,18 +375,20 @@ func trackMetadataFromInfo(id string, info map[string]any) map[string]any {
"id": canonicalID,
"title": title,
"track_number": trackNum,
"artist": map[string]any{"name": artistName},
"performer": map[string]any{"name": artistName},
"artist": map[string]any{"name": artistName, "id": artistID},
"performer": map[string]any{"name": artistName, "id": artistID},
"album": map[string]any{
"id": strings.TrimSpace(stringFromAny(info["album"])),
"title": strings.TrimSpace(stringFromAny(info["album"])),
"artist": map[string]any{"name": artistName},
"id": firstNonEmpty(strings.TrimSpace(stringFromAny(info["album"])), canonicalID),
"title": albumTitle,
"artist": map[string]any{"name": artistName, "id": artistID},
},
"description": strings.TrimSpace(stringFromAny(info["description"])),
"genre": strings.TrimSpace(stringFromAny(info["genre"])),
"isrc": strings.TrimSpace(stringFromAny(info["isrc"])),
"label": strings.TrimSpace(stringFromAny(info["label"])),
"label": strings.TrimSpace(firstNonEmpty(stringFromAny(info["label"]), stringFromAny(info["label_name"]))),
"copyright": strings.TrimSpace(stringFromAny(publisher["p_line"])),
"release_date": strings.TrimSpace(firstNonEmpty(
stringFromAny(info["created_at"]),
stringFromAny(info["release_date"]),
stringFromAny(info["upload_date"]),
)),
@@ -367,7 +397,7 @@ func trackMetadataFromInfo(id string, info map[string]any) map[string]any {
meta["source_track_id"] = trackID
}
if age := intFromAny(info["age_limit"]); age >= 18 {
if boolFromAny(publisher["explicit"]) || intFromAny(info["age_limit"]) >= 18 {
meta["explicit"] = true
}
@@ -376,19 +406,14 @@ func trackMetadataFromInfo(id string, info map[string]any) map[string]any {
}
if thumb := strings.TrimSpace(stringFromAny(info["thumbnail"])); thumb != "" {
meta["image"] = map[string]any{
"small": thumb,
"large": thumb,
"extralarge": thumb,
"original": thumb,
}
meta["image"] = soundcloudImageMap(thumb)
}
if album := strings.TrimSpace(stringFromAny(info["album"])); album == "" {
if strings.TrimSpace(stringFromAny(info["album"])) == "" && strings.TrimSpace(stringFromAny(publisher["album_title"])) == "" {
meta["album"] = map[string]any{
"id": id,
"id": canonicalID,
"title": title,
"artist": map[string]any{"name": artistName},
"artist": map[string]any{"name": artistName, "id": artistID},
}
}
@@ -492,6 +517,49 @@ func firstNonEmpty(items ...string) string {
return ""
}
func nestedMap(m map[string]any, key string) map[string]any {
v, ok := m[key].(map[string]any)
if !ok {
return map[string]any{}
}
return v
}
func boolFromAny(v any) bool {
switch t := v.(type) {
case bool:
return t
case string:
l := strings.ToLower(strings.TrimSpace(t))
return l == "1" || l == "true" || l == "yes"
case int:
return t != 0
case int64:
return t != 0
case float64:
return t != 0
default:
return false
}
}
func soundcloudImageMap(raw string) map[string]any {
base := strings.TrimSpace(raw)
if base == "" {
return map[string]any{}
}
large := strings.Replace(base, "-large.", "-t500x500.", 1)
if large == base {
large = strings.Replace(base, "large", "t500x500", 1)
}
return map[string]any{
"small": base,
"large": large,
"extralarge": large,
"original": large,
}
}
func runCommand(ctx context.Context, name string, args ...string) ([]byte, error) {
cmd := exec.CommandContext(ctx, name, args...)
b, err := cmd.CombinedOutput()

View File

@@ -70,6 +70,9 @@ func TestGetPlaylistMetadata(t *testing.T) {
if len(items) != 2 {
t.Fatalf("playlist items len = %d, want 2", len(items))
}
if stringFromAny(meta["id"]) != "https://soundcloud.com/a/sets/road-trip" {
t.Fatalf("playlist id not canonical: %q", stringFromAny(meta["id"]))
}
}
func TestSearchTrack(t *testing.T) {
@@ -95,6 +98,13 @@ func TestSearchTrack(t *testing.T) {
if len(items) != 1 {
t.Fatalf("items len = %d, want 1", len(items))
}
item0, ok := items[0].(map[string]any)
if !ok {
t.Fatalf("expected first item map")
}
if stringFromAny(item0["id"]) != "https://soundcloud.com/a/b" {
t.Fatalf("track search id not canonical: %q", stringFromAny(item0["id"]))
}
}
func TestSearchPlaylist(t *testing.T) {
@@ -133,6 +143,13 @@ func TestSearchPlaylist(t *testing.T) {
if len(items) != 1 {
t.Fatalf("items len = %d, want 1", len(items))
}
item0, ok := items[0].(map[string]any)
if !ok {
t.Fatalf("expected first item map")
}
if stringFromAny(item0["id"]) != "https://soundcloud.com/a/sets/road-trip" {
t.Fatalf("playlist search id not canonical: %q", stringFromAny(item0["id"]))
}
}
func TestLoginShowsYtDlpHint(t *testing.T) {
@@ -169,6 +186,9 @@ func TestTrackMetadataIncludesExplicitAndISRC(t *testing.T) {
if stringFromAny(meta["source_track_id"]) != "9876" {
t.Fatalf("source_track_id = %q, want 9876", stringFromAny(meta["source_track_id"]))
}
if stringFromAny(nestedMap(meta, "album")["title"]) != "T" {
t.Fatalf("album title mismatch: %#v", nestedMap(meta, "album"))
}
}
func TestCanonicalSoundcloudURL(t *testing.T) {