mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-05-13 04:24:45 +02:00
refactor: remove Qobuz built-in provider and delete qobuz.go
Delete the entire Qobuz downloader implementation (qobuz.go, qobuz_test.go) including all API clients, search, metadata, download, and track matching code. Empty the builtInProviderRegistry now that all built-in providers are retired. Remove Qobuz-specific exports (SearchQobuzAll, GetQobuzMetadata, ParseQobuzURLExport) and the downloadWithBuiltInQobuz adapter. Stub out PreWarmTrackCache and cache management since no built-in providers remain. Move qobuz cover upgrade regex to cover.go. Update Dart screens, providers, and localization strings for the provider-agnostic UI.
This commit is contained in:
+3
-1
@@ -19,6 +19,8 @@ var deezerSizeRegex = regexp.MustCompile(`/(\d+)x(\d+)-\d+-\d+-\d+-\d+\.jpg$`)
|
||||
|
||||
var tidalSizeRegex = regexp.MustCompile(`/\d+x\d+\.jpg$`)
|
||||
|
||||
var qobuzSizeRegex = regexp.MustCompile(`_\d+\.jpg$`)
|
||||
|
||||
func convertSmallToMedium(imageURL string) string {
|
||||
if strings.Contains(imageURL, spotifySize300) {
|
||||
return strings.Replace(imageURL, spotifySize300, spotifySize640, 1)
|
||||
@@ -135,7 +137,7 @@ func upgradeQobuzCover(coverURL string) string {
|
||||
return coverURL
|
||||
}
|
||||
|
||||
upgraded := qobuzImageSizeRe.ReplaceAllString(coverURL, "_max.jpg")
|
||||
upgraded := qobuzSizeRegex.ReplaceAllString(coverURL, "_max.jpg")
|
||||
if upgraded != coverURL {
|
||||
GoLog("[Cover] Qobuz: upgraded to max resolution")
|
||||
}
|
||||
|
||||
@@ -1955,21 +1955,6 @@ func ClearTrackIDCache() {
|
||||
ClearTrackCache()
|
||||
}
|
||||
|
||||
func SearchQobuzAll(query string, trackLimit, artistLimit int, filter string) (string, error) {
|
||||
downloader := NewQobuzDownloader()
|
||||
results, err := downloader.SearchAll(query, trackLimit, artistLimit, filter)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
jsonBytes, err := json.Marshal(results)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(jsonBytes), nil
|
||||
}
|
||||
|
||||
func SearchProviderAllJSON(
|
||||
providerID,
|
||||
query string,
|
||||
@@ -2045,36 +2030,6 @@ func GetDeezerMetadata(resourceType, resourceID string) (string, error) {
|
||||
return string(jsonBytes), nil
|
||||
}
|
||||
|
||||
func GetQobuzMetadata(resourceType, resourceID string) (string, error) {
|
||||
downloader := NewQobuzDownloader()
|
||||
|
||||
var data interface{}
|
||||
var err error
|
||||
|
||||
switch resourceType {
|
||||
case "track":
|
||||
data, err = downloader.GetTrackMetadata(resourceID)
|
||||
case "album":
|
||||
data, err = downloader.GetAlbumMetadata(resourceID)
|
||||
case "artist":
|
||||
data, err = downloader.GetArtistMetadata(resourceID)
|
||||
case "playlist":
|
||||
data, err = downloader.GetPlaylistMetadata(resourceID)
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported Qobuz resource type: %s", resourceType)
|
||||
}
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
jsonBytes, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(jsonBytes), nil
|
||||
}
|
||||
|
||||
func normalizeExtensionTrackMetadataMap(
|
||||
track ExtTrackMetadata,
|
||||
fallbackCover string,
|
||||
@@ -2333,25 +2288,6 @@ func ParseDeezerURLExport(url string) (string, error) {
|
||||
return string(jsonBytes), nil
|
||||
}
|
||||
|
||||
func ParseQobuzURLExport(url string) (string, error) {
|
||||
resourceType, resourceID, err := parseQobuzURL(url)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
result := map[string]string{
|
||||
"type": resourceType,
|
||||
"id": resourceID,
|
||||
}
|
||||
|
||||
jsonBytes, err := json.Marshal(result)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(jsonBytes), nil
|
||||
}
|
||||
|
||||
func ParseProviderURLJSON(url string) (string, error) {
|
||||
parsers := []struct {
|
||||
providerID string
|
||||
|
||||
@@ -158,27 +158,6 @@ func downloadWithBuiltInProvider(providerID string, req DownloadRequest) (Downlo
|
||||
return spec.Download(req)
|
||||
}
|
||||
|
||||
func downloadWithBuiltInQobuz(req DownloadRequest) (DownloadResult, error) {
|
||||
result, err := downloadFromQobuz(req)
|
||||
if err != nil {
|
||||
return DownloadResult{}, err
|
||||
}
|
||||
return DownloadResult{
|
||||
FilePath: result.FilePath,
|
||||
BitDepth: result.BitDepth,
|
||||
SampleRate: result.SampleRate,
|
||||
Title: result.Title,
|
||||
Artist: result.Artist,
|
||||
Album: result.Album,
|
||||
ReleaseDate: result.ReleaseDate,
|
||||
TrackNumber: result.TrackNumber,
|
||||
DiscNumber: result.DiscNumber,
|
||||
ISRC: result.ISRC,
|
||||
CoverURL: result.CoverURL,
|
||||
LyricsLRC: result.LyricsLRC,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func normalizeExtensionDownloadResult(result *ExtDownloadResult) (DownloadResult, bool) {
|
||||
if result == nil {
|
||||
return DownloadResult{}, false
|
||||
|
||||
+2
-150
@@ -4,100 +4,8 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type TrackIDCacheEntry struct {
|
||||
QobuzTrackID int64
|
||||
ExpiresAt time.Time
|
||||
}
|
||||
|
||||
type TrackIDCache struct {
|
||||
cache map[string]*TrackIDCacheEntry
|
||||
mu sync.RWMutex
|
||||
ttl time.Duration
|
||||
lastCleanup time.Time
|
||||
cleanupInterval time.Duration
|
||||
}
|
||||
|
||||
var (
|
||||
globalTrackIDCache *TrackIDCache
|
||||
trackIDCacheOnce sync.Once
|
||||
)
|
||||
|
||||
func GetTrackIDCache() *TrackIDCache {
|
||||
trackIDCacheOnce.Do(func() {
|
||||
globalTrackIDCache = &TrackIDCache{
|
||||
cache: make(map[string]*TrackIDCacheEntry),
|
||||
ttl: 30 * time.Minute,
|
||||
cleanupInterval: 5 * time.Minute,
|
||||
}
|
||||
})
|
||||
return globalTrackIDCache
|
||||
}
|
||||
|
||||
func (c *TrackIDCache) Get(isrc string) *TrackIDCacheEntry {
|
||||
c.mu.RLock()
|
||||
entry, exists := c.cache[isrc]
|
||||
if !exists {
|
||||
c.mu.RUnlock()
|
||||
return nil
|
||||
}
|
||||
expired := time.Now().After(entry.ExpiresAt)
|
||||
c.mu.RUnlock()
|
||||
|
||||
if !expired {
|
||||
return entry
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
entry, exists = c.cache[isrc]
|
||||
if exists && time.Now().After(entry.ExpiresAt) {
|
||||
delete(c.cache, isrc)
|
||||
}
|
||||
c.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *TrackIDCache) pruneExpiredLocked(now time.Time) {
|
||||
for key, entry := range c.cache {
|
||||
if now.After(entry.ExpiresAt) {
|
||||
delete(c.cache, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *TrackIDCache) SetQobuz(isrc string, trackID int64) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
entry, exists := c.cache[isrc]
|
||||
if !exists {
|
||||
entry = &TrackIDCacheEntry{}
|
||||
c.cache[isrc] = entry
|
||||
}
|
||||
entry.QobuzTrackID = trackID
|
||||
now := time.Now()
|
||||
entry.ExpiresAt = now.Add(c.ttl)
|
||||
|
||||
if c.cleanupInterval > 0 && (c.lastCleanup.IsZero() || now.Sub(c.lastCleanup) >= c.cleanupInterval) {
|
||||
c.pruneExpiredLocked(now)
|
||||
c.lastCleanup = now
|
||||
}
|
||||
}
|
||||
|
||||
func (c *TrackIDCache) Clear() {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.cache = make(map[string]*TrackIDCacheEntry)
|
||||
}
|
||||
|
||||
func (c *TrackIDCache) Size() int {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
return len(c.cache)
|
||||
}
|
||||
|
||||
type ParallelDownloadResult struct {
|
||||
CoverData []byte
|
||||
LyricsData *LyricsResponse
|
||||
@@ -167,62 +75,7 @@ type PreWarmCacheRequest struct {
|
||||
}
|
||||
|
||||
func PreWarmTrackCache(requests []PreWarmCacheRequest) {
|
||||
if len(requests) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
cache := GetTrackIDCache()
|
||||
|
||||
semaphore := make(chan struct{}, 3)
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for _, req := range requests {
|
||||
if req.ISRC == "" {
|
||||
continue
|
||||
}
|
||||
if cached := cache.Get(req.ISRC); cached != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
wg.Add(1)
|
||||
go func(r PreWarmCacheRequest) {
|
||||
defer wg.Done()
|
||||
semaphore <- struct{}{}
|
||||
defer func() { <-semaphore }()
|
||||
|
||||
switch r.Service {
|
||||
case "qobuz":
|
||||
preWarmQobuzCache(r.ISRC, r.SpotifyID)
|
||||
}
|
||||
}(req)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// preWarmQobuzCache tries to get Qobuz Track ID in the following order:
|
||||
// 1. From SongLink (fast, no Qobuz API call needed)
|
||||
// 2. Direct ISRC search on Qobuz API (slower, may fail if ISRC not in Qobuz database)
|
||||
func preWarmQobuzCache(isrc, spotifyID string) {
|
||||
if spotifyID != "" {
|
||||
client := NewSongLinkClient()
|
||||
availability, err := client.CheckTrackAvailability(spotifyID, isrc)
|
||||
if err == nil && availability != nil && availability.QobuzID != "" {
|
||||
var trackID int64
|
||||
if _, parseErr := fmt.Sscanf(availability.QobuzID, "%d", &trackID); parseErr == nil && trackID > 0 {
|
||||
GoLog("[Qobuz] Pre-warm cache: Got Qobuz ID %d from SongLink for ISRC %s\n", trackID, isrc)
|
||||
GetTrackIDCache().SetQobuz(isrc, trackID)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
downloader := NewQobuzDownloader()
|
||||
track, err := downloader.SearchTrackByISRC(isrc)
|
||||
if err == nil && track != nil {
|
||||
GoLog("[Qobuz] Pre-warm cache: Got Qobuz ID %d from direct ISRC search for %s\n", track.ID, isrc)
|
||||
GetTrackIDCache().SetQobuz(isrc, track.ID)
|
||||
}
|
||||
_ = requests
|
||||
}
|
||||
|
||||
func PreWarmCache(tracksJSON string) error {
|
||||
@@ -254,9 +107,8 @@ func PreWarmCache(tracksJSON string) error {
|
||||
}
|
||||
|
||||
func ClearTrackCache() {
|
||||
GetTrackIDCache().Clear()
|
||||
}
|
||||
|
||||
func GetCacheSize() int {
|
||||
return GetTrackIDCache().Size()
|
||||
return 0
|
||||
}
|
||||
|
||||
-2896
File diff suppressed because it is too large
Load Diff
@@ -1,553 +0,0 @@
|
||||
package gobackend
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func buildTestQobuzAlbum(id, title, artist string, tracks ...QobuzTrack) *qobuzAlbumDetails {
|
||||
album := &qobuzAlbumDetails{
|
||||
ID: id,
|
||||
Title: title,
|
||||
ReleaseDateOriginal: "2013-05-20",
|
||||
TracksCount: len(tracks),
|
||||
ProductType: "album",
|
||||
ReleaseType: "album",
|
||||
}
|
||||
album.Artist = qobuzArtistRef{ID: 1, Name: artist}
|
||||
album.Artists = []qobuzArtistRef{{ID: 1, Name: artist}}
|
||||
album.Tracks.Items = tracks
|
||||
return album
|
||||
}
|
||||
|
||||
func TestParseQobuzURL(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
wantType string
|
||||
wantID string
|
||||
expectErr bool
|
||||
}{
|
||||
{
|
||||
name: "store album url",
|
||||
input: "https://www.qobuz.com/us-en/album/harry-styles-harry-styles/0886446451985",
|
||||
wantType: "album",
|
||||
wantID: "0886446451985",
|
||||
},
|
||||
{
|
||||
name: "store playlist url",
|
||||
input: "https://www.qobuz.com/us-en/playlists/new-releases/2049430",
|
||||
wantType: "playlist",
|
||||
wantID: "2049430",
|
||||
},
|
||||
{
|
||||
name: "store artist url",
|
||||
input: "https://www.qobuz.com/us-en/interpreter/harry-styles/729886",
|
||||
wantType: "artist",
|
||||
wantID: "729886",
|
||||
},
|
||||
{
|
||||
name: "play track url",
|
||||
input: "https://play.qobuz.com/track/40681594",
|
||||
wantType: "track",
|
||||
wantID: "40681594",
|
||||
},
|
||||
{
|
||||
name: "custom scheme playlist url",
|
||||
input: "qobuzapp://playlist/2049430",
|
||||
wantType: "playlist",
|
||||
wantID: "2049430",
|
||||
},
|
||||
{
|
||||
name: "unsupported url",
|
||||
input: "https://example.com/not-qobuz",
|
||||
expectErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
gotType, gotID, err := parseQobuzURL(test.input)
|
||||
if test.expectErr {
|
||||
if err == nil {
|
||||
t.Fatalf("expected error, got none")
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if gotType != test.wantType || gotID != test.wantID {
|
||||
t.Fatalf("parseQobuzURL(%q) = (%q, %q), want (%q, %q)", test.input, gotType, gotID, test.wantType, test.wantID)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractQobuzArtistAlbumIDs(t *testing.T) {
|
||||
body := []byte(`
|
||||
<div class="product__item">
|
||||
<button data-itemtype="album" data-itemId="yrpbt0lwm3g0y"></button>
|
||||
</div>
|
||||
<div class="product__item">
|
||||
<button data-itemtype="album" data-itemId="yrpbt0lwm3g0y"></button>
|
||||
</div>
|
||||
<div class="product__item">
|
||||
<button data-itemtype="album" data-itemId="0886446451985"></button>
|
||||
</div>
|
||||
`)
|
||||
|
||||
matches := qobuzArtistAlbumIDRegex.FindAllSubmatch(body, -1)
|
||||
if len(matches) != 3 {
|
||||
t.Fatalf("expected 3 regex matches, got %d", len(matches))
|
||||
}
|
||||
if string(matches[0][1]) != "yrpbt0lwm3g0y" {
|
||||
t.Fatalf("unexpected first album id: %q", matches[0][1])
|
||||
}
|
||||
if string(matches[2][1]) != "0886446451985" {
|
||||
t.Fatalf("unexpected last album id: %q", matches[2][1])
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractQobuzDownloadURLFromBody(t *testing.T) {
|
||||
t.Run("reads top-level download_url and quality metadata", func(t *testing.T) {
|
||||
body := []byte(`{"success":true,"download_url":"https://example.test/new.flac","bit_depth":24,"sampling_rate":96}`)
|
||||
|
||||
info, err := extractQobuzDownloadInfoFromBody(body)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if info.DownloadURL != "https://example.test/new.flac" {
|
||||
t.Fatalf("unexpected URL: %q", info.DownloadURL)
|
||||
}
|
||||
if info.BitDepth != 24 {
|
||||
t.Fatalf("unexpected bit depth: %d", info.BitDepth)
|
||||
}
|
||||
if info.SampleRate != 96000 {
|
||||
t.Fatalf("unexpected sample rate: %d", info.SampleRate)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("reads nested data.url", func(t *testing.T) {
|
||||
body := []byte(`{"success":true,"data":{"url":"https://example.test/audio.flac"}}`)
|
||||
|
||||
got, err := extractQobuzDownloadURLFromBody(body)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if got != "https://example.test/audio.flac" {
|
||||
t.Fatalf("unexpected URL: %q", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("reads top-level url", func(t *testing.T) {
|
||||
body := []byte(`{"url":"https://example.test/top.flac"}`)
|
||||
|
||||
got, err := extractQobuzDownloadURLFromBody(body)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if got != "https://example.test/top.flac" {
|
||||
t.Fatalf("unexpected URL: %q", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("returns API error", func(t *testing.T) {
|
||||
body := []byte(`{"error":"track not found"}`)
|
||||
|
||||
_, err := extractQobuzDownloadURLFromBody(body)
|
||||
if err == nil || err.Error() != "track not found" {
|
||||
t.Fatalf("expected track-not-found error, got %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("returns message when success false", func(t *testing.T) {
|
||||
body := []byte(`{"success":false,"message":"blocked"}`)
|
||||
|
||||
_, err := extractQobuzDownloadURLFromBody(body)
|
||||
if err == nil || err.Error() != "blocked" {
|
||||
t.Fatalf("expected blocked error, got %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("returns detail error", func(t *testing.T) {
|
||||
body := []byte(`{"detail":"Invalid quality 'lossless'. Choose from: ['mp3', 'cd', 'hi-res', 'hi-res-max']"}`)
|
||||
|
||||
_, err := extractQobuzDownloadURLFromBody(body)
|
||||
if err == nil || err.Error() != "Invalid quality 'lossless'. Choose from: ['mp3', 'cd', 'hi-res', 'hi-res-max']" {
|
||||
t.Fatalf("expected detail error, got %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestNormalizeQobuzQualityCode(t *testing.T) {
|
||||
tests := map[string]string{
|
||||
"": "6",
|
||||
"5": "6",
|
||||
"6": "6",
|
||||
"cd": "6",
|
||||
"lossless": "6",
|
||||
"7": "7",
|
||||
"hi-res": "7",
|
||||
"27": "27",
|
||||
"hi-res-max": "27",
|
||||
"unexpected": "6",
|
||||
}
|
||||
|
||||
for input, want := range tests {
|
||||
if got := normalizeQobuzQualityCode(input); got != want {
|
||||
t.Fatalf("normalizeQobuzQualityCode(%q) = %q, want %q", input, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildQobuzMusicDLPayloadUsesOpenTrackURL(t *testing.T) {
|
||||
payloadBytes, err := buildQobuzMusicDLPayload(374610875, "7")
|
||||
if err != nil {
|
||||
t.Fatalf("buildQobuzMusicDLPayload returned error: %v", err)
|
||||
}
|
||||
|
||||
var payload map[string]any
|
||||
if err := json.Unmarshal(payloadBytes, &payload); err != nil {
|
||||
t.Fatalf("payload is not valid JSON: %v", err)
|
||||
}
|
||||
|
||||
if got := payload["url"]; got != "https://open.qobuz.com/track/374610875" {
|
||||
t.Fatalf("payload url = %v, want open.qobuz.com track URL", got)
|
||||
}
|
||||
if got := payload["quality"]; got != "hi-res" {
|
||||
t.Fatalf("payload quality = %v, want hi-res", got)
|
||||
}
|
||||
if got := payload["upload_to_r2"]; got != false {
|
||||
t.Fatalf("payload upload_to_r2 = %v, want false", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractQobuzAlbumIDsFromArtistHTML(t *testing.T) {
|
||||
body := []byte(`
|
||||
<button data-itemtype="album" data-itemId="0886446451985"></button>
|
||||
<button data-itemtype="album" data-itemId="0886446451985"></button>
|
||||
<button data-itemtype="album" data-itemId="pvv406bth40ya"></button>
|
||||
`)
|
||||
|
||||
got := extractQobuzAlbumIDsFromArtistHTML(body)
|
||||
if len(got) != 2 {
|
||||
t.Fatalf("expected 2 unique album IDs, got %d (%v)", len(got), got)
|
||||
}
|
||||
if got[0] != "0886446451985" || got[1] != "pvv406bth40ya" {
|
||||
t.Fatalf("unexpected album IDs: %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQobuzAvailableProviders(t *testing.T) {
|
||||
providers := NewQobuzDownloader().GetAvailableProviders()
|
||||
if len(providers) != 6 {
|
||||
t.Fatalf("expected 6 Qobuz providers, got %d", len(providers))
|
||||
}
|
||||
|
||||
want := map[string]string{
|
||||
"musicdl": qobuzAPIKindMusicDL,
|
||||
"zarz": qobuzAPIKindMusicDL,
|
||||
"dabmusic": qobuzAPIKindStandard,
|
||||
"deeb": qobuzAPIKindStandard,
|
||||
"qbz": qobuzAPIKindStandard,
|
||||
"squid": qobuzAPIKindStandard,
|
||||
}
|
||||
|
||||
for _, provider := range providers {
|
||||
wantKind, ok := want[provider.Name]
|
||||
if !ok {
|
||||
t.Fatalf("unexpected provider %q", provider.Name)
|
||||
}
|
||||
if provider.Kind != wantKind {
|
||||
t.Fatalf("provider %q has kind %q, want %q", provider.Name, provider.Kind, wantKind)
|
||||
}
|
||||
delete(want, provider.Name)
|
||||
}
|
||||
|
||||
if len(want) != 0 {
|
||||
t.Fatalf("missing providers: %v", want)
|
||||
}
|
||||
}
|
||||
|
||||
func testQobuzTrack(id int64, title, artist string, duration int) *QobuzTrack {
|
||||
track := &QobuzTrack{
|
||||
ID: id,
|
||||
Title: title,
|
||||
Duration: duration,
|
||||
}
|
||||
track.Performer.Name = artist
|
||||
return track
|
||||
}
|
||||
|
||||
func TestSelectQobuzTracksFromAlbumSearchResultsPrefersMatchingTrack(t *testing.T) {
|
||||
summaries := []qobuzAlbumDetails{
|
||||
{ID: "album-a"},
|
||||
{ID: "album-b"},
|
||||
}
|
||||
|
||||
match := *testQobuzTrack(1, "Get Lucky", "Daft Punk", 369)
|
||||
other := *testQobuzTrack(2, "Fragments of Time", "Daft Punk", 280)
|
||||
fallback := *testQobuzTrack(3, "Da Funk", "Daft Punk", 330)
|
||||
|
||||
albums := map[string]*qobuzAlbumDetails{
|
||||
"album-a": buildTestQobuzAlbum("album-a", "Random Access Memories", "Daft Punk", match, other),
|
||||
"album-b": buildTestQobuzAlbum("album-b", "Homework", "Daft Punk", fallback),
|
||||
}
|
||||
|
||||
tracks, err := selectQobuzTracksFromAlbumSearchResults(
|
||||
"daft punk get lucky",
|
||||
3,
|
||||
summaries,
|
||||
func(id string) (*qobuzAlbumDetails, error) { return albums[id], nil },
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(tracks) == 0 {
|
||||
t.Fatal("expected tracks, got none")
|
||||
}
|
||||
if tracks[0].ID != 1 {
|
||||
t.Fatalf("expected Get Lucky to rank first, got track id %d", tracks[0].ID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectQobuzTracksFromAlbumSearchResultsDedupesTracks(t *testing.T) {
|
||||
summaries := []qobuzAlbumDetails{
|
||||
{ID: "album-a"},
|
||||
{ID: "album-b"},
|
||||
}
|
||||
|
||||
shared := *testQobuzTrack(42, "Get Lucky", "Daft Punk", 369)
|
||||
|
||||
albums := map[string]*qobuzAlbumDetails{
|
||||
"album-a": buildTestQobuzAlbum("album-a", "Random Access Memories", "Daft Punk", shared),
|
||||
"album-b": buildTestQobuzAlbum("album-b", "Random Access Memories Deluxe", "Daft Punk", shared),
|
||||
}
|
||||
|
||||
tracks, err := selectQobuzTracksFromAlbumSearchResults(
|
||||
"daft punk get lucky",
|
||||
5,
|
||||
summaries,
|
||||
func(id string) (*qobuzAlbumDetails, error) { return albums[id], nil },
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(tracks) != 1 {
|
||||
t.Fatalf("expected 1 deduped track, got %d", len(tracks))
|
||||
}
|
||||
if tracks[0].ID != 42 {
|
||||
t.Fatalf("unexpected deduped track id: %d", tracks[0].ID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveQobuzTrackForRequestRejectsSongLinkMismatch(t *testing.T) {
|
||||
origGetTrackByID := qobuzGetTrackByIDFunc
|
||||
origSearchISRC := qobuzSearchTrackByISRCWithDurationFunc
|
||||
origSearchMetadata := qobuzSearchTrackByMetadataWithDurationFunc
|
||||
origSongLinkCheck := songLinkCheckTrackAvailabilityFunc
|
||||
t.Cleanup(func() {
|
||||
qobuzGetTrackByIDFunc = origGetTrackByID
|
||||
qobuzSearchTrackByISRCWithDurationFunc = origSearchISRC
|
||||
qobuzSearchTrackByMetadataWithDurationFunc = origSearchMetadata
|
||||
songLinkCheckTrackAvailabilityFunc = origSongLinkCheck
|
||||
GetTrackIDCache().Clear()
|
||||
})
|
||||
GetTrackIDCache().Clear()
|
||||
|
||||
qobuzGetTrackByIDFunc = func(_ *QobuzDownloader, trackID int64) (*QobuzTrack, error) {
|
||||
if trackID != 111 {
|
||||
t.Fatalf("unexpected track ID lookup: %d", trackID)
|
||||
}
|
||||
return testQobuzTrack(111, "Aperture", "Harry Styles", 180), nil
|
||||
}
|
||||
qobuzSearchTrackByISRCWithDurationFunc = func(_ *QobuzDownloader, isrc string, expectedDurationSec int) (*QobuzTrack, error) {
|
||||
if isrc != "TESTISRC1" {
|
||||
t.Fatalf("unexpected ISRC lookup: %q", isrc)
|
||||
}
|
||||
if expectedDurationSec != 180 {
|
||||
t.Fatalf("unexpected duration: %d", expectedDurationSec)
|
||||
}
|
||||
return testQobuzTrack(222, "Taste Back", "Harry Styles", 180), nil
|
||||
}
|
||||
qobuzSearchTrackByMetadataWithDurationFunc = func(_ *QobuzDownloader, _, _ string, _ int) (*QobuzTrack, error) {
|
||||
t.Fatal("metadata fallback should not run when ISRC fallback succeeds")
|
||||
return nil, nil
|
||||
}
|
||||
songLinkCheckTrackAvailabilityFunc = func(_ *SongLinkClient, spotifyTrackID string, isrc string) (*TrackAvailability, error) {
|
||||
if spotifyTrackID != "spotify-track-id" {
|
||||
t.Fatalf("unexpected spotify ID: %q", spotifyTrackID)
|
||||
}
|
||||
if isrc != "TESTISRC1" {
|
||||
t.Fatalf("unexpected SongLink ISRC: %q", isrc)
|
||||
}
|
||||
return &TrackAvailability{QobuzID: "111"}, nil
|
||||
}
|
||||
|
||||
req := DownloadRequest{
|
||||
ISRC: "TESTISRC1",
|
||||
SpotifyID: "spotify-track-id",
|
||||
TrackName: "Taste Back",
|
||||
ArtistName: "Harry Styles",
|
||||
DurationMS: 180000,
|
||||
}
|
||||
|
||||
track, err := resolveQobuzTrackForRequest(req, &QobuzDownloader{}, "Test")
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if track == nil || track.ID != 222 || track.Title != "Taste Back" {
|
||||
t.Fatalf("unexpected resolved track: %+v", track)
|
||||
}
|
||||
|
||||
cached := GetTrackIDCache().Get(req.ISRC)
|
||||
if cached == nil || cached.QobuzTrackID != 222 {
|
||||
t.Fatalf("expected validated fallback track to be cached, got %+v", cached)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveQobuzTrackForRequestRejectsOdesliMismatch(t *testing.T) {
|
||||
origGetTrackByID := qobuzGetTrackByIDFunc
|
||||
origSearchISRC := qobuzSearchTrackByISRCWithDurationFunc
|
||||
origSearchMetadata := qobuzSearchTrackByMetadataWithDurationFunc
|
||||
origSongLinkCheck := songLinkCheckTrackAvailabilityFunc
|
||||
t.Cleanup(func() {
|
||||
qobuzGetTrackByIDFunc = origGetTrackByID
|
||||
qobuzSearchTrackByISRCWithDurationFunc = origSearchISRC
|
||||
qobuzSearchTrackByMetadataWithDurationFunc = origSearchMetadata
|
||||
songLinkCheckTrackAvailabilityFunc = origSongLinkCheck
|
||||
})
|
||||
|
||||
qobuzGetTrackByIDFunc = func(_ *QobuzDownloader, trackID int64) (*QobuzTrack, error) {
|
||||
if trackID != 333 {
|
||||
t.Fatalf("unexpected track ID lookup: %d", trackID)
|
||||
}
|
||||
return testQobuzTrack(333, "American Girls", "Harry Styles", 181), nil
|
||||
}
|
||||
qobuzSearchTrackByISRCWithDurationFunc = func(_ *QobuzDownloader, _ string, _ int) (*QobuzTrack, error) {
|
||||
t.Fatal("ISRC fallback should not run without an ISRC")
|
||||
return nil, nil
|
||||
}
|
||||
qobuzSearchTrackByMetadataWithDurationFunc = func(_ *QobuzDownloader, _, _ string, _ int) (*QobuzTrack, error) {
|
||||
t.Fatal("metadata fallback should not run")
|
||||
return nil, nil
|
||||
}
|
||||
songLinkCheckTrackAvailabilityFunc = func(_ *SongLinkClient, _, _ string) (*TrackAvailability, error) {
|
||||
t.Fatal("SongLink should not run when Odesli QobuzID is provided")
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
req := DownloadRequest{
|
||||
QobuzID: "333",
|
||||
TrackName: "Taste Back",
|
||||
ArtistName: "Harry Styles",
|
||||
DurationMS: 181000,
|
||||
}
|
||||
|
||||
track, err := resolveQobuzTrackForRequest(req, &QobuzDownloader{}, "Test")
|
||||
if err == nil {
|
||||
t.Fatalf("expected error, got track %+v", track)
|
||||
}
|
||||
if track != nil {
|
||||
t.Fatalf("expected nil track, got %+v", track)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveQobuzTrackForRequestUsesPrefixedQobuzIDWithoutSongLink(t *testing.T) {
|
||||
origGetTrackByID := qobuzGetTrackByIDFunc
|
||||
origSearchISRC := qobuzSearchTrackByISRCWithDurationFunc
|
||||
origSearchMetadata := qobuzSearchTrackByMetadataWithDurationFunc
|
||||
origSongLinkCheck := songLinkCheckTrackAvailabilityFunc
|
||||
t.Cleanup(func() {
|
||||
qobuzGetTrackByIDFunc = origGetTrackByID
|
||||
qobuzSearchTrackByISRCWithDurationFunc = origSearchISRC
|
||||
qobuzSearchTrackByMetadataWithDurationFunc = origSearchMetadata
|
||||
songLinkCheckTrackAvailabilityFunc = origSongLinkCheck
|
||||
})
|
||||
|
||||
qobuzGetTrackByIDFunc = func(_ *QobuzDownloader, trackID int64) (*QobuzTrack, error) {
|
||||
if trackID != 40681594 {
|
||||
t.Fatalf("unexpected track ID lookup: %d", trackID)
|
||||
}
|
||||
return testQobuzTrack(40681594, "Sign of the Times", "Harry Styles", 341), nil
|
||||
}
|
||||
qobuzSearchTrackByISRCWithDurationFunc = func(_ *QobuzDownloader, _ string, _ int) (*QobuzTrack, error) {
|
||||
t.Fatal("ISRC fallback should not run when request qobuz id succeeds")
|
||||
return nil, nil
|
||||
}
|
||||
qobuzSearchTrackByMetadataWithDurationFunc = func(_ *QobuzDownloader, _, _ string, _ int) (*QobuzTrack, error) {
|
||||
t.Fatal("metadata fallback should not run when request qobuz id succeeds")
|
||||
return nil, nil
|
||||
}
|
||||
songLinkCheckTrackAvailabilityFunc = func(_ *SongLinkClient, _, _ string) (*TrackAvailability, error) {
|
||||
t.Fatal("SongLink should not run when request qobuz id is provided")
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
req := DownloadRequest{
|
||||
QobuzID: "qobuz:40681594",
|
||||
TrackName: "Sign of the Times",
|
||||
ArtistName: "Harry Styles",
|
||||
DurationMS: 341000,
|
||||
}
|
||||
|
||||
track, err := resolveQobuzTrackForRequest(req, &QobuzDownloader{}, "Test")
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if track == nil || track.ID != 40681594 {
|
||||
t.Fatalf("unexpected resolved track: %+v", track)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQobuzTrackMatchesRequest_SongLinkBypassesArtistAndTitle(t *testing.T) {
|
||||
req := DownloadRequest{
|
||||
TrackName: "Ringišpil",
|
||||
ArtistName: "Djordje Balasevic",
|
||||
}
|
||||
|
||||
track := &QobuzTrack{
|
||||
Title: "Different Title",
|
||||
Duration: 0,
|
||||
}
|
||||
track.Performer.Name = "Different Artist"
|
||||
|
||||
if !qobuzTrackMatchesRequest(req, track, "Qobuz", "SongLink Qobuz ID", true) {
|
||||
t.Fatal("expected SongLink Qobuz source to bypass artist/title verification")
|
||||
}
|
||||
}
|
||||
|
||||
func TestQobuzTrackMetadataIncludesComposer(t *testing.T) {
|
||||
track := &QobuzTrack{
|
||||
ID: 40681594,
|
||||
Title: "Sign of the Times",
|
||||
ISRC: "USSM11703595",
|
||||
Duration: 340,
|
||||
TrackNumber: 1,
|
||||
MediaNumber: 1,
|
||||
}
|
||||
track.Performer.ID = 729886
|
||||
track.Performer.Name = "Harry Styles"
|
||||
track.Composer.ID = 729886
|
||||
track.Composer.Name = "Harry Styles"
|
||||
track.Album.ID = "0886446451985"
|
||||
track.Album.Title = "Harry Styles"
|
||||
track.Album.ReleaseDate = "2017-05-12"
|
||||
track.Album.TracksCount = 10
|
||||
track.Album.ReleaseType = "album"
|
||||
track.Album.ProductType = "album"
|
||||
track.Album.Artist.ID = 729886
|
||||
track.Album.Artist.Name = "Harry Styles"
|
||||
track.Album.Artists = []qobuzArtistRef{{ID: 729886, Name: "Harry Styles"}}
|
||||
|
||||
trackMeta := qobuzTrackToTrackMetadata(track)
|
||||
if trackMeta.Composer != "Harry Styles" {
|
||||
t.Fatalf("track composer = %q", trackMeta.Composer)
|
||||
}
|
||||
|
||||
albumTrackMeta := qobuzTrackToAlbumTrackMetadata(track)
|
||||
if albumTrackMeta.Composer != "Harry Styles" {
|
||||
t.Fatalf("album track composer = %q", albumTrackMeta.Composer)
|
||||
}
|
||||
}
|
||||
@@ -69,18 +69,3 @@ func TestTitlesMatch_EmojiStrict(t *testing.T) {
|
||||
t.Fatal("expected identical emoji titles to match")
|
||||
}
|
||||
}
|
||||
|
||||
func TestQobuzTitlesMatch_SeparatorVariants(t *testing.T) {
|
||||
if !qobuzTitlesMatch("Doctor / Cops", "Doctor _ Cops") {
|
||||
t.Fatal("expected qobuzTitlesMatch to accept / vs _ variant")
|
||||
}
|
||||
}
|
||||
|
||||
func TestQobuzTitlesMatch_EmojiStrict(t *testing.T) {
|
||||
if qobuzTitlesMatch("🪐", "Higher Power") {
|
||||
t.Fatal("expected emoji title not to match unrelated textual title")
|
||||
}
|
||||
if !qobuzTitlesMatch("🪐", "🪐") {
|
||||
t.Fatal("expected identical emoji titles to match")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -376,10 +376,10 @@ abstract class AppLocalizations {
|
||||
/// **'Choose which tab opens first for new search results.'**
|
||||
String get optionsDefaultSearchTabSubtitle;
|
||||
|
||||
/// Hint to switch back to built-in providers
|
||||
/// Hint to switch away from the current provider selection
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Tap Deezer or Spotify to switch back from extension'**
|
||||
/// **'Choose another provider below to stop using the current extension'**
|
||||
String get optionsSwitchBack;
|
||||
|
||||
/// Auto-retry with other services
|
||||
@@ -406,10 +406,10 @@ abstract class AppLocalizations {
|
||||
/// **'Extensions will be tried first'**
|
||||
String get optionsUseExtensionProvidersOn;
|
||||
|
||||
/// Status when extension providers disabled
|
||||
/// Status when extension providers are disabled
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Using built-in providers only'**
|
||||
/// **'Extensions are turned off'**
|
||||
String get optionsUseExtensionProvidersOff;
|
||||
|
||||
/// Embed lyrics in audio files
|
||||
@@ -5073,16 +5073,16 @@ abstract class AppLocalizations {
|
||||
/// **'Off: strict HTTPS certificate validation (recommended)'**
|
||||
String get downloadNetworkCompatibilityModeDisabled;
|
||||
|
||||
/// Hint shown instead of Ask-quality subtitle when no built-in service selected
|
||||
/// Hint shown instead of Ask-quality subtitle when the selected provider does not expose built-in quality controls
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Select a built-in service to enable'**
|
||||
/// **'Select a compatible download provider to enable quality options'**
|
||||
String get downloadSelectServiceToEnable;
|
||||
|
||||
/// Info hint when non-Tidal/Qobuz service is selected
|
||||
/// Info hint when the selected provider does not expose built-in quality controls
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Select Tidal or Qobuz above to configure quality'**
|
||||
/// **'Select a compatible provider above to configure quality'**
|
||||
String get downloadSelectTidalQobuz;
|
||||
|
||||
/// Subtitle for Embed Lyrics when Embed Metadata is disabled
|
||||
|
||||
@@ -2952,11 +2952,11 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get downloadSelectServiceToEnable =>
|
||||
'Select a built-in service to enable';
|
||||
'Select a compatible download provider to enable quality options';
|
||||
|
||||
@override
|
||||
String get downloadSelectTidalQobuz =>
|
||||
'Select Tidal or Qobuz above to configure quality';
|
||||
'Select a compatible provider above to configure quality';
|
||||
|
||||
@override
|
||||
String get downloadEmbedLyricsDisabled =>
|
||||
|
||||
@@ -143,7 +143,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get optionsSwitchBack =>
|
||||
'Tap Deezer or Spotify to switch back from extension';
|
||||
'Choose another provider below to stop using the current extension';
|
||||
|
||||
@override
|
||||
String get optionsAutoFallback => 'Auto Fallback';
|
||||
@@ -159,7 +159,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
String get optionsUseExtensionProvidersOn => 'Extensions will be tried first';
|
||||
|
||||
@override
|
||||
String get optionsUseExtensionProvidersOff => 'Using built-in providers only';
|
||||
String get optionsUseExtensionProvidersOff => 'Extensions are turned off';
|
||||
|
||||
@override
|
||||
String get optionsEmbedLyrics => 'Embed Lyrics';
|
||||
@@ -2920,11 +2920,11 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get downloadSelectServiceToEnable =>
|
||||
'Select a built-in service to enable';
|
||||
'Select a compatible download provider to enable quality options';
|
||||
|
||||
@override
|
||||
String get downloadSelectTidalQobuz =>
|
||||
'Select Tidal or Qobuz above to configure quality';
|
||||
'Select a compatible provider above to configure quality';
|
||||
|
||||
@override
|
||||
String get downloadEmbedLyricsDisabled =>
|
||||
|
||||
@@ -2920,11 +2920,11 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get downloadSelectServiceToEnable =>
|
||||
'Select a built-in service to enable';
|
||||
'Select a compatible download provider to enable quality options';
|
||||
|
||||
@override
|
||||
String get downloadSelectTidalQobuz =>
|
||||
'Select Tidal or Qobuz above to configure quality';
|
||||
'Select a compatible provider above to configure quality';
|
||||
|
||||
@override
|
||||
String get downloadEmbedLyricsDisabled =>
|
||||
|
||||
@@ -2921,11 +2921,11 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get downloadSelectServiceToEnable =>
|
||||
'Select a built-in service to enable';
|
||||
'Select a compatible download provider to enable quality options';
|
||||
|
||||
@override
|
||||
String get downloadSelectTidalQobuz =>
|
||||
'Select Tidal or Qobuz above to configure quality';
|
||||
'Select a compatible provider above to configure quality';
|
||||
|
||||
@override
|
||||
String get downloadEmbedLyricsDisabled =>
|
||||
|
||||
@@ -2919,11 +2919,11 @@ class AppLocalizationsHi extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get downloadSelectServiceToEnable =>
|
||||
'Select a built-in service to enable';
|
||||
'Select a compatible download provider to enable quality options';
|
||||
|
||||
@override
|
||||
String get downloadSelectTidalQobuz =>
|
||||
'Select Tidal or Qobuz above to configure quality';
|
||||
'Select a compatible provider above to configure quality';
|
||||
|
||||
@override
|
||||
String get downloadEmbedLyricsDisabled =>
|
||||
|
||||
@@ -145,7 +145,7 @@ class AppLocalizationsId extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get optionsSwitchBack =>
|
||||
'Ketuk Deezer atau Spotify untuk beralih dari ekstensi';
|
||||
'Pilih provider lain di bawah untuk berhenti memakai ekstensi saat ini';
|
||||
|
||||
@override
|
||||
String get optionsAutoFallback => 'Cadangan Otomatis';
|
||||
@@ -162,8 +162,7 @@ class AppLocalizationsId extends AppLocalizations {
|
||||
'Ekstensi akan dicoba terlebih dahulu';
|
||||
|
||||
@override
|
||||
String get optionsUseExtensionProvidersOff =>
|
||||
'Hanya menggunakan provider bawaan';
|
||||
String get optionsUseExtensionProvidersOff => 'Ekstensi dimatikan';
|
||||
|
||||
@override
|
||||
String get optionsEmbedLyrics => 'Sematkan Lirik';
|
||||
@@ -2930,11 +2929,11 @@ class AppLocalizationsId extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get downloadSelectServiceToEnable =>
|
||||
'Select a built-in service to enable';
|
||||
'Select a compatible download provider to enable quality options';
|
||||
|
||||
@override
|
||||
String get downloadSelectTidalQobuz =>
|
||||
'Select Tidal or Qobuz above to configure quality';
|
||||
'Select a compatible provider above to configure quality';
|
||||
|
||||
@override
|
||||
String get downloadEmbedLyricsDisabled =>
|
||||
|
||||
@@ -142,8 +142,7 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
'Choose which tab opens first for new search results.';
|
||||
|
||||
@override
|
||||
String get optionsSwitchBack =>
|
||||
'Tap Deezer or Spotify to switch back from extension';
|
||||
String get optionsSwitchBack => '現在の拡張の使用をやめるには、下から別のプロバイダーを選択してください';
|
||||
|
||||
@override
|
||||
String get optionsAutoFallback => 'Auto Fallback';
|
||||
@@ -159,7 +158,7 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
String get optionsUseExtensionProvidersOn => '最初に拡張で試みます';
|
||||
|
||||
@override
|
||||
String get optionsUseExtensionProvidersOff => '内蔵のプロバイダーのみを使用する';
|
||||
String get optionsUseExtensionProvidersOff => '拡張はオフです';
|
||||
|
||||
@override
|
||||
String get optionsEmbedLyrics => '歌詞を埋め込む';
|
||||
@@ -2906,11 +2905,11 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get downloadSelectServiceToEnable =>
|
||||
'Select a built-in service to enable';
|
||||
'Select a compatible download provider to enable quality options';
|
||||
|
||||
@override
|
||||
String get downloadSelectTidalQobuz =>
|
||||
'Select Tidal or Qobuz above to configure quality';
|
||||
'Select a compatible provider above to configure quality';
|
||||
|
||||
@override
|
||||
String get downloadEmbedLyricsDisabled =>
|
||||
|
||||
@@ -2899,11 +2899,11 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get downloadSelectServiceToEnable =>
|
||||
'Select a built-in service to enable';
|
||||
'Select a compatible download provider to enable quality options';
|
||||
|
||||
@override
|
||||
String get downloadSelectTidalQobuz =>
|
||||
'Select Tidal or Qobuz above to configure quality';
|
||||
'Select a compatible provider above to configure quality';
|
||||
|
||||
@override
|
||||
String get downloadEmbedLyricsDisabled =>
|
||||
|
||||
@@ -2919,11 +2919,11 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get downloadSelectServiceToEnable =>
|
||||
'Select a built-in service to enable';
|
||||
'Select a compatible download provider to enable quality options';
|
||||
|
||||
@override
|
||||
String get downloadSelectTidalQobuz =>
|
||||
'Select Tidal or Qobuz above to configure quality';
|
||||
'Select a compatible provider above to configure quality';
|
||||
|
||||
@override
|
||||
String get downloadEmbedLyricsDisabled =>
|
||||
|
||||
@@ -2920,11 +2920,11 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get downloadSelectServiceToEnable =>
|
||||
'Select a built-in service to enable';
|
||||
'Select a compatible download provider to enable quality options';
|
||||
|
||||
@override
|
||||
String get downloadSelectTidalQobuz =>
|
||||
'Select Tidal or Qobuz above to configure quality';
|
||||
'Select a compatible provider above to configure quality';
|
||||
|
||||
@override
|
||||
String get downloadEmbedLyricsDisabled =>
|
||||
|
||||
@@ -2979,11 +2979,11 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get downloadSelectServiceToEnable =>
|
||||
'Select a built-in service to enable';
|
||||
'Select a compatible download provider to enable quality options';
|
||||
|
||||
@override
|
||||
String get downloadSelectTidalQobuz =>
|
||||
'Select Tidal or Qobuz above to configure quality';
|
||||
'Select a compatible provider above to configure quality';
|
||||
|
||||
@override
|
||||
String get downloadEmbedLyricsDisabled =>
|
||||
|
||||
@@ -146,7 +146,7 @@ class AppLocalizationsTr extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get optionsSwitchBack =>
|
||||
'Eklentiden çıkıp varsayılana dönmek için Deezer veya Spotify\'a dokunun';
|
||||
'Geçerli eklentiyi bırakmak için aşağıdan başka bir sağlayıcı seçin';
|
||||
|
||||
@override
|
||||
String get optionsAutoFallback => 'Otomatik Geçiş';
|
||||
@@ -163,8 +163,7 @@ class AppLocalizationsTr extends AppLocalizations {
|
||||
'İndirme için önce eklentiler denenecek';
|
||||
|
||||
@override
|
||||
String get optionsUseExtensionProvidersOff =>
|
||||
'Sadece yerleşik sağlayıcılar kullanılıyor';
|
||||
String get optionsUseExtensionProvidersOff => 'Eklentiler kapalı';
|
||||
|
||||
@override
|
||||
String get optionsEmbedLyrics => 'Şarkı Sözlerini Gömer';
|
||||
@@ -2979,11 +2978,11 @@ class AppLocalizationsTr extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get downloadSelectServiceToEnable =>
|
||||
'Seçenekleri açmak için yerleşik bir sağlayıcı seçin';
|
||||
'Kalite seçeneklerini açmak için uyumlu bir indirme sağlayıcısı seçin';
|
||||
|
||||
@override
|
||||
String get downloadSelectTidalQobuz =>
|
||||
'Kaliteyi ayarlamak için lütfen yukarıdan Tidal veya Qobuz seçin';
|
||||
'Kaliteyi ayarlamak için yukarıdan uyumlu bir sağlayıcı seçin';
|
||||
|
||||
@override
|
||||
String get downloadEmbedLyricsDisabled =>
|
||||
|
||||
@@ -2920,11 +2920,11 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get downloadSelectServiceToEnable =>
|
||||
'Select a built-in service to enable';
|
||||
'Select a compatible download provider to enable quality options';
|
||||
|
||||
@override
|
||||
String get downloadSelectTidalQobuz =>
|
||||
'Select Tidal or Qobuz above to configure quality';
|
||||
'Select a compatible provider above to configure quality';
|
||||
|
||||
@override
|
||||
String get downloadEmbedLyricsDisabled =>
|
||||
|
||||
@@ -174,9 +174,9 @@
|
||||
"@optionsDefaultSearchTabSubtitle": {
|
||||
"description": "Subtitle for the preferred default search tab setting"
|
||||
},
|
||||
"optionsSwitchBack": "Tap Deezer or Spotify to switch back from extension",
|
||||
"optionsSwitchBack": "Choose another provider below to stop using the current extension",
|
||||
"@optionsSwitchBack": {
|
||||
"description": "Hint to switch back to built-in providers"
|
||||
"description": "Hint to switch away from the current provider selection"
|
||||
},
|
||||
"optionsAutoFallback": "Auto Fallback",
|
||||
"@optionsAutoFallback": {
|
||||
@@ -194,9 +194,9 @@
|
||||
"@optionsUseExtensionProvidersOn": {
|
||||
"description": "Status when extension providers enabled"
|
||||
},
|
||||
"optionsUseExtensionProvidersOff": "Using built-in providers only",
|
||||
"optionsUseExtensionProvidersOff": "Extensions are turned off",
|
||||
"@optionsUseExtensionProvidersOff": {
|
||||
"description": "Status when extension providers disabled"
|
||||
"description": "Status when extension providers are disabled"
|
||||
},
|
||||
"optionsEmbedLyrics": "Embed Lyrics",
|
||||
"@optionsEmbedLyrics": {
|
||||
@@ -3867,14 +3867,14 @@
|
||||
"@downloadNetworkCompatibilityModeDisabled": {
|
||||
"description": "Subtitle when network compatibility mode is disabled"
|
||||
},
|
||||
"downloadSelectServiceToEnable": "Select a built-in service to enable",
|
||||
"downloadSelectServiceToEnable": "Select a compatible download provider to enable quality options",
|
||||
"@downloadSelectServiceToEnable": {
|
||||
"description": "Hint shown instead of Ask-quality subtitle when no built-in service selected"
|
||||
"description": "Hint shown instead of Ask-quality subtitle when the selected provider does not expose built-in quality controls"
|
||||
},
|
||||
|
||||
"downloadSelectTidalQobuz": "Select Tidal or Qobuz above to configure quality",
|
||||
"downloadSelectTidalQobuz": "Select a compatible provider above to configure quality",
|
||||
"@downloadSelectTidalQobuz": {
|
||||
"description": "Info hint when non-Tidal/Qobuz service is selected"
|
||||
"description": "Info hint when the selected provider does not expose built-in quality controls"
|
||||
},
|
||||
"downloadEmbedLyricsDisabled": "Disabled while Embed Metadata is turned off",
|
||||
"@downloadEmbedLyricsDisabled": {
|
||||
|
||||
@@ -158,9 +158,9 @@
|
||||
"@optionsDefaultSearchTabSubtitle": {
|
||||
"description": "Subtitle for the preferred default search tab setting"
|
||||
},
|
||||
"optionsSwitchBack": "Ketuk Deezer atau Spotify untuk beralih dari ekstensi",
|
||||
"optionsSwitchBack": "Pilih provider lain di bawah untuk berhenti memakai ekstensi saat ini",
|
||||
"@optionsSwitchBack": {
|
||||
"description": "Hint to switch back to built-in providers"
|
||||
"description": "Hint to switch away from the current provider selection"
|
||||
},
|
||||
"optionsAutoFallback": "Cadangan Otomatis",
|
||||
"@optionsAutoFallback": {
|
||||
@@ -178,9 +178,9 @@
|
||||
"@optionsUseExtensionProvidersOn": {
|
||||
"description": "Status when extension providers enabled"
|
||||
},
|
||||
"optionsUseExtensionProvidersOff": "Hanya menggunakan provider bawaan",
|
||||
"optionsUseExtensionProvidersOff": "Ekstensi dimatikan",
|
||||
"@optionsUseExtensionProvidersOff": {
|
||||
"description": "Status when extension providers disabled"
|
||||
"description": "Status when extension providers are disabled"
|
||||
},
|
||||
"optionsEmbedLyrics": "Sematkan Lirik",
|
||||
"@optionsEmbedLyrics": {
|
||||
|
||||
@@ -150,9 +150,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"optionsSwitchBack": "Tap Deezer or Spotify to switch back from extension",
|
||||
"optionsSwitchBack": "現在の拡張の使用をやめるには、下から別のプロバイダーを選択してください",
|
||||
"@optionsSwitchBack": {
|
||||
"description": "Hint to switch back to built-in providers"
|
||||
"description": "Hint to switch away from the current provider selection"
|
||||
},
|
||||
"optionsAutoFallback": "Auto Fallback",
|
||||
"@optionsAutoFallback": {
|
||||
@@ -170,9 +170,9 @@
|
||||
"@optionsUseExtensionProvidersOn": {
|
||||
"description": "Status when extension providers enabled"
|
||||
},
|
||||
"optionsUseExtensionProvidersOff": "内蔵のプロバイダーのみを使用する",
|
||||
"optionsUseExtensionProvidersOff": "拡張はオフです",
|
||||
"@optionsUseExtensionProvidersOff": {
|
||||
"description": "Status when extension providers disabled"
|
||||
"description": "Status when extension providers are disabled"
|
||||
},
|
||||
"optionsEmbedLyrics": "歌詞を埋め込む",
|
||||
"@optionsEmbedLyrics": {
|
||||
@@ -3198,4 +3198,4 @@
|
||||
"@downloadUseAlbumArtistForFoldersTrackSubtitle": {
|
||||
"description": "Subtitle when Track Artist is used for folder naming"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,9 +150,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"optionsSwitchBack": "Eklentiden çıkıp varsayılana dönmek için Deezer veya Spotify'a dokunun",
|
||||
"optionsSwitchBack": "Geçerli eklentiyi bırakmak için aşağıdan başka bir sağlayıcı seçin",
|
||||
"@optionsSwitchBack": {
|
||||
"description": "Hint to switch back to built-in providers"
|
||||
"description": "Hint to switch away from the current provider selection"
|
||||
},
|
||||
"optionsAutoFallback": "Otomatik Geçiş",
|
||||
"@optionsAutoFallback": {
|
||||
@@ -170,9 +170,9 @@
|
||||
"@optionsUseExtensionProvidersOn": {
|
||||
"description": "Status when extension providers enabled"
|
||||
},
|
||||
"optionsUseExtensionProvidersOff": "Sadece yerleşik sağlayıcılar kullanılıyor",
|
||||
"optionsUseExtensionProvidersOff": "Eklentiler kapalı",
|
||||
"@optionsUseExtensionProvidersOff": {
|
||||
"description": "Status when extension providers disabled"
|
||||
"description": "Status when extension providers are disabled"
|
||||
},
|
||||
"optionsEmbedLyrics": "Şarkı Sözlerini Gömer",
|
||||
"@optionsEmbedLyrics": {
|
||||
@@ -3689,13 +3689,13 @@
|
||||
"@downloadNetworkCompatibilityModeDisabled": {
|
||||
"description": "Subtitle when network compatibility mode is disabled"
|
||||
},
|
||||
"downloadSelectServiceToEnable": "Seçenekleri açmak için yerleşik bir sağlayıcı seçin",
|
||||
"downloadSelectServiceToEnable": "Kalite seçeneklerini açmak için uyumlu bir indirme sağlayıcısı seçin",
|
||||
"@downloadSelectServiceToEnable": {
|
||||
"description": "Hint shown instead of Ask-quality subtitle when no built-in service selected"
|
||||
"description": "Hint shown instead of Ask-quality subtitle when the selected provider does not expose built-in quality controls"
|
||||
},
|
||||
"downloadSelectTidalQobuz": "Kaliteyi ayarlamak için lütfen yukarıdan Tidal veya Qobuz seçin",
|
||||
"downloadSelectTidalQobuz": "Kaliteyi ayarlamak için yukarıdan uyumlu bir sağlayıcı seçin",
|
||||
"@downloadSelectTidalQobuz": {
|
||||
"description": "Info hint when non-Tidal/Qobuz service is selected"
|
||||
"description": "Info hint when the selected provider does not expose built-in quality controls"
|
||||
},
|
||||
"downloadEmbedLyricsDisabled": "Şarkı Verilerini Dosyaya Gömme ayarı kapalıyken kullanılamaz",
|
||||
"@downloadEmbedLyricsDisabled": {
|
||||
|
||||
@@ -4815,7 +4815,7 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
String payloadTidalId = '';
|
||||
if (trackToDownload.id.startsWith('qobuz:')) {
|
||||
payloadQobuzId = trackToDownload.id.substring(6);
|
||||
if (item.service == 'qobuz') {
|
||||
if (_usesBuiltInCompatibleDownloadProvider(item.service, 'qobuz')) {
|
||||
payloadSpotifyId = '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -288,6 +288,85 @@ List<String> get builtInDownloadProviderIds => List<String>.unmodifiable(
|
||||
builtInDownloadProviderSpecs.map((provider) => provider.id),
|
||||
);
|
||||
|
||||
String resolveEffectiveDownloadService(
|
||||
String requestedService,
|
||||
ExtensionState extensionState,
|
||||
) {
|
||||
final normalizedRequested = requestedService.trim().toLowerCase();
|
||||
final builtInDownloadIds = extensionState.builtInProviders
|
||||
.where((provider) => provider.supportsDownload)
|
||||
.map((provider) => provider.id.trim().toLowerCase())
|
||||
.where((providerId) => providerId.isNotEmpty)
|
||||
.toSet();
|
||||
final enabledDownloadExtensions = extensionState.extensions
|
||||
.where((ext) => ext.enabled && ext.hasDownloadProvider)
|
||||
.toList(growable: false);
|
||||
|
||||
if (normalizedRequested.isNotEmpty) {
|
||||
if (builtInDownloadIds.contains(normalizedRequested)) {
|
||||
return normalizedRequested;
|
||||
}
|
||||
|
||||
final matchingExtension = enabledDownloadExtensions
|
||||
.where((ext) => ext.id.trim().toLowerCase() == normalizedRequested)
|
||||
.firstOrNull;
|
||||
if (matchingExtension != null) {
|
||||
return matchingExtension.id;
|
||||
}
|
||||
|
||||
final replacementExtension = enabledDownloadExtensions
|
||||
.where(
|
||||
(ext) => ext.replacesBuiltInProviders.contains(normalizedRequested),
|
||||
)
|
||||
.firstOrNull;
|
||||
if (replacementExtension != null) {
|
||||
return replacementExtension.id;
|
||||
}
|
||||
}
|
||||
|
||||
const preferredBuiltInOrder = ['tidal', 'qobuz', 'deezer'];
|
||||
for (final builtInId in preferredBuiltInOrder) {
|
||||
final replacement = enabledDownloadExtensions
|
||||
.where((ext) => ext.replacesBuiltInProviders.contains(builtInId))
|
||||
.firstOrNull;
|
||||
if (replacement != null) {
|
||||
return replacement.id;
|
||||
}
|
||||
if (builtInDownloadIds.contains(builtInId)) {
|
||||
return builtInId;
|
||||
}
|
||||
}
|
||||
|
||||
return enabledDownloadExtensions.firstOrNull?.id ??
|
||||
extensionState.builtInProviders
|
||||
.where((provider) => provider.supportsDownload)
|
||||
.map((provider) => provider.id)
|
||||
.firstOrNull ??
|
||||
'';
|
||||
}
|
||||
|
||||
bool isDeezerCompatibleDownloadService(
|
||||
String service,
|
||||
ExtensionState extensionState,
|
||||
) {
|
||||
final normalizedService = service.trim().toLowerCase();
|
||||
if (normalizedService.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (normalizedService == 'deezer') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return extensionState.extensions.any(
|
||||
(ext) =>
|
||||
ext.enabled &&
|
||||
ext.hasDownloadProvider &&
|
||||
ext.id.trim().toLowerCase() == normalizedService &&
|
||||
ext.replacesBuiltInProviders.contains('deezer'),
|
||||
);
|
||||
}
|
||||
|
||||
bool isBuiltInSearchProvider(String? providerId) =>
|
||||
builtInProviderSpecForId(providerId)?.supportsSearch ?? false;
|
||||
|
||||
|
||||
@@ -599,7 +599,6 @@ class TrackNotifier extends Notifier<TrackState> {
|
||||
if (effectiveBuiltInProvider == null || effectiveBuiltInProvider.isEmpty) {
|
||||
state = TrackState(
|
||||
isLoading: false,
|
||||
error: 'No active search provider available',
|
||||
hasSearchText: state.hasSearchText,
|
||||
isShowingRecentAccess: state.isShowingRecentAccess,
|
||||
selectedSearchFilter: currentFilter,
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:spotiflac_android/services/cover_cache_manager.dart';
|
||||
import 'package:spotiflac_android/l10n/l10n.dart';
|
||||
import 'package:spotiflac_android/models/track.dart';
|
||||
import 'package:spotiflac_android/providers/download_queue_provider.dart';
|
||||
import 'package:spotiflac_android/providers/extension_provider.dart';
|
||||
import 'package:spotiflac_android/providers/settings_provider.dart';
|
||||
import 'package:spotiflac_android/providers/recent_access_provider.dart';
|
||||
import 'package:spotiflac_android/providers/local_library_provider.dart';
|
||||
@@ -644,9 +645,20 @@ class _AlbumScreenState extends ConsumerState<AlbumScreen> {
|
||||
},
|
||||
);
|
||||
} else {
|
||||
final extensionState = ref.read(extensionProvider);
|
||||
final service = resolveEffectiveDownloadService(
|
||||
settings.defaultService,
|
||||
extensionState,
|
||||
);
|
||||
if (service.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(context.l10n.extensionsNoDownloadProvider)),
|
||||
);
|
||||
return;
|
||||
}
|
||||
ref
|
||||
.read(downloadQueueProvider.notifier)
|
||||
.addToQueue(track, settings.defaultService);
|
||||
.addToQueue(track, service);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(context.l10n.snackbarAddedToQueue(track.name))),
|
||||
);
|
||||
@@ -716,9 +728,20 @@ class _AlbumScreenState extends ConsumerState<AlbumScreen> {
|
||||
},
|
||||
);
|
||||
} else {
|
||||
final extensionState = ref.read(extensionProvider);
|
||||
final service = resolveEffectiveDownloadService(
|
||||
settings.defaultService,
|
||||
extensionState,
|
||||
);
|
||||
if (service.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(context.l10n.extensionsNoDownloadProvider)),
|
||||
);
|
||||
return;
|
||||
}
|
||||
ref
|
||||
.read(downloadQueueProvider.notifier)
|
||||
.addMultipleToQueue(tracksToQueue, settings.defaultService);
|
||||
.addMultipleToQueue(tracksToQueue, service);
|
||||
_showQueuedSnackbar(context, tracksToQueue.length, skippedCount);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:intl/intl.dart';
|
||||
import 'package:spotiflac_android/services/cover_cache_manager.dart';
|
||||
import 'package:spotiflac_android/l10n/l10n.dart';
|
||||
import 'package:spotiflac_android/models/track.dart';
|
||||
import 'package:spotiflac_android/providers/extension_provider.dart';
|
||||
import 'package:spotiflac_android/providers/track_provider.dart';
|
||||
import 'package:spotiflac_android/providers/settings_provider.dart';
|
||||
import 'package:spotiflac_android/providers/download_queue_provider.dart';
|
||||
@@ -1627,7 +1628,18 @@ class _ArtistScreenState extends ConsumerState<ArtistScreen> {
|
||||
return;
|
||||
}
|
||||
|
||||
enqueue(settings.defaultService);
|
||||
final extensionState = ref.read(extensionProvider);
|
||||
final service = resolveEffectiveDownloadService(
|
||||
settings.defaultService,
|
||||
extensionState,
|
||||
);
|
||||
if (service.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(context.l10n.extensionsNoDownloadProvider)),
|
||||
);
|
||||
return;
|
||||
}
|
||||
enqueue(service);
|
||||
}
|
||||
|
||||
Widget _buildAlbumSection(
|
||||
|
||||
@@ -1004,9 +1004,20 @@ class _HomeTabState extends ConsumerState<HomeTab>
|
||||
},
|
||||
);
|
||||
} else {
|
||||
final extensionState = ref.read(extensionProvider);
|
||||
final service = resolveEffectiveDownloadService(
|
||||
settings.defaultService,
|
||||
extensionState,
|
||||
);
|
||||
if (service.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(context.l10n.extensionsNoDownloadProvider)),
|
||||
);
|
||||
return;
|
||||
}
|
||||
ref
|
||||
.read(downloadQueueProvider.notifier)
|
||||
.addToQueue(track, settings.defaultService);
|
||||
.addToQueue(track, service);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.snackbarAddedToQueue(track.name)),
|
||||
@@ -1227,9 +1238,24 @@ class _HomeTabState extends ConsumerState<HomeTab>
|
||||
},
|
||||
);
|
||||
} else {
|
||||
final extensionState = ref.read(extensionProvider);
|
||||
final service = resolveEffectiveDownloadService(
|
||||
settings.defaultService,
|
||||
extensionState,
|
||||
);
|
||||
if (service.isEmpty) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(this.context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(this.context.l10n.extensionsNoDownloadProvider),
|
||||
),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
ref
|
||||
.read(downloadQueueProvider.notifier)
|
||||
.addMultipleToQueue(tracksToQueue, settings.defaultService);
|
||||
.addMultipleToQueue(tracksToQueue, service);
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(this.context).showSnackBar(
|
||||
SnackBar(
|
||||
@@ -2139,9 +2165,20 @@ class _HomeTabState extends ConsumerState<HomeTab>
|
||||
},
|
||||
);
|
||||
} else {
|
||||
final extensionState = ref.read(extensionProvider);
|
||||
final service = resolveEffectiveDownloadService(
|
||||
settings.defaultService,
|
||||
extensionState,
|
||||
);
|
||||
if (service.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(context.l10n.extensionsNoDownloadProvider)),
|
||||
);
|
||||
return;
|
||||
}
|
||||
ref
|
||||
.read(downloadQueueProvider.notifier)
|
||||
.addToQueue(track, settings.defaultService);
|
||||
.addToQueue(track, service);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(context.l10n.snackbarAddedToQueue(track.name))),
|
||||
);
|
||||
|
||||
@@ -8,6 +8,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:spotiflac_android/l10n/l10n.dart';
|
||||
import 'package:spotiflac_android/models/track.dart';
|
||||
import 'package:spotiflac_android/providers/download_queue_provider.dart';
|
||||
import 'package:spotiflac_android/providers/extension_provider.dart';
|
||||
import 'package:spotiflac_android/providers/library_collections_provider.dart';
|
||||
import 'package:spotiflac_android/providers/playback_provider.dart';
|
||||
import 'package:spotiflac_android/providers/local_library_provider.dart';
|
||||
@@ -214,12 +215,23 @@ class _LibraryTracksFolderScreenState
|
||||
|
||||
void _downloadSelected(List<CollectionTrackEntry> entries) {
|
||||
final settings = ref.read(settingsProvider);
|
||||
final extensionState = ref.read(extensionProvider);
|
||||
final service = resolveEffectiveDownloadService(
|
||||
settings.defaultService,
|
||||
extensionState,
|
||||
);
|
||||
if (service.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(context.l10n.extensionsNoDownloadProvider)),
|
||||
);
|
||||
return;
|
||||
}
|
||||
final queueNotifier = ref.read(downloadQueueProvider.notifier);
|
||||
var count = 0;
|
||||
|
||||
for (final entry in entries) {
|
||||
if (!_selectedKeys.contains(entry.key)) continue;
|
||||
queueNotifier.addToQueue(entry.track, settings.defaultService);
|
||||
queueNotifier.addToQueue(entry.track, service);
|
||||
count++;
|
||||
}
|
||||
|
||||
@@ -1349,9 +1361,20 @@ class _CollectionTrackTile extends ConsumerWidget {
|
||||
},
|
||||
);
|
||||
} else {
|
||||
final extensionState = ref.read(extensionProvider);
|
||||
final service = resolveEffectiveDownloadService(
|
||||
settings.defaultService,
|
||||
extensionState,
|
||||
);
|
||||
if (service.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(context.l10n.extensionsNoDownloadProvider)),
|
||||
);
|
||||
return;
|
||||
}
|
||||
ref
|
||||
.read(downloadQueueProvider.notifier)
|
||||
.addToQueue(track, settings.defaultService);
|
||||
.addToQueue(track, service);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(context.l10n.snackbarAddedToQueue(track.name))),
|
||||
);
|
||||
|
||||
@@ -965,10 +965,18 @@ class _LocalAlbumScreenState extends ConsumerState<LocalAlbumScreen> {
|
||||
);
|
||||
final targetService = LocalTrackRedownloadService.preferredFlacService(
|
||||
settings,
|
||||
extensionState,
|
||||
);
|
||||
if (targetService.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(context.l10n.extensionsNoDownloadProvider)),
|
||||
);
|
||||
return;
|
||||
}
|
||||
final targetQuality =
|
||||
LocalTrackRedownloadService.preferredFlacQualityForService(
|
||||
targetService,
|
||||
extensionState,
|
||||
);
|
||||
|
||||
final matchedTracks = <Track>[];
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:spotiflac_android/services/platform_bridge.dart';
|
||||
import 'package:spotiflac_android/l10n/l10n.dart';
|
||||
import 'package:spotiflac_android/models/track.dart';
|
||||
import 'package:spotiflac_android/providers/download_queue_provider.dart';
|
||||
import 'package:spotiflac_android/providers/extension_provider.dart';
|
||||
import 'package:spotiflac_android/providers/library_collections_provider.dart';
|
||||
import 'package:spotiflac_android/utils/file_access.dart';
|
||||
import 'package:spotiflac_android/utils/image_cache_utils.dart';
|
||||
@@ -495,11 +496,22 @@ class _PlaylistScreenState extends ConsumerState<PlaylistScreen> {
|
||||
},
|
||||
);
|
||||
} else {
|
||||
final extensionState = ref.read(extensionProvider);
|
||||
final service = resolveEffectiveDownloadService(
|
||||
settings.defaultService,
|
||||
extensionState,
|
||||
);
|
||||
if (service.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(context.l10n.extensionsNoDownloadProvider)),
|
||||
);
|
||||
return;
|
||||
}
|
||||
ref
|
||||
.read(downloadQueueProvider.notifier)
|
||||
.addToQueue(
|
||||
track,
|
||||
settings.defaultService,
|
||||
service,
|
||||
playlistName: _playlistName,
|
||||
);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
|
||||
@@ -1845,10 +1845,24 @@ class _QueueTabState extends ConsumerState<QueueTab> {
|
||||
if (confirmed != true || !context.mounted) return;
|
||||
|
||||
final settings = ref.read(settingsProvider);
|
||||
final extensionState = ref.read(extensionProvider);
|
||||
final queueNotifier = ref.read(downloadQueueProvider.notifier);
|
||||
|
||||
void enqueueAll({String? qualityOverride, String? service}) {
|
||||
final svc = service ?? settings.defaultService;
|
||||
final svc =
|
||||
service ??
|
||||
resolveEffectiveDownloadService(
|
||||
settings.defaultService,
|
||||
extensionState,
|
||||
);
|
||||
if (svc.isEmpty) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(context.l10n.extensionsNoDownloadProvider)),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
for (final playlist in selectedPlaylists) {
|
||||
final tracks = playlist.tracks.map((e) => e.track).toList();
|
||||
queueNotifier.addMultipleToQueue(
|
||||
@@ -5218,10 +5232,18 @@ class _QueueTabState extends ConsumerState<QueueTab> {
|
||||
);
|
||||
final targetService = LocalTrackRedownloadService.preferredFlacService(
|
||||
settings,
|
||||
extensionState,
|
||||
);
|
||||
if (targetService.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(context.l10n.extensionsNoDownloadProvider)),
|
||||
);
|
||||
return;
|
||||
}
|
||||
final targetQuality =
|
||||
LocalTrackRedownloadService.preferredFlacQualityForService(
|
||||
targetService,
|
||||
extensionState,
|
||||
);
|
||||
|
||||
final matchedTracks = <Track>[];
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:spotiflac_android/l10n/l10n.dart';
|
||||
import 'package:spotiflac_android/services/cover_cache_manager.dart';
|
||||
import 'package:spotiflac_android/models/track.dart';
|
||||
import 'package:spotiflac_android/providers/extension_provider.dart';
|
||||
import 'package:spotiflac_android/providers/track_provider.dart';
|
||||
import 'package:spotiflac_android/providers/download_queue_provider.dart';
|
||||
import 'package:spotiflac_android/providers/settings_provider.dart';
|
||||
@@ -49,9 +50,20 @@ class _SearchScreenState extends ConsumerState<SearchScreen> {
|
||||
|
||||
void _downloadTrack(Track track) {
|
||||
final settings = ref.read(settingsProvider);
|
||||
final extensionState = ref.read(extensionProvider);
|
||||
final service = resolveEffectiveDownloadService(
|
||||
settings.defaultService,
|
||||
extensionState,
|
||||
);
|
||||
if (service.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(context.l10n.extensionsNoDownloadProvider)),
|
||||
);
|
||||
return;
|
||||
}
|
||||
ref
|
||||
.read(downloadQueueProvider.notifier)
|
||||
.addToQueue(track, settings.defaultService);
|
||||
.addToQueue(track, service);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(context.l10n.snackbarAddedToQueue(track.name))),
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:spotiflac_android/models/settings.dart';
|
||||
import 'package:spotiflac_android/models/track.dart';
|
||||
import 'package:spotiflac_android/providers/extension_provider.dart';
|
||||
import 'package:spotiflac_android/services/library_database.dart';
|
||||
import 'package:spotiflac_android/services/platform_bridge.dart';
|
||||
|
||||
@@ -101,19 +102,26 @@ class LocalTrackRedownloadService {
|
||||
);
|
||||
}
|
||||
|
||||
static String preferredFlacService(AppSettings settings) {
|
||||
switch (settings.defaultService.toLowerCase()) {
|
||||
case 'tidal':
|
||||
case 'qobuz':
|
||||
case 'deezer':
|
||||
return settings.defaultService.toLowerCase();
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
static String preferredFlacService(
|
||||
AppSettings settings,
|
||||
ExtensionState extensionState,
|
||||
) {
|
||||
return resolveEffectiveDownloadService(
|
||||
settings.defaultService,
|
||||
extensionState,
|
||||
);
|
||||
}
|
||||
|
||||
static String preferredFlacQualityForService(String service) {
|
||||
return service.toLowerCase() == 'deezer' ? 'FLAC' : 'LOSSLESS';
|
||||
static String preferredFlacQualityForService(
|
||||
String service,
|
||||
ExtensionState extensionState,
|
||||
) {
|
||||
if (service.trim().isEmpty) {
|
||||
return 'LOSSLESS';
|
||||
}
|
||||
return isDeezerCompatibleDownloadService(service, extensionState)
|
||||
? 'FLAC'
|
||||
: 'LOSSLESS';
|
||||
}
|
||||
|
||||
static String _buildSearchQuery(LocalLibraryItem item) {
|
||||
|
||||
Reference in New Issue
Block a user