mirror of
https://git.sr.ht/~joren/streamrip-go
synced 2026-06-17 15:05:39 +02:00
unify folder naming with resolved audio profiles across providers
This commit is contained in:
@@ -308,7 +308,14 @@ func (c *Client) GetDownloadable(ctx context.Context, item string, _ int) (*prov
|
||||
if trackID == "" {
|
||||
trackID = strings.TrimSpace(item)
|
||||
}
|
||||
return &provider.Downloadable{URL: media.URL, Extension: ext, Source: "deezer", Cipher: media.Cipher, TrackID: trackID}, nil
|
||||
return &provider.Downloadable{
|
||||
URL: media.URL,
|
||||
Extension: ext,
|
||||
Source: "deezer",
|
||||
Cipher: media.Cipher,
|
||||
TrackID: trackID,
|
||||
Audio: audioProfileForFormat(media.Format),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Client) apiGet(ctx context.Context, path string, params url.Values) (map[string]any, error) {
|
||||
@@ -1223,6 +1230,55 @@ func extensionForFormat(format string) string {
|
||||
}
|
||||
}
|
||||
|
||||
func audioProfileForFormat(format string) provider.AudioProfile {
|
||||
profile := provider.AudioProfile{}
|
||||
switch strings.ToUpper(strings.TrimSpace(format)) {
|
||||
case "FLAC":
|
||||
profile.Container = "FLAC"
|
||||
profile.Codec = "FLAC"
|
||||
profile.Quality = "LOSSLESS"
|
||||
profile.BitDepth = 16
|
||||
profile.SamplingRate = "44.1"
|
||||
case "MP3_320":
|
||||
profile.Container = "MP3"
|
||||
profile.Codec = "MP3"
|
||||
profile.Quality = "HIGH"
|
||||
profile.BitrateKbps = 320
|
||||
profile.BitDepth = 16
|
||||
profile.SamplingRate = "44.1"
|
||||
case "MP3_128":
|
||||
profile.Container = "MP3"
|
||||
profile.Codec = "MP3"
|
||||
profile.Quality = "LOW"
|
||||
profile.BitrateKbps = 128
|
||||
profile.BitDepth = 16
|
||||
profile.SamplingRate = "44.1"
|
||||
case "MP3_64", "MP3_MISC":
|
||||
profile.Container = "MP3"
|
||||
profile.Codec = "MP3"
|
||||
profile.Quality = "LOW"
|
||||
profile.BitrateKbps = 64
|
||||
profile.BitDepth = 16
|
||||
profile.SamplingRate = "44.1"
|
||||
default:
|
||||
if ext := extensionForFormat(format); ext == "flac" {
|
||||
profile.Container = "FLAC"
|
||||
profile.Codec = "FLAC"
|
||||
profile.Quality = "LOSSLESS"
|
||||
profile.BitDepth = 16
|
||||
profile.SamplingRate = "44.1"
|
||||
} else {
|
||||
profile.Container = "MP3"
|
||||
profile.Codec = "MP3"
|
||||
profile.Quality = "LOW"
|
||||
profile.BitrateKbps = 128
|
||||
profile.BitDepth = 16
|
||||
profile.SamplingRate = "44.1"
|
||||
}
|
||||
}
|
||||
return profile
|
||||
}
|
||||
|
||||
func findStringByKey(v any, wantedKey string) string {
|
||||
w := strings.ToLower(strings.TrimSpace(wantedKey))
|
||||
switch x := v.(type) {
|
||||
|
||||
@@ -139,6 +139,9 @@ func TestGetDownloadableNativeCipher(t *testing.T) {
|
||||
if d.Cipher != "BF_CBC_STRIPE" || d.Extension != "flac" || d.TrackID != "42" {
|
||||
t.Fatalf("unexpected downloadable: %+v", d)
|
||||
}
|
||||
if d.Audio.Container != "FLAC" || d.Audio.Quality != "LOSSLESS" {
|
||||
t.Fatalf("unexpected audio profile: %+v", d.Audio)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetDownloadableRequiresARL(t *testing.T) {
|
||||
|
||||
@@ -8,6 +8,16 @@ type Downloadable struct {
|
||||
Source string
|
||||
Cipher string
|
||||
TrackID string
|
||||
Audio AudioProfile
|
||||
}
|
||||
|
||||
type AudioProfile struct {
|
||||
Container string
|
||||
Codec string
|
||||
Quality string
|
||||
BitDepth int
|
||||
SamplingRate string
|
||||
BitrateKbps int
|
||||
}
|
||||
|
||||
type Client interface {
|
||||
|
||||
@@ -275,11 +275,13 @@ func (c *Client) GetDownloadable(ctx context.Context, item string, quality int)
|
||||
}
|
||||
|
||||
ext := qobuzDownloadExtension(resp, quality, streamURL)
|
||||
profile := qobuzAudioProfile(resp, quality, ext)
|
||||
|
||||
return &provider.Downloadable{
|
||||
URL: streamURL,
|
||||
Extension: ext,
|
||||
Source: "qobuz",
|
||||
Audio: profile,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -318,6 +320,81 @@ func qobuzDownloadExtension(resp map[string]any, quality int, streamURL string)
|
||||
return "mp3"
|
||||
}
|
||||
|
||||
func qobuzAudioProfile(resp map[string]any, requestedQuality int, ext string) provider.AudioProfile {
|
||||
if formatID, ok := intValue(resp["format_id"]); ok {
|
||||
switch formatID {
|
||||
case 5:
|
||||
return provider.AudioProfile{
|
||||
Container: "MP3",
|
||||
Codec: "MP3",
|
||||
Quality: "HIGH",
|
||||
BitDepth: 16,
|
||||
SamplingRate: "44.1",
|
||||
BitrateKbps: 320,
|
||||
}
|
||||
case 6:
|
||||
return provider.AudioProfile{
|
||||
Container: "FLAC",
|
||||
Codec: "FLAC",
|
||||
Quality: "LOSSLESS",
|
||||
BitDepth: 16,
|
||||
SamplingRate: "44.1",
|
||||
}
|
||||
case 7:
|
||||
return provider.AudioProfile{
|
||||
Container: "FLAC",
|
||||
Codec: "FLAC",
|
||||
Quality: "HI_RES",
|
||||
BitDepth: 24,
|
||||
SamplingRate: "96",
|
||||
}
|
||||
case 27:
|
||||
return provider.AudioProfile{
|
||||
Container: "FLAC",
|
||||
Codec: "FLAC",
|
||||
Quality: "HI_RES",
|
||||
BitDepth: 24,
|
||||
SamplingRate: "192",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if strings.EqualFold(ext, "mp3") {
|
||||
bitrate := 128
|
||||
if requestedQuality >= 1 {
|
||||
bitrate = 320
|
||||
}
|
||||
return provider.AudioProfile{
|
||||
Container: "MP3",
|
||||
Codec: "MP3",
|
||||
Quality: "HIGH",
|
||||
BitDepth: 16,
|
||||
SamplingRate: "44.1",
|
||||
BitrateKbps: bitrate,
|
||||
}
|
||||
}
|
||||
|
||||
quality := "LOSSLESS"
|
||||
bitDepth := 16
|
||||
sampling := "44.1"
|
||||
if requestedQuality >= 4 {
|
||||
quality = "HI_RES"
|
||||
bitDepth = 24
|
||||
sampling = "192"
|
||||
} else if requestedQuality >= 3 {
|
||||
quality = "HI_RES"
|
||||
bitDepth = 24
|
||||
sampling = "96"
|
||||
}
|
||||
return provider.AudioProfile{
|
||||
Container: "FLAC",
|
||||
Codec: "FLAC",
|
||||
Quality: quality,
|
||||
BitDepth: bitDepth,
|
||||
SamplingRate: sampling,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -366,6 +366,9 @@ func TestGetDownloadableUsesReturnedURLExtension(t *testing.T) {
|
||||
if d.Extension != "mp3" {
|
||||
t.Fatalf("extension = %q, want mp3", d.Extension)
|
||||
}
|
||||
if d.Audio.Container != "MP3" || d.Audio.Codec != "MP3" {
|
||||
t.Fatalf("unexpected audio profile: %+v", d.Audio)
|
||||
}
|
||||
}
|
||||
|
||||
func qobuzSecretSig(requestTS, secret string) string {
|
||||
|
||||
@@ -288,13 +288,30 @@ func (c *Client) GetDownloadable(ctx context.Context, item string, _ int) (*prov
|
||||
if ext == "" {
|
||||
ext = "m4a"
|
||||
}
|
||||
return &provider.Downloadable{URL: streamURL, Extension: ext, Source: "soundcloud"}, nil
|
||||
return &provider.Downloadable{URL: streamURL, Extension: ext, Source: "soundcloud", Audio: soundcloudAudioProfile(ext)}, nil
|
||||
}
|
||||
|
||||
func (c *Client) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func soundcloudAudioProfile(ext string) provider.AudioProfile {
|
||||
switch strings.ToLower(strings.TrimSpace(ext)) {
|
||||
case "mp3":
|
||||
return provider.AudioProfile{Container: "MP3", Codec: "MP3", Quality: "LOSSY", BitDepth: 16, SamplingRate: "44.1"}
|
||||
case "flac":
|
||||
return provider.AudioProfile{Container: "FLAC", Codec: "FLAC", Quality: "LOSSLESS", BitDepth: 16, SamplingRate: "44.1"}
|
||||
case "m4a", "aac":
|
||||
return provider.AudioProfile{Container: "M4A", Codec: "AAC", Quality: "LOSSY", BitDepth: 16, SamplingRate: "44.1"}
|
||||
default:
|
||||
container := strings.ToUpper(strings.TrimSpace(ext))
|
||||
if container == "" {
|
||||
container = "M4A"
|
||||
}
|
||||
return provider.AudioProfile{Container: container, Codec: container, Quality: "LOSSY", BitDepth: 16, SamplingRate: "44.1"}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) trackInfo(ctx context.Context, item string) (map[string]any, error) {
|
||||
if strings.TrimSpace(item) == "" {
|
||||
return nil, errors.New("empty soundcloud item")
|
||||
|
||||
@@ -43,6 +43,9 @@ func TestGetTrackMetadataAndDownloadable(t *testing.T) {
|
||||
if d.URL != "https://cdn.example/audio.m4a" || d.Extension != "m4a" {
|
||||
t.Fatalf("unexpected downloadable: %+v", d)
|
||||
}
|
||||
if d.Audio.Container != "M4A" || d.Audio.Codec != "AAC" {
|
||||
t.Fatalf("unexpected audio profile: %+v", d.Audio)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetPlaylistMetadata(t *testing.T) {
|
||||
|
||||
@@ -531,7 +531,11 @@ func (c *Client) getDownloadableFromTrackManifestForFormats(ctx context.Context,
|
||||
}
|
||||
}
|
||||
|
||||
return &provider.Downloadable{URL: uri, Extension: ext, Source: "tidal"}, nil
|
||||
profile := tidalAudioProfileFromFormats(attrFormats)
|
||||
if profile.Container == "" {
|
||||
profile = tidalAudioProfileFromExtension(ext)
|
||||
}
|
||||
return &provider.Downloadable{URL: uri, Extension: ext, Source: "tidal", Audio: profile}, nil
|
||||
}
|
||||
|
||||
func formatsForQuality(quality int, preferAtmos bool) []string {
|
||||
@@ -638,7 +642,121 @@ func downloadableFromPlaybackManifest(resp map[string]any) *provider.Downloadabl
|
||||
} else if strings.Contains(codec, "ec-3") || strings.Contains(codec, "eac3") || strings.Contains(codec, "joc") || strings.Contains(codec, "atmos") {
|
||||
ext = "mka"
|
||||
}
|
||||
return &provider.Downloadable{URL: streamURL, Extension: ext, Source: "tidal"}
|
||||
profile := tidalAudioProfileFromCodec(codec)
|
||||
if profile.Container == "" {
|
||||
profile = tidalAudioProfileFromExtension(ext)
|
||||
}
|
||||
audioQuality := strings.ToUpper(strings.TrimSpace(stringify(resp["audioQuality"])))
|
||||
if audioQuality == "" {
|
||||
audioQuality = strings.ToUpper(strings.TrimSpace(stringify(manifest["audioQuality"])))
|
||||
}
|
||||
if audioQuality != "" {
|
||||
profile = applyTidalAudioQuality(profile, audioQuality)
|
||||
}
|
||||
if strings.Contains(strings.ToUpper(stringify(resp["audioMode"])), "ATMOS") {
|
||||
profile = tidalAtmosAudioProfile()
|
||||
}
|
||||
return &provider.Downloadable{URL: streamURL, Extension: ext, Source: "tidal", Audio: profile}
|
||||
}
|
||||
|
||||
func tidalAudioProfileFromFormats(formats []any) provider.AudioProfile {
|
||||
best := provider.AudioProfile{}
|
||||
for _, raw := range formats {
|
||||
f := strings.ToUpper(strings.TrimSpace(stringify(raw)))
|
||||
switch {
|
||||
case strings.Contains(f, "EAC3") || strings.Contains(f, "JOC") || strings.Contains(f, "ATMOS"):
|
||||
return tidalAtmosAudioProfile()
|
||||
case strings.Contains(f, "FLAC_HIRES"):
|
||||
best = provider.AudioProfile{Container: "FLAC", Codec: "FLAC", Quality: "HI_RES_LOSSLESS", BitDepth: 24}
|
||||
case strings.Contains(f, "FLAC"):
|
||||
if best.Container == "" {
|
||||
best = provider.AudioProfile{Container: "FLAC", Codec: "FLAC", Quality: "LOSSLESS", BitDepth: 16, SamplingRate: "44.1"}
|
||||
}
|
||||
case strings.Contains(f, "AACLC"):
|
||||
if best.Container == "" {
|
||||
best = provider.AudioProfile{Container: "M4A", Codec: "AACLC", Quality: "HIGH", BitDepth: 16, SamplingRate: "44.1", BitrateKbps: 320}
|
||||
}
|
||||
case strings.Contains(f, "HEAAC"):
|
||||
if best.Container == "" {
|
||||
best = provider.AudioProfile{Container: "M4A", Codec: "HEAACV1", Quality: "LOW", BitDepth: 16, SamplingRate: "44.1", BitrateKbps: 96}
|
||||
}
|
||||
}
|
||||
}
|
||||
return best
|
||||
}
|
||||
|
||||
func tidalAudioProfileFromCodec(codec string) provider.AudioProfile {
|
||||
c := strings.ToLower(strings.TrimSpace(codec))
|
||||
switch {
|
||||
case strings.Contains(c, "ec-3") || strings.Contains(c, "eac3") || strings.Contains(c, "joc") || strings.Contains(c, "atmos"):
|
||||
return tidalAtmosAudioProfile()
|
||||
case strings.Contains(c, "flac"):
|
||||
return provider.AudioProfile{Container: "FLAC", Codec: "FLAC", Quality: "LOSSLESS", BitDepth: 16, SamplingRate: "44.1"}
|
||||
case strings.Contains(c, "mp4a.40.5") || strings.Contains(c, "mp4a.40.29"):
|
||||
return provider.AudioProfile{Container: "M4A", Codec: "HEAACV1", Quality: "LOW", BitDepth: 16, SamplingRate: "44.1", BitrateKbps: 96}
|
||||
case strings.Contains(c, "mp4a") || strings.Contains(c, "aac"):
|
||||
return provider.AudioProfile{Container: "M4A", Codec: "AACLC", Quality: "HIGH", BitDepth: 16, SamplingRate: "44.1", BitrateKbps: 320}
|
||||
default:
|
||||
return provider.AudioProfile{}
|
||||
}
|
||||
}
|
||||
|
||||
func tidalAtmosAudioProfile() provider.AudioProfile {
|
||||
return provider.AudioProfile{Container: "MKA", Codec: "EAC3_JOC", Quality: "ATMOS", BitDepth: 24, SamplingRate: "48"}
|
||||
}
|
||||
|
||||
func tidalAudioProfileFromExtension(ext string) provider.AudioProfile {
|
||||
switch strings.ToLower(strings.TrimSpace(ext)) {
|
||||
case "flac":
|
||||
return provider.AudioProfile{Container: "FLAC", Codec: "FLAC", Quality: "LOSSLESS", BitDepth: 16, SamplingRate: "44.1"}
|
||||
case "mka":
|
||||
return tidalAtmosAudioProfile()
|
||||
case "m4a":
|
||||
return provider.AudioProfile{Container: "M4A", Codec: "AACLC", Quality: "HIGH", BitDepth: 16, SamplingRate: "44.1", BitrateKbps: 320}
|
||||
default:
|
||||
container := strings.ToUpper(strings.TrimSpace(ext))
|
||||
if container == "" {
|
||||
container = "M4A"
|
||||
}
|
||||
return provider.AudioProfile{Container: container, Codec: container}
|
||||
}
|
||||
}
|
||||
|
||||
func applyTidalAudioQuality(profile provider.AudioProfile, audioQuality string) provider.AudioProfile {
|
||||
aq := strings.ToUpper(strings.TrimSpace(audioQuality))
|
||||
if aq == "" {
|
||||
return profile
|
||||
}
|
||||
profile.Quality = aq
|
||||
switch aq {
|
||||
case "HI_RES", "HI_RES_LOSSLESS":
|
||||
if strings.EqualFold(profile.Container, "FLAC") {
|
||||
if profile.BitDepth < 24 {
|
||||
profile.BitDepth = 24
|
||||
}
|
||||
}
|
||||
case "LOSSLESS":
|
||||
if strings.EqualFold(profile.Container, "FLAC") {
|
||||
if profile.BitDepth == 0 {
|
||||
profile.BitDepth = 16
|
||||
}
|
||||
if profile.SamplingRate == "" {
|
||||
profile.SamplingRate = "44.1"
|
||||
}
|
||||
}
|
||||
case "HIGH":
|
||||
if strings.EqualFold(profile.Container, "M4A") && profile.BitrateKbps == 0 {
|
||||
profile.BitrateKbps = 320
|
||||
}
|
||||
case "LOW":
|
||||
if strings.EqualFold(profile.Container, "M4A") {
|
||||
profile.Codec = "HEAACV1"
|
||||
if profile.BitrateKbps == 0 {
|
||||
profile.BitrateKbps = 96
|
||||
}
|
||||
}
|
||||
}
|
||||
return profile
|
||||
}
|
||||
|
||||
func bestHLSVariantURL(masterURL, playlist string) string {
|
||||
|
||||
@@ -315,4 +315,7 @@ func TestGetDownloadableLosslessUsesTrackManifestWhenPlaybackIsAAC(t *testing.T)
|
||||
if d.Extension != "flac" {
|
||||
t.Fatalf("extension = %q, want flac", d.Extension)
|
||||
}
|
||||
if d.Audio.Container != "FLAC" || d.Audio.Quality != "LOSSLESS" {
|
||||
t.Fatalf("unexpected audio profile: %+v", d.Audio)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user