2 Commits
Author SHA1 Message Date
Joren eb7854bac3 fix: use all album artists for folder naming 2026-07-22 22:34:42 +02:00
Joren 5f61b1a3cf fix: use exact qobuz download quality 2026-07-12 21:44:01 +02:00
3 changed files with 103 additions and 16 deletions
+41 -8
View File
@@ -648,10 +648,7 @@ func (m *Main) ripAlbum(ctx context.Context, p provider.Client, source, albumID
} }
albumTitle := titleFromMetadata(albumMeta, albumID) albumTitle := titleFromMetadata(albumMeta, albumID)
albumArtist := jsonutil.NestedString(albumMeta, "artist", "name") albumArtist := extractAlbumArtist(albumMeta)
if albumArtist == "" {
albumArtist = "Unknown"
}
releaseDate := jsonutil.StringFromAny(albumMeta["release_date_original"]) releaseDate := jsonutil.StringFromAny(albumMeta["release_date_original"])
if releaseDate == "" { if releaseDate == "" {
releaseDate = jsonutil.StringFromAny(albumMeta["release_date"]) releaseDate = jsonutil.StringFromAny(albumMeta["release_date"])
@@ -1224,10 +1221,7 @@ func (m *Main) trackOutputPath(source, id, title, ext string, d *provider.Downlo
if albumID == "" { if albumID == "" {
albumID = id albumID = id
} }
albumArtist := jsonutil.NestedString(trackMeta, "album", "artist", "name") albumArtist := extractAlbumArtist(trackMetaAlbum(trackMeta))
if albumArtist == "" {
albumArtist = jsonutil.NestedString(trackMeta, "performer", "name")
}
albumYear := naming.YearFromDate(jsonutil.StringFromAny(trackMeta["release_date_original"])) albumYear := naming.YearFromDate(jsonutil.StringFromAny(trackMeta["release_date_original"]))
if albumYear == "Unknown" { if albumYear == "Unknown" {
albumYear = naming.YearFromDate(jsonutil.StringFromAny(trackMeta["release_date"])) albumYear = naming.YearFromDate(jsonutil.StringFromAny(trackMeta["release_date"]))
@@ -1613,3 +1607,42 @@ func isFFmpegMissingError(err error) bool {
} }
return strings.Contains(strings.ToLower(err.Error()), "ffmpeg not found") return strings.Contains(strings.ToLower(err.Error()), "ffmpeg not found")
} }
func extractAlbumArtist(albumMeta map[string]any) string {
if artistsRaw, ok := albumMeta["artists"].([]any); ok {
names := make([]string, 0, len(artistsRaw))
for _, a := range artistsRaw {
artist, ok := a.(map[string]any)
if !ok {
continue
}
if name := jsonutil.StringFromAny(artist["name"]); name != "" {
names = append(names, name)
}
}
if len(names) > 0 {
return displayArtistNames(names)
}
}
if names := stringSliceFromAny(albumMeta["artist_names"]); len(names) > 0 {
return displayArtistNames(names)
}
artist := jsonutil.NestedString(albumMeta, "artist", "name")
if artist != "" {
return artist
}
return "Unknown"
}
func displayArtistNames(names []string) string {
switch len(names) {
case 0:
return ""
case 1:
return names[0]
case 2:
return names[0] + " & " + names[1]
default:
return strings.Join(names[:len(names)-1], ", ") + " & " + names[len(names)-1]
}
}
+44 -8
View File
@@ -364,40 +364,60 @@ func qobuzDownloadExtension(resp map[string]any, quality int, streamURL string)
} }
func qobuzAudioProfile(resp map[string]any, requestedQuality int, ext string) provider.AudioProfile { func qobuzAudioProfile(resp map[string]any, requestedQuality int, ext string) provider.AudioProfile {
exactBitDepth, _ := intValue(firstNonNil(resp["bit_depth"], resp["bits_depth"], resp["maximum_bit_depth"]))
exactSampling, _ := floatValue(firstNonNil(resp["sampling_rate"], resp["sample_rate"], resp["maximum_sampling_rate"]))
if formatID, ok := intValue(resp["format_id"]); ok { if formatID, ok := intValue(resp["format_id"]); ok {
switch formatID { switch formatID {
case 5: case 5:
if exactBitDepth == 0 {
exactBitDepth = 16
}
if exactSampling == 0 {
exactSampling = 44.1
}
return provider.AudioProfile{ return provider.AudioProfile{
Container: "MP3", Container: "MP3",
Codec: "MP3", Codec: "MP3",
Quality: "HIGH", Quality: "HIGH",
BitDepth: 16, BitDepth: exactBitDepth,
SamplingRate: "44.1", SamplingRate: formatSamplingRate(exactSampling),
BitrateKbps: 320, BitrateKbps: 320,
} }
case 6: case 6:
if exactBitDepth == 0 {
exactBitDepth = 16
}
if exactSampling == 0 {
exactSampling = 44.1
}
return provider.AudioProfile{ return provider.AudioProfile{
Container: "FLAC", Container: "FLAC",
Codec: "FLAC", Codec: "FLAC",
Quality: "LOSSLESS", Quality: "LOSSLESS",
BitDepth: 16, BitDepth: exactBitDepth,
SamplingRate: "44.1", SamplingRate: formatSamplingRate(exactSampling),
} }
case 7: case 7:
if exactBitDepth == 0 {
exactBitDepth = 24
}
return provider.AudioProfile{ return provider.AudioProfile{
Container: "FLAC", Container: "FLAC",
Codec: "FLAC", Codec: "FLAC",
Quality: "HI_RES", Quality: "HI_RES",
BitDepth: 24, BitDepth: exactBitDepth,
SamplingRate: "96", SamplingRate: formatSamplingRate(exactSampling),
} }
case 27: case 27:
if exactBitDepth == 0 {
exactBitDepth = 24
}
return provider.AudioProfile{ return provider.AudioProfile{
Container: "FLAC", Container: "FLAC",
Codec: "FLAC", Codec: "FLAC",
Quality: "HI_RES", Quality: "HI_RES",
BitDepth: 24, BitDepth: exactBitDepth,
SamplingRate: "192", SamplingRate: formatSamplingRate(exactSampling),
} }
} }
} }
@@ -438,6 +458,22 @@ func qobuzAudioProfile(resp map[string]any, requestedQuality int, ext string) pr
} }
} }
func firstNonNil(vals ...any) any {
for _, v := range vals {
if v != nil {
return v
}
}
return nil
}
func formatSamplingRate(v float64) string {
if v <= 0 {
return ""
}
return strconv.FormatFloat(v, 'f', -1, 64)
}
func (c *Client) Close() error { func (c *Client) Close() error {
return nil return nil
} }
+18
View File
@@ -371,6 +371,24 @@ func TestGetDownloadableUsesReturnedURLExtension(t *testing.T) {
} }
} }
func TestQobuzAudioProfileUsesExactReturnedQuality(t *testing.T) {
profile := qobuzAudioProfile(map[string]any{
"format_id": float64(7),
"bit_depth": float64(24),
"sampling_rate": float64(44.1),
}, 4, "flac")
if profile.BitDepth != 24 || profile.SamplingRate != "44.1" || profile.Quality != "HI_RES" {
t.Fatalf("unexpected profile: %+v", profile)
}
}
func TestQobuzAudioProfileAvoidsGuessingHiResSampleRate(t *testing.T) {
profile := qobuzAudioProfile(map[string]any{"format_id": float64(7)}, 4, "flac")
if profile.BitDepth != 24 || profile.SamplingRate != "" || profile.Quality != "HI_RES" {
t.Fatalf("unexpected profile: %+v", profile)
}
}
func qobuzSecretSig(requestTS, secret string) string { func qobuzSecretSig(requestTS, secret string) string {
raw := "trackgetFileUrlformat_id27intentstreamtrack_id19512574" + requestTS + secret raw := "trackgetFileUrlformat_id27intentstreamtrack_id19512574" + requestTS + secret
hash := md5.Sum([]byte(raw)) hash := md5.Sum([]byte(raw))