diff --git a/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt b/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt index f762b0a3..baaba090 100644 --- a/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt +++ b/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt @@ -2795,23 +2795,6 @@ class MainActivity: FlutterFragmentActivity() { } result.success(null) } - "searchProviderAll" -> { - val providerId = call.argument("provider_id") ?: "" - val query = call.argument("query") ?: "" - val trackLimit = call.argument("track_limit") ?: 15 - val artistLimit = call.argument("artist_limit") ?: 2 - val filter = call.argument("filter") ?: "" - val response = withContext(Dispatchers.IO) { - Gobackend.searchProviderAllJSON(providerId, query, trackLimit.toLong(), artistLimit.toLong(), filter) - } - result.success(response) - } - "getBuiltInProviders" -> { - val response = withContext(Dispatchers.IO) { - Gobackend.getBuiltInProvidersJSON() - } - result.success(response) - } "getDeezerRelatedArtists" -> { val artistId = call.argument("artist_id") ?: "" val limit = call.argument("limit") ?: 12 @@ -2829,13 +2812,6 @@ class MainActivity: FlutterFragmentActivity() { } result.success(response) } - "parseProviderUrl" -> { - val url = call.argument("url") ?: "" - val response = withContext(Dispatchers.IO) { - Gobackend.parseProviderURLJSON(url) - } - result.success(response) - } "searchDeezerByISRC" -> { val isrc = call.argument("isrc") ?: "" val itemId = call.argument("item_id") ?: "" diff --git a/android/app/src/main/kotlin/com/zarz/spotiflac/SafDownloadHandler.kt b/android/app/src/main/kotlin/com/zarz/spotiflac/SafDownloadHandler.kt index 6ab54380..742b5841 100644 --- a/android/app/src/main/kotlin/com/zarz/spotiflac/SafDownloadHandler.kt +++ b/android/app/src/main/kotlin/com/zarz/spotiflac/SafDownloadHandler.kt @@ -205,6 +205,7 @@ object SafDownloadHandler { mimeType: String, srcPath: String ): String? { + var stagedDocument: DocumentFile? = null return try { val treeUri = Uri.parse(treeUriStr) val targetDir = ensureDocumentDir(context, treeUri, relativeDir) ?: return null @@ -213,11 +214,18 @@ object SafDownloadHandler { val stagedName = buildStagedSafFileName(finalName, ext) val document = createOrReuseDocumentFile(targetDir, mimeType, stagedName) ?: return null - context.contentResolver.openOutputStream(document.uri, "wt")?.use { output -> + stagedDocument = document + val outputStream = context.contentResolver.openOutputStream(document.uri, "wt") + if (outputStream == null) { + document.delete() + stagedDocument = null + return null + } + outputStream.use { output -> File(srcPath).inputStream().use { input -> input.copyTo(output) } - } ?: return null + } val existingFinal = targetDir.findFile(finalName) if (existingFinal != null && existingFinal.uri != document.uri) { @@ -227,8 +235,10 @@ object SafDownloadHandler { document.delete() return null } + stagedDocument = null targetDir.findFile(finalName)?.uri?.toString() ?: document.uri.toString() } catch (e: Exception) { + stagedDocument?.delete() android.util.Log.w("SpotiFLAC", "Failed to write file to SAF: ${e.message}") null } diff --git a/go_backend/exports.go b/go_backend/exports.go index 25566881..285c0d0b 100644 --- a/go_backend/exports.go +++ b/go_backend/exports.go @@ -1782,28 +1782,6 @@ func ClearTrackIDCache() { ClearTrackCache() } -func SearchProviderAllJSON( - providerID, - query string, - trackLimit, - artistLimit int, - filter string, -) (string, error) { - normalizedProviderID := strings.ToLower(strings.TrimSpace(providerID)) - if !isBuiltInSearchProvider(normalizedProviderID) { - return "", fmt.Errorf("unsupported search provider: %s", providerID) - } - return searchBuiltInProviderAll(normalizedProviderID, query, trackLimit, artistLimit, filter) -} - -func GetBuiltInProvidersJSON() (string, error) { - jsonBytes, err := json.Marshal(getBuiltInProviderSpecs()) - if err != nil { - return "", err - } - return string(jsonBytes), nil -} - func GetDeezerRelatedArtists(artistID string, limit int) (string, error) { ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) defer cancel() @@ -2076,12 +2054,7 @@ func GetProviderMetadataJSON(providerID, resourceType, resourceID string) (strin return "", fmt.Errorf("empty provider ID") } - normalizedProviderID := strings.ToLower(trimmedProviderID) - if isBuiltInMetadataProvider(normalizedProviderID) { - return getBuiltInProviderMetadata(normalizedProviderID, resourceType, resourceID) - } - - switch normalizedProviderID { + switch strings.ToLower(trimmedProviderID) { case "deezer": return GetDeezerMetadata(resourceType, resourceID) default: @@ -2098,55 +2071,6 @@ func GetProviderMetadataJSON(providerID, resourceType, resourceID string) (strin } } -func ParseDeezerURLExport(url string) (string, error) { - resourceType, resourceID, err := parseDeezerURL(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 - parse func(string) (string, string, error) - }{ - {providerID: "deezer", parse: parseDeezerURL}, - } - - for _, parser := range parsers { - resourceType, resourceID, err := parser.parse(url) - if err != nil { - continue - } - - result := map[string]string{ - "provider_id": parser.providerID, - "type": resourceType, - "id": resourceID, - } - - jsonBytes, err := json.Marshal(result) - if err != nil { - return "", err - } - return string(jsonBytes), nil - } - - return "", fmt.Errorf("unsupported provider URL") -} - func GetDeezerExtendedMetadata(trackID string) (string, error) { if trackID == "" { return "", fmt.Errorf("empty track ID") diff --git a/go_backend/exports_supplement_test.go b/go_backend/exports_supplement_test.go index 25e7b615..df23af30 100644 --- a/go_backend/exports_supplement_test.go +++ b/go_backend/exports_supplement_test.go @@ -199,13 +199,6 @@ func TestExportsJSONWrappersAndExtensionManagerSurface(t *testing.T) { if firstNonEmptyTrimmed(" ", " value ") != "value" { t.Fatal("expected first trimmed value") } - if jsonText, err := GetBuiltInProvidersJSON(); err != nil || jsonText == "" { - t.Fatalf("GetBuiltInProvidersJSON = %q/%v", jsonText, err) - } - if _, err := SearchProviderAllJSON("missing", "q", 1, 1, ""); err == nil { - t.Fatal("expected unsupported search provider") - } - requestJSON := `{"use_extensions":true,"use_fallback":false,"service":"coverage-ext","source":"coverage-ext","track_name":"Song","artist_name":"Artist","album_name":"Album","output_dir":"` + escapeJSONPath(dir) + `","output_ext":".flac","quality":"LOSSLESS"}` if jsonText, err := DownloadWithExtensionsJSON(requestJSON); err != nil || !strings.Contains(jsonText, "coverage-ext") { t.Fatalf("DownloadWithExtensionsJSON = %q/%v", jsonText, err) @@ -286,15 +279,6 @@ func TestExportsJSONWrappersAndExtensionManagerSurface(t *testing.T) { if jsonText, err := GetDeezerRelatedArtists("301", 2); err != nil || !strings.Contains(jsonText, "Related") { t.Fatalf("GetDeezerRelatedArtists = %q/%v", jsonText, err) } - if jsonText, err := ParseDeezerURLExport("https://www.deezer.com/track/101"); err != nil || !strings.Contains(jsonText, "101") { - t.Fatalf("ParseDeezerURLExport = %q/%v", jsonText, err) - } - if jsonText, err := ParseProviderURLJSON("https://www.deezer.com/album/201"); err != nil || !strings.Contains(jsonText, "deezer") { - t.Fatalf("ParseProviderURLJSON = %q/%v", jsonText, err) - } - if _, err := ParseProviderURLJSON("https://example.com/1"); err == nil { - t.Fatal("expected unsupported provider URL") - } if jsonText, err := GetDeezerExtendedMetadata("101"); err != nil || !strings.Contains(jsonText, "Label") { t.Fatalf("GetDeezerExtendedMetadata = %q/%v", jsonText, err) } diff --git a/go_backend/extension_provider_supplement_test.go b/go_backend/extension_provider_supplement_test.go index 4d775677..d46b569d 100644 --- a/go_backend/extension_provider_supplement_test.go +++ b/go_backend/extension_provider_supplement_test.go @@ -1,14 +1,8 @@ package gobackend import ( - "context" - "encoding/json" - "io" - "net/http" "path/filepath" - "strings" "testing" - "time" ) func TestExtensionProviderWrapperFullSurface(t *testing.T) { @@ -126,79 +120,7 @@ func TestExtensionProviderWrapperFullSurface(t *testing.T) { } } -func TestBuiltInProviderAndManagerSelectionHelpers(t *testing.T) { - previousRegistry := builtInProviderRegistry - builtInProviderRegistry = []builtInProviderSpec{{ - ID: "deezer", - DisplayName: "Deezer", - SupportsMetadata: true, - SupportsSearch: true, - GetMetadata: GetDeezerMetadata, - SearchAll: func(query string, trackLimit, artistLimit int, filter string) (string, error) { - ctx, cancel := context.WithTimeout(context.Background(), time.Second) - defer cancel() - result, err := GetDeezerClient().SearchAll(ctx, query, trackLimit, artistLimit, filter) - if err != nil { - return "", err - } - data, err := json.Marshal(result) - return string(data), err - }, - SearchTracks: func(query string, limit int) ([]ExtTrackMetadata, error) { - ctx, cancel := context.WithTimeout(context.Background(), time.Second) - defer cancel() - result, err := GetDeezerClient().SearchAll(ctx, query, limit, limit, "track") - if err != nil { - return nil, err - } - tracks := make([]ExtTrackMetadata, len(result.Tracks)) - for i, track := range result.Tracks { - tracks[i] = normalizeBuiltInMetadataTrack(track, "deezer") - } - return tracks, nil - }, - }} - defer func() { builtInProviderRegistry = previousRegistry }() - - deezerClient = &DeezerClient{ - httpClient: &http.Client{Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) { - body := fakeDeezerResponse(req.URL.Path, req.URL.RawQuery) - if body == "" { - body = `{"data":[]}` - } - return &http.Response{StatusCode: http.StatusOK, Header: make(http.Header), Body: io.NopCloser(strings.NewReader(body)), Request: req}, nil - })}, - searchCache: map[string]*cacheEntry{}, - albumCache: map[string]*cacheEntry{}, - artistCache: map[string]*cacheEntry{}, - isrcCache: map[string]string{}, - cacheCleanupInterval: time.Hour, - } - deezerClientOnce.Do(func() {}) - - if !isBuiltInProvider("deezer") || !isBuiltInMetadataProvider("deezer") || !isBuiltInSearchProvider("deezer") { - t.Fatal("expected Deezer built-in provider") - } - if _, ok := getBuiltInProviderSpec(" missing "); ok { - t.Fatal("unexpected missing provider spec") - } - if _, err := getBuiltInProviderMetadata("missing", "track", "1"); err == nil { - t.Fatal("expected unsupported metadata provider") - } - if jsonText, err := getBuiltInProviderMetadata("deezer", "track", "101"); err != nil || !strings.Contains(jsonText, "Track 101") { - t.Fatalf("built-in metadata = %q/%v", jsonText, err) - } - if jsonText, err := searchBuiltInProviderAll("deezer", "artist song", 2, 2, "track"); err != nil || !strings.Contains(jsonText, "Track 101") { - t.Fatalf("built-in search all = %q/%v", jsonText, err) - } - tracks, err := searchBuiltInProviderTracks("deezer", "artist song", 2) - if err != nil || len(tracks) != 1 || tracks[0].ProviderID != "deezer" { - t.Fatalf("built-in tracks = %#v/%v", tracks, err) - } - if _, err := searchBuiltInProviderTracks("missing", "q", 1); err == nil { - t.Fatal("expected unsupported built-in tracks") - } - +func TestExtensionProviderAndManagerSelectionHelpers(t *testing.T) { manifest := &ExtensionManifest{Capabilities: map[string]interface{}{ "replacesBuiltInProviders": []interface{}{" Deezer ", 7, ""}, }} @@ -211,22 +133,11 @@ func TestBuiltInProviderAndManagerSelectionHelpers(t *testing.T) { if trimKnownProviderPrefix("Deezer:101", "deezer") != "101" || trimKnownProviderPrefix("101", "deezer") != "101" { t.Fatal("trimKnownProviderPrefix mismatch") } - normalized := normalizeBuiltInMetadataTrack(TrackMetadata{SpotifyID: "deezer:101", Name: "Song", Artists: "Artist", ISRC: "ISRC"}, "deezer") - if normalized.DeezerID != "101" || normalized.ProviderID != "deezer" { - t.Fatalf("normalized built-in track = %#v", normalized) - } if metadataTrackDedupKey(ExtTrackMetadata{ISRC: "usrc"}) != "isrc:USRC" || metadataTrackDedupKey(ExtTrackMetadata{SpotifyID: "sp"}) != "spotify:sp" || metadataTrackDedupKey(ExtTrackMetadata{ProviderID: "p", ID: "1"}) != "p:1" { t.Fatal("metadata dedup key mismatch") } - searchBuiltInMetadataTracksFunc = func(providerID, query string, limit int) ([]ExtTrackMetadata, error) { - return []ExtTrackMetadata{{ID: "built-in", ProviderID: providerID}}, nil - } - defer func() { searchBuiltInMetadataTracksFunc = searchBuiltInMetadataTracks }() - if tracks, err := searchBuiltInMetadataTracksForItemID("deezer", "q", 1, "item"); err != nil || len(tracks) != 1 { - t.Fatalf("searchBuiltInMetadataTracksForItemID = %#v/%v", tracks, err) - } manager := &extensionManager{extensions: map[string]*loadedExtension{}} downloadExt := newTestLoadedExtension(t, ExtensionTypeDownloadProvider, ExtensionTypeMetadataProvider) @@ -247,7 +158,7 @@ func TestBuiltInProviderAndManagerSelectionHelpers(t *testing.T) { t.Fatal("nil fallback list should allow all") } SetMetadataProviderPriority([]string{"spotify", "deezer", "coverage-ext", "coverage-ext"}) - if priority := GetMetadataProviderPriority(); len(priority) != 2 || priority[0] != "deezer" || priority[1] != "coverage-ext" { + if priority := GetMetadataProviderPriority(); len(priority) != 1 || priority[0] != "coverage-ext" { t.Fatalf("metadata priority = %#v", priority) } } diff --git a/go_backend/extension_providers.go b/go_backend/extension_providers.go index bfdd7e3e..0aac5418 100644 --- a/go_backend/extension_providers.go +++ b/go_backend/extension_providers.go @@ -101,58 +101,6 @@ type ExtDownloadURLResult struct { SampleRate int `json:"sample_rate,omitempty"` } -type builtInProviderSpec struct { - ID string `json:"id"` - DisplayName string `json:"display_name"` - SupportsMetadata bool `json:"supports_metadata"` - SupportsSearch bool `json:"supports_search"` - GetMetadata func(resourceType, resourceID string) (string, error) `json:"-"` - SearchAll func(query string, trackLimit, artistLimit int, filter string) (string, error) `json:"-"` - SearchTracks func(query string, limit int) ([]ExtTrackMetadata, error) `json:"-"` -} - -var builtInProviderRegistry = []builtInProviderSpec{} - -func getBuiltInProviderSpecs() []builtInProviderSpec { - specs := make([]builtInProviderSpec, len(builtInProviderRegistry)) - copy(specs, builtInProviderRegistry) - return specs -} - -func getBuiltInProviderSpec(providerID string) (builtInProviderSpec, bool) { - normalized := strings.ToLower(strings.TrimSpace(providerID)) - for _, spec := range builtInProviderRegistry { - if spec.ID == normalized { - return spec, true - } - } - return builtInProviderSpec{}, false -} - -func getBuiltInProviderMetadata(providerID, resourceType, resourceID string) (string, error) { - spec, ok := getBuiltInProviderSpec(providerID) - if !ok || !spec.SupportsMetadata || spec.GetMetadata == nil { - return "", fmt.Errorf("unsupported built-in metadata provider: %s", providerID) - } - return spec.GetMetadata(resourceType, resourceID) -} - -func searchBuiltInProviderAll(providerID, query string, trackLimit, artistLimit int, filter string) (string, error) { - spec, ok := getBuiltInProviderSpec(providerID) - if !ok || !spec.SupportsSearch || spec.SearchAll == nil { - return "", fmt.Errorf("unsupported search provider: %s", providerID) - } - return spec.SearchAll(query, trackLimit, artistLimit, filter) -} - -func searchBuiltInProviderTracks(providerID, query string, limit int) ([]ExtTrackMetadata, error) { - spec, ok := getBuiltInProviderSpec(providerID) - if !ok || !spec.SupportsMetadata || spec.SearchTracks == nil { - return nil, fmt.Errorf("unsupported built-in metadata provider: %s", providerID) - } - return spec.SearchTracks(query, limit) -} - func manifestCapabilityStringList(manifest *ExtensionManifest, key string) []string { if manifest == nil || manifest.Capabilities == nil { return nil @@ -1783,40 +1731,6 @@ var extensionFallbackProviderIDsMu sync.RWMutex var metadataProviderPriority []string var metadataProviderPriorityMu sync.RWMutex -var searchBuiltInMetadataTracksFunc = searchBuiltInMetadataTracks - -func searchBuiltInMetadataTracksForItemID(providerID, query string, limit int, itemID string) ([]ExtTrackMetadata, error) { - if itemID == "" { - return searchBuiltInMetadataTracksFunc(providerID, query, limit) - } - - ctx := initDownloadCancel(itemID) - defer clearDownloadCancel(itemID) - if isDownloadCancelled(itemID) { - return nil, ErrDownloadCancelled - } - - type searchResult struct { - tracks []ExtTrackMetadata - err error - } - done := make(chan searchResult, 1) - go func() { - tracks, err := searchBuiltInMetadataTracksFunc(providerID, query, limit) - done <- searchResult{tracks: tracks, err: err} - }() - - select { - case <-ctx.Done(): - return nil, ErrDownloadCancelled - case result := <-done: - if isDownloadCancelled(itemID) { - return nil, ErrDownloadCancelled - } - return result.tracks, result.err - } -} - func SetProviderPriority(providerIDs []string) { providerPriorityMu.Lock() defer providerPriorityMu.Unlock() @@ -1880,11 +1794,8 @@ func isRetiredBuiltInMetadataProvider(providerID string) bool { if normalized == "" { return false } - if isBuiltInMetadataProvider(normalized) { - return false - } switch normalized { - case "spotify", "qobuz", "tidal": + case "deezer", "spotify", "qobuz", "tidal": return true default: return false @@ -1980,61 +1891,6 @@ func GetMetadataProviderPriority() []string { return result } -func isBuiltInProvider(providerID string) bool { - _, ok := getBuiltInProviderSpec(providerID) - return ok -} - -func isBuiltInMetadataProvider(providerID string) bool { - spec, ok := getBuiltInProviderSpec(providerID) - return ok && spec.SupportsMetadata -} - -func isBuiltInSearchProvider(providerID string) bool { - spec, ok := getBuiltInProviderSpec(providerID) - return ok && spec.SupportsSearch -} - -func normalizeBuiltInMetadataTrack(track TrackMetadata, providerID string) ExtTrackMetadata { - deezerID := "" - tidalID := "" - qobuzID := "" - prefixedID := strings.TrimSpace(track.SpotifyID) - - switch providerID { - case "deezer": - deezerID = strings.TrimPrefix(prefixedID, "deezer:") - case "tidal": - tidalID = strings.TrimPrefix(prefixedID, "tidal:") - case "qobuz": - qobuzID = strings.TrimPrefix(prefixedID, "qobuz:") - } - - return ExtTrackMetadata{ - ID: prefixedID, - Name: track.Name, - Artists: track.Artists, - AlbumName: track.AlbumName, - AlbumArtist: track.AlbumArtist, - DurationMS: track.DurationMS, - CoverURL: track.Images, - Images: track.Images, - ReleaseDate: track.ReleaseDate, - TrackNumber: track.TrackNumber, - TotalTracks: track.TotalTracks, - DiscNumber: track.DiscNumber, - TotalDiscs: track.TotalDiscs, - ISRC: track.ISRC, - ProviderID: providerID, - SpotifyID: prefixedID, - DeezerID: deezerID, - TidalID: tidalID, - QobuzID: qobuzID, - AlbumType: track.AlbumType, - Composer: track.Composer, - } -} - func metadataTrackDedupKey(track ExtTrackMetadata) string { if isrc := strings.TrimSpace(track.ISRC); isrc != "" { return "isrc:" + strings.ToUpper(isrc) @@ -2048,10 +1904,6 @@ func metadataTrackDedupKey(track ExtTrackMetadata) string { return strings.TrimSpace(track.Name) + "|" + strings.TrimSpace(track.Artists) } -func searchBuiltInMetadataTracks(providerID, query string, limit int) ([]ExtTrackMetadata, error) { - return searchBuiltInProviderTracks(providerID, query, limit) -} - func (m *extensionManager) SearchTracksWithMetadataProviders(query string, limit int, includeExtensions bool) ([]ExtTrackMetadata, error) { return m.SearchTracksWithMetadataProvidersForItemID(query, limit, includeExtensions, "") } @@ -2098,29 +1950,17 @@ func (m *extensionManager) SearchTracksWithMetadataProvidersForItemID(query stri return nil, ErrDownloadCancelled } - var ( - providerTracks []ExtTrackMetadata - err error - ) - - if isBuiltInProvider(providerID) { - providerTracks, err = searchBuiltInMetadataTracksForItemID(providerID, query, limit, itemID) - if isDownloadCancelled(itemID) { - return nil, ErrDownloadCancelled - } - } else { - if !includeExtensions { - continue - } - provider := extensionProviders[providerID] - if provider == nil { - continue - } - var result *ExtSearchResult - result, err = provider.SearchTracksForItemID(query, limit, itemID) - if result != nil { - providerTracks = result.Tracks - } + if !includeExtensions { + continue + } + provider := extensionProviders[providerID] + if provider == nil { + continue + } + result, err := provider.SearchTracksForItemID(query, limit, itemID) + providerTracks := []ExtTrackMetadata(nil) + if result != nil { + providerTracks = result.Tracks } if err != nil { @@ -2199,9 +2039,7 @@ func DownloadWithExtensionFallback(req DownloadRequest) (*DownloadResponse, erro var sourceExtensionAvailability *ExtAvailabilityResult var sourceExtensionTrackID string - if req.Source != "" && - !isBuiltInProvider(strings.ToLower(req.Source)) && - selectedProvider != req.Source { + if req.Source != "" && selectedProvider != req.Source { ext, err := extManager.GetExtension(req.Source) if err == nil && ext.Enabled && ext.Error == "" && ext.Manifest.IsDownloadProvider() { provider := newExtensionProviderWrapper(ext) @@ -2221,7 +2059,7 @@ func DownloadWithExtensionFallback(req DownloadRequest) (*DownloadResponse, erro } } - if req.Source != "" && !isBuiltInProvider(strings.ToLower(req.Source)) { + if req.Source != "" { ext, err := extManager.GetExtension(req.Source) if err == nil && ext.Enabled && ext.Error == "" && ext.Manifest.IsMetadataProvider() { GoLog("[DownloadWithExtensionFallback] Enriching track from extension '%s'...\n", req.Source) @@ -2328,7 +2166,7 @@ func DownloadWithExtensionFallback(req DownloadRequest) (*DownloadResponse, erro } } - if req.Source != "" && !isBuiltInProvider(strings.ToLower(req.Source)) && + if req.Source != "" && req.TrackName != "" && req.ArtistName != "" && (req.AlbumName == "" || req.ReleaseDate == "" || req.ISRC == "") { @@ -2396,9 +2234,7 @@ func DownloadWithExtensionFallback(req DownloadRequest) (*DownloadResponse, erro } } - if req.Source != "" && - !isBuiltInProvider(strings.ToLower(req.Source)) && - selectedProvider == req.Source { + if req.Source != "" && selectedProvider == req.Source { if isDownloadCancelled(req.ItemID) { return nil, ErrDownloadCancelled } diff --git a/go_backend/extension_providers_test.go b/go_backend/extension_providers_test.go index 6efb6e41..4b0e1aab 100644 --- a/go_backend/extension_providers_test.go +++ b/go_backend/extension_providers_test.go @@ -372,20 +372,12 @@ func TestCanEmbedGenreLabelRequiresExistingAbsoluteLocalFile(t *testing.T) { func TestSearchTracksWithMetadataProvidersIgnoresRetiredBuiltIns(t *testing.T) { originalPriority := GetMetadataProviderPriority() - originalSearch := searchBuiltInMetadataTracksFunc defer func() { SetMetadataProviderPriority(originalPriority) - searchBuiltInMetadataTracksFunc = originalSearch }() SetMetadataProviderPriority([]string{"qobuz"}) - var calls []string - searchBuiltInMetadataTracksFunc = func(providerID, query string, limit int) ([]ExtTrackMetadata, error) { - calls = append(calls, providerID) - return nil, nil - } - manager := getExtensionManager() tracks, err := manager.SearchTracksWithMetadataProviders("query", 3, false) if err != nil { @@ -394,9 +386,6 @@ func TestSearchTracksWithMetadataProvidersIgnoresRetiredBuiltIns(t *testing.T) { if len(tracks) != 0 { t.Fatalf("expected no tracks from retired built-in provider, got %+v", tracks) } - if len(calls) != 0 { - t.Fatalf("expected retired built-in provider not to be queried, got %v", calls) - } } func TestParseExtensionSearchResultAcceptsObjectAndArrayShapes(t *testing.T) { diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index d7ff7dc8..67bd8f5a 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -458,22 +458,6 @@ import Gobackend // Import Go framework if let error = error { throw error } return response - case "searchProviderAll": - let args = call.arguments as! [String: Any] - let providerId = args["provider_id"] as! String - let query = args["query"] as! String - let trackLimit = args["track_limit"] as? Int ?? 15 - let artistLimit = args["artist_limit"] as? Int ?? 3 - let filter = args["filter"] as? String ?? "" - let response = GobackendSearchProviderAllJSON(providerId, query, Int(trackLimit), Int(artistLimit), filter, &error) - if let error = error { throw error } - return response - - case "getBuiltInProviders": - let response = GobackendGetBuiltInProvidersJSON(&error) - if let error = error { throw error } - return response - case "getDeezerRelatedArtists": let args = call.arguments as! [String: Any] let artistId = args["artist_id"] as! String @@ -491,13 +475,6 @@ import Gobackend // Import Go framework if let error = error { throw error } return response - case "parseProviderUrl": - let args = call.arguments as! [String: Any] - let url = args["url"] as! String - let response = GobackendParseProviderURLJSON(url, &error) - if let error = error { throw error } - return response - case "searchDeezerByISRC": let args = call.arguments as! [String: Any] let isrc = args["isrc"] as! String diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index b9238ea7..968d6838 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -1773,7 +1773,7 @@ abstract class AppLocalizations { /// Section description for extension fallback selection /// /// In en, this message translates to: - /// **'Choose which installed download extensions can be used during automatic fallback. Built-in providers still follow the priority order above.'** + /// **'Choose which installed download extensions can be used during automatic fallback.'** String get providerPriorityFallbackExtensionsDescription; /// Hint below the extension fallback selection list diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 84d55fd7..66b85c1e 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -956,7 +956,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get providerPriorityFallbackExtensionsDescription => - 'Choose which installed download extensions can be used during automatic fallback. Built-in providers still follow the priority order above.'; + 'Choose which installed download extensions can be used during automatic fallback.'; @override String get providerPriorityFallbackExtensionsHint => diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index e0703ac1..9504bb4f 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -943,7 +943,7 @@ class AppLocalizationsEn extends AppLocalizations { @override String get providerPriorityFallbackExtensionsDescription => - 'Choose which installed download extensions can be used during automatic fallback. Built-in providers still follow the priority order above.'; + 'Choose which installed download extensions can be used during automatic fallback.'; @override String get providerPriorityFallbackExtensionsHint => diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index d3c10667..806275ae 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -943,7 +943,7 @@ class AppLocalizationsEs extends AppLocalizations { @override String get providerPriorityFallbackExtensionsDescription => - 'Choose which installed download extensions can be used during automatic fallback. Built-in providers still follow the priority order above.'; + 'Choose which installed download extensions can be used during automatic fallback.'; @override String get providerPriorityFallbackExtensionsHint => diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index b2d58788..784eecb6 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -946,7 +946,7 @@ class AppLocalizationsFr extends AppLocalizations { @override String get providerPriorityFallbackExtensionsDescription => - 'Choose which installed download extensions can be used during automatic fallback. Built-in providers still follow the priority order above.'; + 'Choose which installed download extensions can be used during automatic fallback.'; @override String get providerPriorityFallbackExtensionsHint => diff --git a/lib/l10n/app_localizations_hi.dart b/lib/l10n/app_localizations_hi.dart index a6fa87b8..598e1be9 100644 --- a/lib/l10n/app_localizations_hi.dart +++ b/lib/l10n/app_localizations_hi.dart @@ -943,7 +943,7 @@ class AppLocalizationsHi extends AppLocalizations { @override String get providerPriorityFallbackExtensionsDescription => - 'Choose which installed download extensions can be used during automatic fallback. Built-in providers still follow the priority order above.'; + 'Choose which installed download extensions can be used during automatic fallback.'; @override String get providerPriorityFallbackExtensionsHint => diff --git a/lib/l10n/app_localizations_id.dart b/lib/l10n/app_localizations_id.dart index cb6342b7..ad58c390 100644 --- a/lib/l10n/app_localizations_id.dart +++ b/lib/l10n/app_localizations_id.dart @@ -946,7 +946,7 @@ class AppLocalizationsId extends AppLocalizations { @override String get providerPriorityFallbackExtensionsDescription => - 'Choose which installed download extensions can be used during automatic fallback. Built-in providers still follow the priority order above.'; + 'Choose which installed download extensions can be used during automatic fallback.'; @override String get providerPriorityFallbackExtensionsHint => diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index eba3e4d9..ffbabbae 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -937,7 +937,7 @@ class AppLocalizationsJa extends AppLocalizations { @override String get providerPriorityFallbackExtensionsDescription => - 'Choose which installed download extensions can be used during automatic fallback. Built-in providers still follow the priority order above.'; + 'Choose which installed download extensions can be used during automatic fallback.'; @override String get providerPriorityFallbackExtensionsHint => diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index 47ce7031..61095077 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -925,7 +925,7 @@ class AppLocalizationsKo extends AppLocalizations { @override String get providerPriorityFallbackExtensionsDescription => - 'Choose which installed download extensions can be used during automatic fallback. Built-in providers still follow the priority order above.'; + 'Choose which installed download extensions can be used during automatic fallback.'; @override String get providerPriorityFallbackExtensionsHint => diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index dbc0c4b7..ab35af0e 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -943,7 +943,7 @@ class AppLocalizationsNl extends AppLocalizations { @override String get providerPriorityFallbackExtensionsDescription => - 'Choose which installed download extensions can be used during automatic fallback. Built-in providers still follow the priority order above.'; + 'Choose which installed download extensions can be used during automatic fallback.'; @override String get providerPriorityFallbackExtensionsHint => diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 62bc9612..6cc6ae1a 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -943,7 +943,7 @@ class AppLocalizationsPt extends AppLocalizations { @override String get providerPriorityFallbackExtensionsDescription => - 'Choose which installed download extensions can be used during automatic fallback. Built-in providers still follow the priority order above.'; + 'Choose which installed download extensions can be used during automatic fallback.'; @override String get providerPriorityFallbackExtensionsHint => @@ -4734,7 +4734,7 @@ class AppLocalizationsPtPt extends AppLocalizationsPt { @override String get providerPriorityFallbackExtensionsDescription => - 'Choose which installed download extensions can be used during automatic fallback. Built-in providers still follow the priority order above.'; + 'Choose which installed download extensions can be used during automatic fallback.'; @override String get providerPriorityFallbackExtensionsHint => diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index ca895859..47f67f6d 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -956,7 +956,7 @@ class AppLocalizationsRu extends AppLocalizations { @override String get providerPriorityFallbackExtensionsDescription => - 'Choose which installed download extensions can be used during automatic fallback. Built-in providers still follow the priority order above.'; + 'Choose which installed download extensions can be used during automatic fallback.'; @override String get providerPriorityFallbackExtensionsHint => diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index b7da2294..869c059c 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -943,7 +943,7 @@ class AppLocalizationsZh extends AppLocalizations { @override String get providerPriorityFallbackExtensionsDescription => - 'Choose which installed download extensions can be used during automatic fallback. Built-in providers still follow the priority order above.'; + 'Choose which installed download extensions can be used during automatic fallback.'; @override String get providerPriorityFallbackExtensionsHint => @@ -4710,7 +4710,7 @@ class AppLocalizationsZhCn extends AppLocalizationsZh { @override String get providerPriorityFallbackExtensionsDescription => - 'Choose which installed download extensions can be used during automatic fallback. Built-in providers still follow the priority order above.'; + 'Choose which installed download extensions can be used during automatic fallback.'; @override String get providerPriorityFallbackExtensionsHint => @@ -8187,7 +8187,7 @@ class AppLocalizationsZhTw extends AppLocalizationsZh { @override String get providerPriorityFallbackExtensionsDescription => - 'Choose which installed download extensions can be used during automatic fallback. Built-in providers still follow the priority order above.'; + 'Choose which installed download extensions can be used during automatic fallback.'; @override String get providerPriorityFallbackExtensionsHint => diff --git a/lib/l10n/arb/app_de.arb b/lib/l10n/arb/app_de.arb index 67ff25c0..de2b86c8 100644 --- a/lib/l10n/arb/app_de.arb +++ b/lib/l10n/arb/app_de.arb @@ -1211,7 +1211,7 @@ "@providerPriorityFallbackExtensionsTitle": { "description": "Section title for choosing which download extensions can be used as fallback providers" }, - "providerPriorityFallbackExtensionsDescription": "Choose which installed download extensions can be used during automatic fallback. Built-in providers still follow the priority order above.", + "providerPriorityFallbackExtensionsDescription": "Choose which installed download extensions can be used during automatic fallback.", "@providerPriorityFallbackExtensionsDescription": { "description": "Section description for extension fallback selection" }, diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 01e9d64a..ac2fab15 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -1223,7 +1223,7 @@ "@providerPriorityFallbackExtensionsTitle": { "description": "Section title for choosing which download extensions can be used as fallback providers" }, - "providerPriorityFallbackExtensionsDescription": "Choose which installed download extensions can be used during automatic fallback. Built-in providers still follow the priority order above.", + "providerPriorityFallbackExtensionsDescription": "Choose which installed download extensions can be used during automatic fallback.", "@providerPriorityFallbackExtensionsDescription": { "description": "Section description for extension fallback selection" }, diff --git a/lib/l10n/arb/app_es.arb b/lib/l10n/arb/app_es.arb index 7ceaf942..e6b5d3f6 100644 --- a/lib/l10n/arb/app_es.arb +++ b/lib/l10n/arb/app_es.arb @@ -1933,7 +1933,7 @@ "@providerPriorityFallbackExtensionsTitle": { "description": "Section title for choosing which download extensions can be used as fallback providers" }, - "providerPriorityFallbackExtensionsDescription": "Choose which installed download extensions can be used during automatic fallback. Built-in providers still follow the priority order above.", + "providerPriorityFallbackExtensionsDescription": "Choose which installed download extensions can be used during automatic fallback.", "@providerPriorityFallbackExtensionsDescription": { "description": "Section description for extension fallback selection" }, diff --git a/lib/l10n/arb/app_fr.arb b/lib/l10n/arb/app_fr.arb index b31e1881..5626f34c 100644 --- a/lib/l10n/arb/app_fr.arb +++ b/lib/l10n/arb/app_fr.arb @@ -1211,7 +1211,7 @@ "@providerPriorityFallbackExtensionsTitle": { "description": "Section title for choosing which download extensions can be used as fallback providers" }, - "providerPriorityFallbackExtensionsDescription": "Choose which installed download extensions can be used during automatic fallback. Built-in providers still follow the priority order above.", + "providerPriorityFallbackExtensionsDescription": "Choose which installed download extensions can be used during automatic fallback.", "@providerPriorityFallbackExtensionsDescription": { "description": "Section description for extension fallback selection" }, diff --git a/lib/l10n/arb/app_hi.arb b/lib/l10n/arb/app_hi.arb index 93162629..5f05054a 100644 --- a/lib/l10n/arb/app_hi.arb +++ b/lib/l10n/arb/app_hi.arb @@ -1211,7 +1211,7 @@ "@providerPriorityFallbackExtensionsTitle": { "description": "Section title for choosing which download extensions can be used as fallback providers" }, - "providerPriorityFallbackExtensionsDescription": "Choose which installed download extensions can be used during automatic fallback. Built-in providers still follow the priority order above.", + "providerPriorityFallbackExtensionsDescription": "Choose which installed download extensions can be used during automatic fallback.", "@providerPriorityFallbackExtensionsDescription": { "description": "Section description for extension fallback selection" }, diff --git a/lib/l10n/arb/app_id.arb b/lib/l10n/arb/app_id.arb index e177f7cf..58fc1eb6 100644 --- a/lib/l10n/arb/app_id.arb +++ b/lib/l10n/arb/app_id.arb @@ -1127,7 +1127,7 @@ "@providerPriorityFallbackExtensionsTitle": { "description": "Section title for choosing which download extensions can be used as fallback providers" }, - "providerPriorityFallbackExtensionsDescription": "Choose which installed download extensions can be used during automatic fallback. Built-in providers still follow the priority order above.", + "providerPriorityFallbackExtensionsDescription": "Choose which installed download extensions can be used during automatic fallback.", "@providerPriorityFallbackExtensionsDescription": { "description": "Section description for extension fallback selection" }, diff --git a/lib/l10n/arb/app_ja.arb b/lib/l10n/arb/app_ja.arb index d7c12c06..4ab8416e 100644 --- a/lib/l10n/arb/app_ja.arb +++ b/lib/l10n/arb/app_ja.arb @@ -3290,7 +3290,7 @@ "@providerPriorityFallbackExtensionsTitle": { "description": "Section title for choosing which download extensions can be used as fallback providers" }, - "providerPriorityFallbackExtensionsDescription": "Choose which installed download extensions can be used during automatic fallback. Built-in providers still follow the priority order above.", + "providerPriorityFallbackExtensionsDescription": "Choose which installed download extensions can be used during automatic fallback.", "@providerPriorityFallbackExtensionsDescription": { "description": "Section description for extension fallback selection" }, diff --git a/lib/l10n/arb/app_ko.arb b/lib/l10n/arb/app_ko.arb index e648109e..695bb684 100644 --- a/lib/l10n/arb/app_ko.arb +++ b/lib/l10n/arb/app_ko.arb @@ -1211,7 +1211,7 @@ "@providerPriorityFallbackExtensionsTitle": { "description": "Section title for choosing which download extensions can be used as fallback providers" }, - "providerPriorityFallbackExtensionsDescription": "Choose which installed download extensions can be used during automatic fallback. Built-in providers still follow the priority order above.", + "providerPriorityFallbackExtensionsDescription": "Choose which installed download extensions can be used during automatic fallback.", "@providerPriorityFallbackExtensionsDescription": { "description": "Section description for extension fallback selection" }, diff --git a/lib/l10n/arb/app_nl.arb b/lib/l10n/arb/app_nl.arb index ec55d841..d53257a0 100644 --- a/lib/l10n/arb/app_nl.arb +++ b/lib/l10n/arb/app_nl.arb @@ -1211,7 +1211,7 @@ "@providerPriorityFallbackExtensionsTitle": { "description": "Section title for choosing which download extensions can be used as fallback providers" }, - "providerPriorityFallbackExtensionsDescription": "Choose which installed download extensions can be used during automatic fallback. Built-in providers still follow the priority order above.", + "providerPriorityFallbackExtensionsDescription": "Choose which installed download extensions can be used during automatic fallback.", "@providerPriorityFallbackExtensionsDescription": { "description": "Section description for extension fallback selection" }, diff --git a/lib/l10n/arb/app_pt.arb b/lib/l10n/arb/app_pt.arb index 164c96be..5c109f83 100644 --- a/lib/l10n/arb/app_pt.arb +++ b/lib/l10n/arb/app_pt.arb @@ -1933,7 +1933,7 @@ "@providerPriorityFallbackExtensionsTitle": { "description": "Section title for choosing which download extensions can be used as fallback providers" }, - "providerPriorityFallbackExtensionsDescription": "Choose which installed download extensions can be used during automatic fallback. Built-in providers still follow the priority order above.", + "providerPriorityFallbackExtensionsDescription": "Choose which installed download extensions can be used during automatic fallback.", "@providerPriorityFallbackExtensionsDescription": { "description": "Section description for extension fallback selection" }, diff --git a/lib/l10n/arb/app_pt_PT.arb b/lib/l10n/arb/app_pt_PT.arb index a30411e1..f9744986 100644 --- a/lib/l10n/arb/app_pt_PT.arb +++ b/lib/l10n/arb/app_pt_PT.arb @@ -1211,7 +1211,7 @@ "@providerPriorityFallbackExtensionsTitle": { "description": "Section title for choosing which download extensions can be used as fallback providers" }, - "providerPriorityFallbackExtensionsDescription": "Choose which installed download extensions can be used during automatic fallback. Built-in providers still follow the priority order above.", + "providerPriorityFallbackExtensionsDescription": "Choose which installed download extensions can be used during automatic fallback.", "@providerPriorityFallbackExtensionsDescription": { "description": "Section description for extension fallback selection" }, diff --git a/lib/l10n/arb/app_ru.arb b/lib/l10n/arb/app_ru.arb index 19d7e2db..abcdfb6a 100644 --- a/lib/l10n/arb/app_ru.arb +++ b/lib/l10n/arb/app_ru.arb @@ -1211,7 +1211,7 @@ "@providerPriorityFallbackExtensionsTitle": { "description": "Section title for choosing which download extensions can be used as fallback providers" }, - "providerPriorityFallbackExtensionsDescription": "Choose which installed download extensions can be used during automatic fallback. Built-in providers still follow the priority order above.", + "providerPriorityFallbackExtensionsDescription": "Choose which installed download extensions can be used during automatic fallback.", "@providerPriorityFallbackExtensionsDescription": { "description": "Section description for extension fallback selection" }, diff --git a/lib/l10n/arb/app_zh.arb b/lib/l10n/arb/app_zh.arb index 136a0a32..c5deab20 100644 --- a/lib/l10n/arb/app_zh.arb +++ b/lib/l10n/arb/app_zh.arb @@ -1933,7 +1933,7 @@ "@providerPriorityFallbackExtensionsTitle": { "description": "Section title for choosing which download extensions can be used as fallback providers" }, - "providerPriorityFallbackExtensionsDescription": "Choose which installed download extensions can be used during automatic fallback. Built-in providers still follow the priority order above.", + "providerPriorityFallbackExtensionsDescription": "Choose which installed download extensions can be used during automatic fallback.", "@providerPriorityFallbackExtensionsDescription": { "description": "Section description for extension fallback selection" }, diff --git a/lib/l10n/arb/app_zh_CN.arb b/lib/l10n/arb/app_zh_CN.arb index e099be2b..9c825991 100644 --- a/lib/l10n/arb/app_zh_CN.arb +++ b/lib/l10n/arb/app_zh_CN.arb @@ -1211,7 +1211,7 @@ "@providerPriorityFallbackExtensionsTitle": { "description": "Section title for choosing which download extensions can be used as fallback providers" }, - "providerPriorityFallbackExtensionsDescription": "Choose which installed download extensions can be used during automatic fallback. Built-in providers still follow the priority order above.", + "providerPriorityFallbackExtensionsDescription": "Choose which installed download extensions can be used during automatic fallback.", "@providerPriorityFallbackExtensionsDescription": { "description": "Section description for extension fallback selection" }, diff --git a/lib/l10n/arb/app_zh_TW.arb b/lib/l10n/arb/app_zh_TW.arb index 94b368c4..3cce0c94 100644 --- a/lib/l10n/arb/app_zh_TW.arb +++ b/lib/l10n/arb/app_zh_TW.arb @@ -1211,7 +1211,7 @@ "@providerPriorityFallbackExtensionsTitle": { "description": "Section title for choosing which download extensions can be used as fallback providers" }, - "providerPriorityFallbackExtensionsDescription": "Choose which installed download extensions can be used during automatic fallback. Built-in providers still follow the priority order above.", + "providerPriorityFallbackExtensionsDescription": "Choose which installed download extensions can be used during automatic fallback.", "@providerPriorityFallbackExtensionsDescription": { "description": "Section description for extension fallback selection" }, diff --git a/lib/providers/download_queue_provider.dart b/lib/providers/download_queue_provider.dart index ff1cce35..2cbdc2bf 100644 --- a/lib/providers/download_queue_provider.dart +++ b/lib/providers/download_queue_provider.dart @@ -2567,9 +2567,6 @@ class DownloadQueueNotifier extends Notifier { if (normalized.isEmpty) { return false; } - if (isBuiltInDownloadProvider(normalized)) { - return true; - } final extensionState = ref.read(extensionProvider); return extensionState.extensions.any( diff --git a/lib/providers/extension_provider.dart b/lib/providers/extension_provider.dart index bd1a4ef6..0697132b 100644 --- a/lib/providers/extension_provider.dart +++ b/lib/providers/extension_provider.dart @@ -48,44 +48,6 @@ List? _tryDecodeStringListPreference(String rawJson, String key) { } } -class BuiltInProviderSpec { - final String id; - final String displayName; - final bool supportsMetadata; - final bool supportsDownload; - final bool supportsSearch; - - const BuiltInProviderSpec({ - required this.id, - required this.displayName, - this.supportsMetadata = false, - this.supportsDownload = false, - this.supportsSearch = false, - }); - - factory BuiltInProviderSpec.fromJson(Map json) { - return BuiltInProviderSpec( - id: json['id'] as String? ?? '', - displayName: - json['display_name'] as String? ?? - json['displayName'] as String? ?? - '', - supportsMetadata: json['supports_metadata'] as bool? ?? false, - supportsDownload: json['supports_download'] as bool? ?? false, - supportsSearch: json['supports_search'] as bool? ?? false, - ); - } -} - -List _builtInProviderRegistry = const []; - -List get builtInProviderSpecs => - List.unmodifiable(_builtInProviderRegistry); - -void _replaceBuiltInProviderRegistry(List providers) { - _builtInProviderRegistry = List.unmodifiable(providers); -} - class Extension { final String id; final String name; @@ -302,66 +264,16 @@ class Extension { } } -BuiltInProviderSpec? builtInProviderSpecForId(String? providerId) { - if (providerId == null) return null; - - for (final provider in builtInProviderSpecs) { - if (provider.id == providerId) { - return provider; - } - } - - return null; -} - -List _builtInProvidersWhere( - bool Function(BuiltInProviderSpec provider) predicate, -) { - return List.unmodifiable( - builtInProviderSpecs.where(predicate), - ); -} - -List get builtInSearchProviderSpecs => - _builtInProvidersWhere((provider) => provider.supportsSearch); - -List get builtInMetadataProviderSpecs => - _builtInProvidersWhere((provider) => provider.supportsMetadata); - -List get builtInDownloadProviderSpecs => - _builtInProvidersWhere((provider) => provider.supportsDownload); - -List get builtInSearchProviderIds => List.unmodifiable( - builtInSearchProviderSpecs.map((provider) => provider.id), -); - -List get builtInMetadataProviderIds => List.unmodifiable( - builtInMetadataProviderSpecs.map((provider) => provider.id), -); - -List get builtInDownloadProviderIds => List.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; @@ -379,25 +291,7 @@ String resolveEffectiveDownloadService( } } - 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 ?? - ''; + return enabledDownloadExtensions.firstOrNull?.id ?? ''; } String resolveEffectiveMetadataProvider( @@ -405,20 +299,11 @@ String resolveEffectiveMetadataProvider( ExtensionState extensionState, ) { final normalizedRequested = requestedProvider.trim().toLowerCase(); - final builtInMetadataIds = extensionState.builtInProviders - .where((provider) => provider.supportsMetadata) - .map((provider) => provider.id.trim().toLowerCase()) - .where((providerId) => providerId.isNotEmpty) - .toSet(); final enabledMetadataExtensions = extensionState.extensions .where((ext) => ext.enabled && ext.hasMetadataProvider) .toList(growable: false); if (normalizedRequested.isNotEmpty) { - if (builtInMetadataIds.contains(normalizedRequested)) { - return normalizedRequested; - } - final matchingExtension = enabledMetadataExtensions .where((ext) => ext.id.trim().toLowerCase() == normalizedRequested) .firstOrNull; @@ -436,12 +321,7 @@ String resolveEffectiveMetadataProvider( } } - return enabledMetadataExtensions.firstOrNull?.id ?? - extensionState.builtInProviders - .where((provider) => provider.supportsMetadata) - .map((provider) => provider.id) - .firstOrNull ?? - ''; + return enabledMetadataExtensions.firstOrNull?.id ?? ''; } bool isDeezerCompatibleDownloadService( @@ -453,10 +333,6 @@ bool isDeezerCompatibleDownloadService( return false; } - if (normalizedService == 'deezer') { - return true; - } - return extensionState.extensions.any( (ext) => ext.enabled && @@ -466,33 +342,10 @@ bool isDeezerCompatibleDownloadService( ); } -bool isBuiltInSearchProvider(String? providerId) => - builtInProviderSpecForId(providerId)?.supportsSearch ?? false; - -bool isBuiltInMetadataProvider(String? providerId) => - builtInProviderSpecForId(providerId)?.supportsMetadata ?? false; - -bool isBuiltInDownloadProvider(String? providerId) => - builtInProviderSpecForId(providerId)?.supportsDownload ?? false; - -String? get defaultBuiltInSearchProviderId => builtInSearchProviderSpecs.isEmpty - ? null - : builtInSearchProviderSpecs.first.id; - -String? get defaultBuiltInSearchProviderDisplayName => - builtInSearchProviderSpecs.isEmpty - ? null - : builtInSearchProviderSpecs.first.displayName; - String resolveProviderDisplayName( String providerId, { Iterable extensions = const [], }) { - final builtIn = builtInProviderSpecForId(providerId); - if (builtIn != null) { - return builtIn.displayName; - } - for (final extension in extensions) { if (extension.id == providerId) { return extension.displayName; @@ -884,7 +737,6 @@ class ExtensionSetting { class ExtensionState { final List extensions; - final List builtInProviders; final List providerPriority; final List metadataProviderPriority; final Map healthStatuses; @@ -894,7 +746,6 @@ class ExtensionState { const ExtensionState({ this.extensions = const [], - this.builtInProviders = const [], this.providerPriority = const [], this.metadataProviderPriority = const [], this.healthStatuses = const {}, @@ -905,7 +756,6 @@ class ExtensionState { ExtensionState copyWith({ List? extensions, - List? builtInProviders, List? providerPriority, List? metadataProviderPriority, Map? healthStatuses, @@ -915,7 +765,6 @@ class ExtensionState { }) { return ExtensionState( extensions: extensions ?? this.extensions, - builtInProviders: builtInProviders ?? this.builtInProviders, providerPriority: providerPriority ?? this.providerPriority, metadataProviderPriority: metadataProviderPriority ?? this.metadataProviderPriority, @@ -998,12 +847,6 @@ class ExtensionNotifier extends Notifier { state = state.copyWith(isLoading: true, error: null); - try { - await refreshBuiltInProviders(); - } catch (e) { - _log.w('Failed to refresh built-in providers before init: $e'); - } - if (!PlatformBridge.supportsExtensionSystem) { state = state.copyWith( isInitialized: true, @@ -1168,16 +1011,6 @@ class ExtensionNotifier extends Notifier { return future; } - Future refreshBuiltInProviders() async { - final list = await PlatformBridge.getBuiltInProviders(); - final providers = list - .map((e) => BuiltInProviderSpec.fromJson(e)) - .where((provider) => provider.id.isNotEmpty) - .toList(); - _replaceBuiltInProviderRegistry(providers); - state = state.copyWith(builtInProviders: providers); - } - void clearError() { state = state.copyWith(error: null); } @@ -1402,8 +1235,7 @@ class ExtensionNotifier extends Notifier { state.extensions .where((ext) => ext.enabled && ext.hasCustomSearch) .map((ext) => ext.id) - .firstOrNull ?? - defaultBuiltInSearchProviderId; + .firstOrNull; } String? replacedBuiltInDownloadProviderFor(String providerId) { @@ -1505,8 +1337,7 @@ class ExtensionNotifier extends Notifier { currentExtension == null || !currentExtension.enabled || !currentExtension.hasDownloadProvider; - if (!isBuiltInDownloadProvider(currentService) && - isMissingOrInvalidExtension) { + if (isMissingOrInvalidExtension) { final fallbackService = preferredExtensionId ?? ''; ref.read(settingsProvider.notifier).setDefaultService(fallbackService); _log.d( @@ -1551,8 +1382,7 @@ class ExtensionNotifier extends Notifier { (ext) => ext.enabled && ext.hasCustomSearch && ext.id == currentSearchProvider, ); - if (!isBuiltInSearchProvider(currentSearchProvider) && - !hasMatchingExtension) { + if (!hasMatchingExtension) { ref .read(settingsProvider.notifier) .setSearchProvider( @@ -1815,13 +1645,10 @@ class ExtensionNotifier extends Notifier { } List getAllDownloadProviders() { - final providers = List.from(builtInDownloadProviderIds); - for (final ext in state.extensions) { - if (ext.enabled && ext.hasDownloadProvider) { - providers.add(ext.id); - } - } - return providers; + return state.extensions + .where((ext) => ext.enabled && ext.hasDownloadProvider) + .map((ext) => ext.id) + .toList(growable: false); } List getAllMetadataProviders() { @@ -1837,7 +1664,6 @@ class ExtensionNotifier extends Notifier { return [ ...primarySearchMetadataExtensions, - ...builtInMetadataProviderIds, ...otherMetadataExtensions, ]; } @@ -1865,14 +1691,7 @@ class ExtensionNotifier extends Notifier { } } - final hasPreferredExtension = preferredOrder.any( - (provider) => !isBuiltInMetadataProvider(provider), - ); - final hasSavedExtension = result.any( - (provider) => !isBuiltInMetadataProvider(provider), - ); - - if (!hasSavedExtension && hasPreferredExtension) { + if (result.isEmpty && preferredOrder.isNotEmpty) { return List.from(preferredOrder); } diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart index 2c5e6da4..dba67db3 100644 --- a/lib/providers/settings_provider.dart +++ b/lib/providers/settings_provider.dart @@ -69,12 +69,6 @@ class SettingsNotifier extends Notifier { final sanitizedDefaultSearchTab = _normalizeDefaultSearchTab( loaded.defaultSearchTab, ); - final sanitizedDefaultService = _sanitizeRetiredBuiltInProviderId( - loaded.defaultService, - ); - final sanitizedSearchProvider = _sanitizeRetiredBuiltInProviderId( - loaded.searchProvider, - ); state = loaded.copyWith( useExtensionProviders: true, downloadFallbackExtensionIds: sanitizedDownloadFallbackExtensionIds, @@ -82,10 +76,8 @@ class SettingsNotifier extends Notifier { loaded.downloadFallbackExtensionIds != null && sanitizedDownloadFallbackExtensionIds == null, defaultSearchTab: sanitizedDefaultSearchTab, - defaultService: sanitizedDefaultService ?? '', - searchProvider: sanitizedSearchProvider, - clearSearchProvider: - loaded.searchProvider != null && sanitizedSearchProvider == null, + defaultService: loaded.defaultService, + searchProvider: loaded.searchProvider, ); await _runMigrations(prefs); @@ -166,23 +158,8 @@ class SettingsNotifier extends Notifier { ); } state = state.copyWith(lastSeenVersion: AppInfo.version); - // Migration 7/11: retired built-in services no longer fall back to a - // preinstalled provider. - final sanitizedDefaultService = _sanitizeRetiredBuiltInProviderId( - state.defaultService, - ); - final sanitizedSearchProvider = _sanitizeRetiredBuiltInProviderId( - state.searchProvider, - ); - if (sanitizedDefaultService != state.defaultService || - sanitizedSearchProvider != state.searchProvider) { - state = state.copyWith( - defaultService: sanitizedDefaultService ?? '', - searchProvider: sanitizedSearchProvider, - clearSearchProvider: - state.searchProvider != null && sanitizedSearchProvider == null, - ); - } + // Migration 7/11: retired built-in services are now reconciled after + // extensions load so manifest-declared replacements can adopt old prefs. if (!state.useExtensionProviders) { state = state.copyWith(useExtensionProviders: true); } diff --git a/lib/providers/track_provider.dart b/lib/providers/track_provider.dart index 3a1c54a5..63957a62 100644 --- a/lib/providers/track_provider.dart +++ b/lib/providers/track_provider.dart @@ -195,7 +195,6 @@ class SearchPlaylist { class TrackNotifier extends Notifier { int _currentRequestId = 0; - static const int _maxPreWarmTracksPerRequest = 80; @override TrackState build() { @@ -204,15 +203,6 @@ class TrackNotifier extends Notifier { bool _isRequestValid(int requestId) => requestId == _currentRequestId; - bool _usesBuiltInUrlResolver(String url) { - final normalized = url.toLowerCase(); - return normalized.contains('deezer.com') || - normalized.contains('deezer.page.link') || - normalized.contains('qobuz.com') || - normalized.startsWith('qobuzapp://') || - normalized.contains('tidal.com'); - } - Future fetchFromUrl(String url, {bool useDeezerFallback = true}) async { final requestId = ++_currentRequestId; @@ -220,7 +210,7 @@ class TrackNotifier extends Notifier { try { var extensionHandler = await PlatformBridge.findURLHandler(url); - if (extensionHandler == null && !_usesBuiltInUrlResolver(url)) { + if (extensionHandler == null) { final extensionState = ref.read(extensionProvider); if (!extensionState.isInitialized && extensionState.isLoading) { _log.i( @@ -234,132 +224,124 @@ class TrackNotifier extends Notifier { } } - if (extensionHandler != null) { - _log.i('Found extension URL handler: $extensionHandler for URL: $url'); + if (extensionHandler == null) { + state = TrackState( + isLoading: false, + error: 'url_not_recognized', + hasSearchText: state.hasSearchText, + ); + return; + } - Map? result; - for (int attempt = 1; attempt <= 3; attempt++) { - result = await PlatformBridge.handleURLWithExtension(url); - if (!_isRequestValid(requestId)) return; + _log.i('Found extension URL handler: $extensionHandler for URL: $url'); - if (result != null && - result['type'] == 'track' && - result['track'] != null) { - final trackData = result['track'] as Map; - final name = trackData['name']?.toString() ?? ''; - if (name.isNotEmpty) { - break; - } - } else if (result != null && - (result['type'] == 'album' || result['type'] == 'playlist')) { - break; - } else if (result != null && result['type'] == 'artist') { + Map? result; + for (int attempt = 1; attempt <= 3; attempt++) { + result = await PlatformBridge.handleURLWithExtension(url); + if (!_isRequestValid(requestId)) return; + + if (result != null && + result['type'] == 'track' && + result['track'] != null) { + final trackData = result['track'] as Map; + final name = trackData['name']?.toString() ?? ''; + if (name.isNotEmpty) { break; } - - if (attempt < 3) { - await Future.delayed(const Duration(milliseconds: 500)); - } + } else if (result != null && + (result['type'] == 'album' || result['type'] == 'playlist')) { + break; + } else if (result != null && result['type'] == 'artist') { + break; } - if (result != null) { - final type = result['type'] as String?; - final extensionId = result['extension_id'] as String?; - - if (type == 'track' && result['track'] != null) { - final trackData = result['track'] as Map; - final track = _parseSearchTrack(trackData, source: extensionId); - - if (track.name.isEmpty) { - state = TrackState( - isLoading: false, - error: 'Failed to load track metadata from extension', - ); - return; - } - - state = TrackState( - tracks: [track], - isLoading: false, - coverUrl: track.coverUrl, - searchExtensionId: extensionId, - ); - return; - } else if ((type == 'album' || type == 'playlist') && - result['tracks'] != null) { - final trackList = result['tracks'] as List; - final tracks = trackList - .map( - (t) => _parseSearchTrack( - t as Map, - source: extensionId, - ), - ) - .toList(); - state = TrackState( - tracks: tracks, - isLoading: false, - albumId: - (result['album'] as Map?)?['id'] as String?, - albumName: - result['name'] as String? ?? - (result['album'] as Map?)?['name'] - as String?, - playlistName: type == 'playlist' - ? result['name'] as String? - : null, - coverUrl: normalizeCoverReference( - result['cover_url']?.toString(), - ), - searchExtensionId: extensionId, - ); - return; - } else if (type == 'artist' && result['artist'] != null) { - final artistData = result['artist'] as Map; - final albumsList = artistData['albums'] as List? ?? []; - final albums = albumsList - .map((a) => _parseArtistAlbum(a as Map)) - .toList(); - - final topTracksList = - artistData['top_tracks'] as List? ?? []; - final topTracks = topTracksList - .map( - (t) => _parseSearchTrack( - t as Map, - source: extensionId, - ), - ) - .toList(); - - state = TrackState( - tracks: [], - isLoading: false, - artistId: artistData['id'] as String?, - artistName: artistData['name'] as String?, - coverUrl: normalizeRemoteHttpUrl( - (artistData['image_url'] ?? artistData['images'])?.toString(), - ), - headerImageUrl: normalizeRemoteHttpUrl( - artistData['header_image']?.toString(), - ), - monthlyListeners: artistData['listeners'] as int?, - artistAlbums: albums, - artistTopTracks: topTracks.isNotEmpty ? topTracks : null, - searchExtensionId: extensionId, - ); - return; - } + if (attempt < 3) { + await Future.delayed(const Duration(milliseconds: 500)); } } - final handledBuiltInUrl = await _tryResolveBuiltInProviderUrl( - url, - requestId, - ); - if (!_isRequestValid(requestId)) return; - if (handledBuiltInUrl) { - return; + if (result != null) { + final type = result['type'] as String?; + final extensionId = result['extension_id'] as String?; + + if (type == 'track' && result['track'] != null) { + final trackData = result['track'] as Map; + final track = _parseSearchTrack(trackData, source: extensionId); + + if (track.name.isEmpty) { + state = TrackState( + isLoading: false, + error: 'Failed to load track metadata from extension', + ); + return; + } + + state = TrackState( + tracks: [track], + isLoading: false, + coverUrl: track.coverUrl, + searchExtensionId: extensionId, + ); + return; + } else if ((type == 'album' || type == 'playlist') && + result['tracks'] != null) { + final trackList = result['tracks'] as List; + final tracks = trackList + .map( + (t) => _parseSearchTrack( + t as Map, + source: extensionId, + ), + ) + .toList(); + state = TrackState( + tracks: tracks, + isLoading: false, + albumId: + (result['album'] as Map?)?['id'] as String?, + albumName: + result['name'] as String? ?? + (result['album'] as Map?)?['name'] as String?, + playlistName: type == 'playlist' ? result['name'] as String? : null, + coverUrl: normalizeCoverReference(result['cover_url']?.toString()), + searchExtensionId: extensionId, + ); + return; + } else if (type == 'artist' && result['artist'] != null) { + final artistData = result['artist'] as Map; + final albumsList = artistData['albums'] as List? ?? []; + final albums = albumsList + .map((a) => _parseArtistAlbum(a as Map)) + .toList(); + + final topTracksList = artistData['top_tracks'] as List? ?? []; + final topTracks = topTracksList + .map( + (t) => _parseSearchTrack( + t as Map, + source: extensionId, + ), + ) + .toList(); + + state = TrackState( + tracks: [], + isLoading: false, + artistId: artistData['id'] as String?, + artistName: artistData['name'] as String?, + coverUrl: normalizeRemoteHttpUrl( + (artistData['image_url'] ?? artistData['images'])?.toString(), + ), + headerImageUrl: normalizeRemoteHttpUrl( + artistData['header_image']?.toString(), + ), + monthlyListeners: artistData['listeners'] as int?, + artistAlbums: albums, + artistTopTracks: topTracks.isNotEmpty ? topTracks : null, + searchExtensionId: extensionId, + ); + return; + } } state = TrackState( @@ -377,138 +359,9 @@ class TrackNotifier extends Notifier { } } - Future _tryResolveBuiltInProviderUrl(String url, int requestId) async { - Map parsed; - try { - parsed = await PlatformBridge.parseProviderUrl(url); - } catch (_) { - return false; - } - - if (!_isRequestValid(requestId)) return true; - - final providerId = parsed['provider_id']?.toString(); - final type = parsed['type']?.toString(); - final id = parsed['id']?.toString(); - if (providerId == null || - providerId.isEmpty || - type == null || - type.isEmpty || - id == null || - id.isEmpty) { - return false; - } - - _log.i('Detected built-in provider URL: $providerId:$type:$id'); - - final metadata = await _getResolvedProviderMetadata(providerId, type, id); - if (!_isRequestValid(requestId)) return true; - - _applyResolvedProviderMetadata(providerId, type, id, metadata); - return true; - } - - Future> _getResolvedProviderMetadata( - String providerId, - String resourceType, - String resourceId, - ) async { - return PlatformBridge.getProviderMetadata( - providerId, - resourceType, - resourceId, - ); - } - - void _applyResolvedProviderMetadata( - String providerId, - String resourceType, - String resourceId, - Map metadata, - ) { - switch (resourceType) { - case 'track': - final trackData = metadata['track'] as Map; - final track = _parseTrack(trackData); - state = TrackState( - tracks: [track], - isLoading: false, - coverUrl: track.coverUrl, - ); - return; - case 'album': - final albumInfo = metadata['album_info'] as Map; - final trackList = metadata['track_list'] as List; - final tracks = trackList - .map((t) => _parseTrack(t as Map)) - .toList(); - state = TrackState( - tracks: tracks, - isLoading: false, - albumId: _buildResolvedAlbumId(providerId, resourceId), - albumName: albumInfo['name'] as String?, - coverUrl: normalizeRemoteHttpUrl(albumInfo['images']?.toString()), - ); - _preWarmCacheForTracks(tracks, service: providerId); - return; - case 'playlist': - final playlistInfo = metadata['playlist_info'] as Map; - final trackList = metadata['track_list'] as List; - final tracks = trackList - .map((t) => _parseTrack(t as Map)) - .toList(); - final owner = playlistInfo['owner'] as Map?; - final playlistName = - (playlistInfo['name'] ?? owner?['name']) as String?; - final coverUrl = normalizeRemoteHttpUrl( - (playlistInfo['images'] ?? owner?['images'])?.toString(), - ); - state = TrackState( - tracks: tracks, - isLoading: false, - playlistName: playlistName, - coverUrl: coverUrl, - ); - _preWarmCacheForTracks(tracks, service: providerId); - return; - case 'artist': - final artistInfo = metadata['artist_info'] as Map; - final albumsList = metadata['albums'] as List; - final albums = albumsList - .map((a) => _parseArtistAlbum(a as Map)) - .toList(); - final topTracksList = metadata['top_tracks'] as List? ?? []; - final topTracks = topTracksList - .map((t) => _parseTrack(t as Map)) - .toList(); - state = TrackState( - tracks: [], - isLoading: false, - artistId: artistInfo['id'] as String?, - artistName: artistInfo['name'] as String?, - coverUrl: normalizeRemoteHttpUrl(artistInfo['images']?.toString()), - headerImageUrl: normalizeRemoteHttpUrl( - (artistInfo['header_image'] ?? artistInfo['cover_url'])?.toString(), - ), - monthlyListeners: artistInfo['listeners'] as int?, - artistAlbums: albums, - artistTopTracks: topTracks.isNotEmpty ? topTracks : null, - ); - return; - } - } - - String _buildResolvedAlbumId(String providerId, String resourceId) { - if (providerId == 'deezer') { - return resourceId; - } - return '$providerId:$resourceId'; - } - Future search( String query, { String? filterOverride, - String? builtInSearchProvider, }) async { final requestId = ++_currentRequestId; final currentFilter = filterOverride ?? state.selectedSearchFilter; @@ -516,33 +369,29 @@ class TrackNotifier extends Notifier { final settings = ref.read(settingsProvider); final extensionState = ref.read(extensionProvider); - String? resolvedProvider = builtInSearchProvider; - if (resolvedProvider == null || resolvedProvider.isEmpty) { - final explicitProvider = settings.searchProvider?.trim(); - if (explicitProvider != null && explicitProvider.isNotEmpty) { - resolvedProvider = explicitProvider; - } else { - resolvedProvider = - extensionState.extensions - .where( - (ext) => - ext.enabled && - ext.hasCustomSearch && - ext.searchBehavior?.primary == true, - ) - .map((ext) => ext.id) - .firstOrNull ?? - extensionState.extensions - .where((ext) => ext.enabled && ext.hasCustomSearch) - .map((ext) => ext.id) - .firstOrNull; - } - resolvedProvider ??= defaultBuiltInSearchProviderId; + String? resolvedProvider; + final explicitProvider = settings.searchProvider?.trim(); + if (explicitProvider != null && explicitProvider.isNotEmpty) { + resolvedProvider = explicitProvider; + } else { + resolvedProvider = + extensionState.extensions + .where( + (ext) => + ext.enabled && + ext.hasCustomSearch && + ext.searchBehavior?.primary == true, + ) + .map((ext) => ext.id) + .firstOrNull ?? + extensionState.extensions + .where((ext) => ext.enabled && ext.hasCustomSearch) + .map((ext) => ext.id) + .firstOrNull; } if (resolvedProvider != null && resolvedProvider.isNotEmpty && - !isBuiltInSearchProvider(resolvedProvider) && !extensionState.extensions.any( (ext) => ext.enabled && ext.id == resolvedProvider, ) && @@ -562,7 +411,6 @@ class TrackNotifier extends Notifier { .where((ext) => ext.enabled && ext.hasCustomSearch) .map((ext) => ext.id) .firstOrNull; - resolvedProvider ??= defaultBuiltInSearchProviderId; } final isEnabledExtensionProvider = @@ -571,11 +419,9 @@ class TrackNotifier extends Notifier { extensionState.extensions.any( (ext) => ext.enabled && ext.id == resolvedProvider, ); - final isBuiltInProvider = isBuiltInSearchProvider(resolvedProvider); if (resolvedProvider != null && resolvedProvider.isNotEmpty && - !isBuiltInProvider && isEnabledExtensionProvider) { final resolvedFilter = requestFilter ?? 'track'; Map? options; @@ -589,23 +435,6 @@ class TrackNotifier extends Notifier { return; } - final fallbackBuiltInProvider = builtInSearchProvider?.isNotEmpty == true - ? builtInSearchProvider - : defaultBuiltInSearchProviderId; - final effectiveBuiltInProvider = isBuiltInProvider - ? resolvedProvider - : fallbackBuiltInProvider; - - if (effectiveBuiltInProvider == null || effectiveBuiltInProvider.isEmpty) { - state = TrackState( - isLoading: false, - hasSearchText: state.hasSearchText, - isShowingRecentAccess: state.isShowingRecentAccess, - selectedSearchFilter: currentFilter, - ); - return; - } - state = TrackState( isLoading: true, hasSearchText: state.hasSearchText, @@ -614,47 +443,21 @@ class TrackNotifier extends Notifier { ); try { - final hasActiveMetadataExtensions = extensionState.extensions.any( - (e) => e.enabled && e.hasMetadataProvider, - ); - final includeExtensions = - settings.useExtensionProviders && hasActiveMetadataExtensions; - - final effectiveProvider = effectiveBuiltInProvider; + final includeExtensions = settings.useExtensionProviders; _log.i( - 'Search started: provider=$effectiveProvider, query="$query", includeExtensions=$includeExtensions, filter=$requestFilter', + 'Search started: provider=metadata_extensions, query="$query", includeExtensions=$includeExtensions, filter=$requestFilter', ); - Map results; - List> metadataTrackResults = []; - - if (isBuiltInSearchProvider(effectiveProvider)) { - _log.d('Calling built-in search API for $effectiveProvider...'); - results = await PlatformBridge.searchProviderAll( - effectiveProvider, - query, - trackLimit: 20, - artistLimit: 2, - filter: requestFilter, - ); - } else { - _log.d('Calling metadata provider track search API...'); - metadataTrackResults = - await PlatformBridge.searchTracksWithMetadataProviders( - query, - limit: 20, - includeExtensions: includeExtensions, - ); - results = const >{ - 'tracks': [], - 'artists': [], - 'albums': [], - 'playlists': [], - }; - } + _log.d('Calling metadata provider track search API...'); + final metadataTrackResults = + await PlatformBridge.searchTracksWithMetadataProviders( + query, + limit: 20, + includeExtensions: includeExtensions, + ); _log.i( - '$effectiveProvider returned ${(results['tracks'] as List?)?.length ?? 0} tracks, ${(results['artists'] as List?)?.length ?? 0} artists, ${(results['albums'] as List?)?.length ?? 0} albums', + 'metadata_extensions returned ${metadataTrackResults.length} tracks', ); if (!_isRequestValid(requestId)) { @@ -662,12 +465,9 @@ class TrackNotifier extends Notifier { return; } - final trackList = results['tracks'] as List? ?? []; - final artistList = results['artists'] as List? ?? []; - final albumList = results['albums'] as List? ?? []; - final trackSearchResults = metadataTrackResults.isNotEmpty - ? metadataTrackResults - : trackList.whereType>().toList(); + final trackSearchResults = metadataTrackResults; + const artistList = []; + const albumList = []; _log.d( 'Raw results: ${trackSearchResults.length} tracks, ${artistList.length} artists, ${albumList.length} albums', @@ -712,7 +512,7 @@ class TrackNotifier extends Notifier { } } - final playlistList = results['playlists'] as List? ?? []; + const playlistList = []; final playlists = []; for (int i = 0; i < playlistList.length; i++) { final p = playlistList[i]; @@ -740,7 +540,7 @@ class TrackNotifier extends Notifier { hasSearchText: state.hasSearchText, isShowingRecentAccess: state.isShowingRecentAccess, selectedSearchFilter: currentFilter, - searchSource: effectiveProvider, + searchSource: resolvedProvider, ); } catch (e, stackTrace) { if (!_isRequestValid(requestId)) return; @@ -915,33 +715,6 @@ class TrackNotifier extends Notifier { ); } - Track _parseTrack(Map data) { - final durationMs = _extractDurationMs(data); - final spotifyId = (data['spotify_id'] ?? '').toString(); - final nativeId = (data['id'] ?? '').toString(); - return Track( - id: spotifyId.isNotEmpty ? spotifyId : nativeId, - name: data['name'] as String? ?? '', - artistName: data['artists'] as String? ?? '', - albumName: data['album_name'] as String? ?? '', - albumArtist: data['album_artist'] as String?, - artistId: (data['artist_id'] ?? data['artistId'])?.toString(), - albumId: data['album_id']?.toString(), - coverUrl: normalizeCoverReference(data['images']?.toString()), - isrc: data['isrc'] as String?, - duration: (durationMs / 1000).round(), - trackNumber: data['track_number'] as int?, - discNumber: data['disc_number'] as int?, - totalDiscs: data['total_discs'] as int?, - releaseDate: data['release_date'] as String?, - albumType: normalizeOptionalString(data['album_type']?.toString()), - totalTracks: data['total_tracks'] as int?, - composer: data['composer']?.toString(), - audioQuality: data['audio_quality']?.toString(), - audioModes: data['audio_modes']?.toString(), - ); - } - Track _parseSearchTrack(Map data, {String? source}) { final durationMs = _extractDurationMs(data); @@ -1054,33 +827,6 @@ class TrackNotifier extends Notifier { ); } - void _preWarmCacheForTracks(List tracks, {String? service}) { - if (tracks.isEmpty) return; - final cacheRequests = >[]; - for (final track in tracks) { - final isrc = track.isrc; - if (isrc == null || isrc.isEmpty) { - continue; - } - final effectiveService = - (track.source?.trim().isNotEmpty == true ? track.source : service) - ?.trim(); - cacheRequests.add({ - 'isrc': isrc, - 'track_name': track.name, - 'artist_name': track.artistName, - 'spotify_id': track.id, - if (effectiveService != null && effectiveService.isNotEmpty) - 'service': effectiveService, - }); - if (cacheRequests.length >= _maxPreWarmTracksPerRequest) { - break; - } - } - if (cacheRequests.isEmpty) return; - - PlatformBridge.preWarmTrackCache(cacheRequests).catchError((_) {}); - } } final trackProvider = NotifierProvider( diff --git a/lib/screens/home_tab.dart b/lib/screens/home_tab.dart index bd5ca57d..1f7d6221 100644 --- a/lib/screens/home_tab.dart +++ b/lib/screens/home_tab.dart @@ -29,7 +29,6 @@ import 'package:spotiflac_android/widgets/download_service_picker.dart'; import 'package:spotiflac_android/widgets/track_collection_quick_actions.dart'; import 'package:spotiflac_android/widgets/animation_utils.dart'; import 'package:spotiflac_android/utils/clickable_metadata.dart'; -import 'package:spotiflac_android/utils/provider_ui_utils.dart'; import 'package:spotiflac_android/widgets/audio_quality_badges.dart'; import 'package:spotiflac_android/widgets/cached_cover_image.dart'; @@ -315,26 +314,20 @@ class _HomeTabState extends ConsumerState final explicit = explicitSearchProvider?.trim(); if (explicit != null && explicit.isNotEmpty && - (isBuiltInSearchProvider(explicit) || - extensions.any( - (ext) => ext.enabled && ext.hasCustomSearch && ext.id == explicit, - ))) { + extensions.any( + (ext) => ext.enabled && ext.hasCustomSearch && ext.id == explicit, + )) { return explicit; } - return _defaultSearchExtension(extensions)?.id ?? - defaultBuiltInSearchProviderId; + return _defaultSearchExtension(extensions)?.id; } bool _hasSearchProvider( String? explicitSearchProvider, List extensions, - List builtInProviders, ) { final explicit = explicitSearchProvider?.trim(); if (explicit != null && explicit.isNotEmpty) { - if (builtInProviders.any((p) => p.supportsSearch && p.id == explicit)) { - return true; - } if (extensions.any( (ext) => ext.enabled && ext.hasCustomSearch && ext.id == explicit, )) { @@ -342,8 +335,7 @@ class _HomeTabState extends ConsumerState } } - return extensions.any((ext) => ext.enabled && ext.hasCustomSearch) || - builtInProviders.any((provider) => provider.supportsSearch); + return extensions.any((ext) => ext.enabled && ext.hasCustomSearch); } String? _sanitizeSearchFilterForProvider( @@ -358,8 +350,7 @@ class _HomeTabState extends ConsumerState final canonicalFilter = _canonicalSearchFilterId(filter); if (currentSearchProvider == null || - currentSearchProvider.isEmpty || - isBuiltInSearchProvider(currentSearchProvider)) { + currentSearchProvider.isEmpty) { switch (canonicalFilter) { case 'track': case 'artist': @@ -554,8 +545,6 @@ class _HomeTabState extends ConsumerState if (searchProvider == null || searchProvider.isEmpty) return false; - if (isBuiltInSearchProvider(searchProvider)) return true; - final extension = extState.extensions .where((e) => e.id == searchProvider && e.enabled) .firstOrNull; @@ -655,13 +644,9 @@ class _HomeTabState extends ConsumerState _searchSortOption = _SearchSortOption.defaultOrder; _invalidateSearchSortCaches(); - final isBuiltInProvider = - searchProvider != null && isBuiltInSearchProvider(searchProvider); - final isExtensionEnabled = searchProvider != null && searchProvider.isNotEmpty && - !isBuiltInProvider && extState.extensions.any((e) => e.id == searchProvider && e.enabled); if (isExtensionEnabled) { @@ -677,19 +662,10 @@ class _HomeTabState extends ConsumerState options: options, selectedFilter: selectedFilter, ); - } else if (isBuiltInProvider) { - await ref - .read(trackProvider.notifier) - .search( - query, - filterOverride: selectedFilter, - builtInSearchProvider: searchProvider, - ); } else { if (searchProvider != null && searchProvider.isNotEmpty && - !isExtensionEnabled && - !isBuiltInProvider) { + !isExtensionEnabled) { ref.read(settingsProvider.notifier).setSearchProvider(null); } await ref @@ -1147,7 +1123,6 @@ class _HomeTabState extends ConsumerState (s) => ( isInitialized: s.isInitialized, error: s.error, - builtInProviders: s.builtInProviders, ), ), ); @@ -1195,7 +1170,6 @@ class _HomeTabState extends ConsumerState final hasSearchProvider = _hasSearchProvider( explicitSearchProvider, extensions, - extensionReadiness.builtInProviders, ); final showSearchBar = hasSearchProvider || isSearchProviderLoading; final hasResults = @@ -3378,11 +3352,6 @@ class _HomeTabState extends ConsumerState } if (searchProvider != null && searchProvider.isNotEmpty) { - final builtIn = builtInProviderSpecForId(searchProvider); - if (builtIn != null && builtIn.supportsSearch) { - return context.l10n.homeSearchHintProvider(builtIn.displayName); - } - final ext = extState.extensions .where((e) => e.id == searchProvider) .firstOrNull; diff --git a/lib/screens/home_tab_widgets.dart b/lib/screens/home_tab_widgets.dart index 1da20943..3637b4e6 100644 --- a/lib/screens/home_tab_widgets.dart +++ b/lib/screens/home_tab_widgets.dart @@ -36,9 +36,7 @@ class _SearchProviderDropdown extends ConsumerWidget { final searchProviders = extensions .where((ext) => ext.enabled && ext.hasCustomSearch) .toList(); - final builtInProviders = builtInSearchProviderSpecs; - final hasAnyProvider = - searchProviders.isNotEmpty || builtInProviders.isNotEmpty; + final hasAnyProvider = searchProviders.isNotEmpty; final isProviderLoading = !providerReadiness.isInitialized && providerReadiness.error == null; @@ -71,11 +69,9 @@ class _SearchProviderDropdown extends ConsumerWidget { final resolvedCurrentProvider = rawCurrentProvider != null && rawCurrentProvider.isNotEmpty && - (isBuiltInSearchProvider(rawCurrentProvider) || - searchProviders.any((e) => e.id == rawCurrentProvider)) + searchProviders.any((e) => e.id == rawCurrentProvider) ? rawCurrentProvider - : _defaultSearchExtension(searchProviders)?.id ?? - defaultBuiltInSearchProviderId; + : _defaultSearchExtension(searchProviders)?.id; final currentProvider = resolvedCurrentProvider != null && resolvedCurrentProvider.isNotEmpty ? resolvedCurrentProvider @@ -88,9 +84,6 @@ class _SearchProviderDropdown extends ConsumerWidget { .firstOrNull; } - final isBuiltInProvider = - currentProvider != null && isBuiltInSearchProvider(currentProvider); - IconData displayIcon = Icons.search; String? iconPath; if (currentExt != null) { @@ -98,8 +91,6 @@ class _SearchProviderDropdown extends ConsumerWidget { if (currentExt.searchBehavior?.icon != null) { displayIcon = _getIconFromName(currentExt.searchBehavior!.icon!); } - } else if (isBuiltInProvider) { - displayIcon = resolveProviderIcon(currentProvider); } return Padding( @@ -137,36 +128,6 @@ class _SearchProviderDropdown extends ConsumerWidget { onProviderChanged?.call(); }, itemBuilder: (context) => [ - ...builtInProviders.map( - (provider) => PopupMenuItem( - value: provider.id, - child: Row( - children: [ - Icon( - resolveProviderIcon(provider.id), - size: 20, - color: currentProvider == provider.id - ? colorScheme.primary - : colorScheme.onSurfaceVariant, - ), - const SizedBox(width: 12), - Expanded( - child: Text( - provider.displayName, - style: TextStyle( - fontWeight: currentProvider == provider.id - ? FontWeight.w600 - : FontWeight.normal, - ), - ), - ), - if (currentProvider == provider.id) - Icon(Icons.check, size: 18, color: colorScheme.primary), - ], - ), - ), - ), - if (searchProviders.isNotEmpty) const PopupMenuDivider(), ...searchProviders.map( (ext) => PopupMenuItem( value: ext.id, diff --git a/lib/screens/settings/extensions_page.dart b/lib/screens/settings/extensions_page.dart index 1a9819de..3210b054 100644 --- a/lib/screens/settings/extensions_page.dart +++ b/lib/screens/settings/extensions_page.dart @@ -726,29 +726,23 @@ class _SearchProviderSelector extends ConsumerWidget { final settings = ref.watch(settingsProvider); final extState = ref.watch(extensionProvider); final colorScheme = Theme.of(context).colorScheme; - final builtInProviders = builtInSearchProviderSpecs; final searchProviders = extState.extensions .where((e) => e.enabled && e.hasCustomSearch) .toList(); - final hasAnyProvider = - searchProviders.isNotEmpty || builtInProviders.isNotEmpty; + final hasAnyProvider = searchProviders.isNotEmpty; final resolvedProviderId = (settings.searchProvider != null && settings.searchProvider!.isNotEmpty) ? settings.searchProvider! - : searchProviders.firstOrNull?.id ?? defaultBuiltInSearchProviderId; + : searchProviders.firstOrNull?.id; String currentProviderName = context.l10n.optionsPrimaryProviderSubtitle; if (resolvedProviderId != null && resolvedProviderId.isNotEmpty) { - if (isBuiltInSearchProvider(resolvedProviderId)) { - currentProviderName = resolveProviderDisplayName(resolvedProviderId); - } else { - final ext = searchProviders - .where((e) => e.id == resolvedProviderId) - .firstOrNull; - currentProviderName = ext?.displayName ?? resolvedProviderId; - } + final ext = searchProviders + .where((e) => e.id == resolvedProviderId) + .firstOrNull; + currentProviderName = ext?.displayName ?? resolvedProviderId; } return Column( @@ -817,7 +811,6 @@ class _SearchProviderSelector extends ConsumerWidget { List searchProviders, ) { final colorScheme = Theme.of(context).colorScheme; - final builtInProviders = builtInSearchProviderSpecs; showModalBottomSheet( context: context, @@ -850,25 +843,6 @@ class _SearchProviderSelector extends ConsumerWidget { ), ), ), - ...builtInProviders.map( - (provider) => ListTile( - leading: Icon(Icons.search, color: colorScheme.tertiary), - title: Text(provider.displayName), - subtitle: Text( - ctx.l10n.extensionsSearchWith(provider.displayName), - ), - trailing: settings.searchProvider == provider.id - ? Icon(Icons.check_circle, color: colorScheme.primary) - : Icon(Icons.circle_outlined, color: colorScheme.outline), - onTap: () { - ref - .read(settingsProvider.notifier) - .setSearchProvider(provider.id); - Navigator.pop(ctx); - }, - ), - ), - if (searchProviders.isNotEmpty) const Divider(height: 1), ...searchProviders.map( (ext) => ListTile( leading: Icon(Icons.extension, color: colorScheme.secondary), diff --git a/lib/screens/settings/metadata_provider_priority_page.dart b/lib/screens/settings/metadata_provider_priority_page.dart index c61fe930..c100bf34 100644 --- a/lib/screens/settings/metadata_provider_priority_page.dart +++ b/lib/screens/settings/metadata_provider_priority_page.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spotiflac_android/l10n/l10n.dart'; import 'package:spotiflac_android/providers/extension_provider.dart'; -import 'package:spotiflac_android/utils/provider_ui_utils.dart'; import 'package:spotiflac_android/widgets/priority_settings_scaffold.dart'; class MetadataProviderPriorityPage extends ConsumerStatefulWidget { @@ -67,6 +66,11 @@ class _MetadataProviderPriorityPageState index: index, isFirst: index == 0, isLast: index == _providers.length - 1, + extension: ref + .read(extensionProvider) + .extensions + .where((ext) => ext.id == provider) + .firstOrNull, ); }, onReorder: (oldIndex, newIndex) { @@ -126,6 +130,7 @@ class _MetadataProviderItem extends StatelessWidget { final int index; final bool isFirst; final bool isLast; + final Extension? extension; const _MetadataProviderItem({ super.key, @@ -133,6 +138,7 @@ class _MetadataProviderItem extends StatelessWidget { required this.index, required this.isFirst, required this.isLast, + this.extension, }); @override @@ -147,7 +153,7 @@ class _MetadataProviderItem extends StatelessWidget { ) : colorScheme.surfaceContainerHigh; - final info = _getProviderInfo(context, provider); + final info = _getProviderInfo(context, provider, extension); return Padding( padding: const EdgeInsets.only(bottom: 8), @@ -184,9 +190,7 @@ class _MetadataProviderItem extends StatelessWidget { const SizedBox(width: 16), Icon( info.icon, - color: info.isBuiltIn - ? colorScheme.primary - : colorScheme.secondary, + color: colorScheme.secondary, ), const SizedBox(width: 12), Expanded( @@ -220,34 +224,12 @@ class _MetadataProviderItem extends StatelessWidget { _MetadataProviderInfo _getProviderInfo( BuildContext context, String provider, + Extension? extension, ) { - final builtIn = builtInProviderSpecForId(provider); - if (builtIn != null) { - return _MetadataProviderInfo( - name: builtIn.displayName, - icon: resolveProviderIcon( - provider, - builtInDefaultIcon: Icons.library_music, - ), - description: context.l10n.providerBuiltIn, - isBuiltIn: true, - ); - } - - if (provider == 'deezer') { - return _MetadataProviderInfo( - name: 'Deezer', - icon: Icons.album, - description: context.l10n.providerExtension, - isBuiltIn: false, - ); - } - return _MetadataProviderInfo( - name: provider, + name: extension?.displayName ?? provider, icon: Icons.extension, description: context.l10n.providerExtension, - isBuiltIn: false, ); } } @@ -256,12 +238,10 @@ class _MetadataProviderInfo { final String name; final IconData icon; final String description; - final bool isBuiltIn; _MetadataProviderInfo({ required this.name, required this.icon, required this.description, - required this.isBuiltIn, }); } diff --git a/lib/screens/settings/provider_priority_page.dart b/lib/screens/settings/provider_priority_page.dart index 758f525c..8c4f7933 100644 --- a/lib/screens/settings/provider_priority_page.dart +++ b/lib/screens/settings/provider_priority_page.dart @@ -3,7 +3,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spotiflac_android/l10n/l10n.dart'; import 'package:spotiflac_android/providers/extension_provider.dart'; import 'package:spotiflac_android/utils/app_bar_layout.dart'; -import 'package:spotiflac_android/utils/provider_ui_utils.dart'; class ProviderPriorityPage extends ConsumerStatefulWidget { const ProviderPriorityPage({super.key}); @@ -139,6 +138,11 @@ class _ProviderPriorityPageState extends ConsumerState { index: index, isFirst: index == 0, isLast: index == _providers.length - 1, + extension: ref + .read(extensionProvider) + .extensions + .where((ext) => ext.id == provider) + .firstOrNull, ); }, onReorder: (oldIndex, newIndex) { @@ -232,6 +236,7 @@ class _ProviderItem extends StatelessWidget { final int index; final bool isFirst; final bool isLast; + final Extension? extension; const _ProviderItem({ super.key, @@ -239,6 +244,7 @@ class _ProviderItem extends StatelessWidget { required this.index, required this.isFirst, required this.isLast, + this.extension, }); @override @@ -253,7 +259,7 @@ class _ProviderItem extends StatelessWidget { ) : colorScheme.surfaceContainerHigh; - final info = _getProviderInfo(provider); + final info = _getProviderInfo(provider, extension); return Padding( padding: const EdgeInsets.only(bottom: 8), @@ -290,9 +296,7 @@ class _ProviderItem extends StatelessWidget { const SizedBox(width: 16), Icon( info.icon, - color: info.isBuiltIn - ? colorScheme.primary - : colorScheme.secondary, + color: colorScheme.secondary, ), const SizedBox(width: 12), Expanded( @@ -306,9 +310,7 @@ class _ProviderItem extends StatelessWidget { ), ), Text( - info.isBuiltIn - ? context.l10n.providerBuiltIn - : context.l10n.providerExtension, + context.l10n.providerExtension, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: colorScheme.onSurfaceVariant, ), @@ -325,28 +327,10 @@ class _ProviderItem extends StatelessWidget { ); } - _ProviderInfo _getProviderInfo(String provider) { - final builtIn = builtInProviderSpecForId(provider); - if (builtIn != null) { - return _ProviderInfo( - name: builtIn.displayName, - icon: resolveProviderIcon(provider), - isBuiltIn: true, - ); - } - - if (provider == 'deezer') { - return _ProviderInfo( - name: 'Deezer', - icon: Icons.graphic_eq, - isBuiltIn: false, - ); - } - + _ProviderInfo _getProviderInfo(String provider, Extension? extension) { return _ProviderInfo( - name: provider, + name: extension?.displayName ?? provider, icon: Icons.extension, - isBuiltIn: false, ); } } @@ -354,11 +338,9 @@ class _ProviderItem extends StatelessWidget { class _ProviderInfo { final String name; final IconData icon; - final bool isBuiltIn; _ProviderInfo({ required this.name, required this.icon, - required this.isBuiltIn, }); } diff --git a/lib/services/platform_bridge.dart b/lib/services/platform_bridge.dart index 02b49cb0..75be7b9e 100644 --- a/lib/services/platform_bridge.dart +++ b/lib/services/platform_bridge.dart @@ -884,23 +884,6 @@ class PlatformBridge { await _channel.invokeMethod('clearTrackCache'); } - static Future> searchProviderAll( - String providerId, - String query, { - int trackLimit = 15, - int artistLimit = 2, - String? filter, - }) async { - final result = await _channel.invokeMethod('searchProviderAll', { - 'provider_id': providerId, - 'query': query, - 'track_limit': trackLimit, - 'artist_limit': artistLimit, - 'filter': filter ?? '', - }); - return _decodeRequiredMapResult(result, 'searchProviderAll'); - } - static Future> getDeezerRelatedArtists( String artistId, { int limit = 12, @@ -912,13 +895,6 @@ class PlatformBridge { return _decodeRequiredMapResult(result, 'getDeezerRelatedArtists'); } - static Future> parseProviderUrl(String url) async { - final result = await _channel.invokeMethod('parseProviderUrl', { - 'url': url, - }); - return _decodeRequiredMapResult(result, 'parseProviderUrl'); - } - static Future> getProviderMetadata( String providerId, String resourceType, @@ -1394,11 +1370,6 @@ class PlatformBridge { return _decodeMapListResult(result, 'getSearchProviders'); } - static Future>> getBuiltInProviders() async { - final result = await _channel.invokeMethod('getBuiltInProviders'); - return _decodeMapListResult(result, 'getBuiltInProviders'); - } - static Future?> handleURLWithExtension( String url, ) async { diff --git a/lib/utils/clickable_metadata.dart b/lib/utils/clickable_metadata.dart index d561ef83..7180cb2c 100644 --- a/lib/utils/clickable_metadata.dart +++ b/lib/utils/clickable_metadata.dart @@ -61,17 +61,6 @@ Future>> _searchMetadataProvider( required String filter, required int limit, }) async { - if (isBuiltInSearchProvider(providerId)) { - final result = await PlatformBridge.searchProviderAll( - providerId, - query, - trackLimit: 0, - artistLimit: filter == 'artist' ? limit : 0, - filter: filter, - ); - return _extractSearchItems(result, filter); - } - return PlatformBridge.customSearchWithExtension( providerId, query, @@ -79,24 +68,6 @@ Future>> _searchMetadataProvider( ); } -List> _extractSearchItems( - Map result, - String filter, -) { - final key = switch (filter) { - 'artist' => 'artists', - 'album' => 'albums', - _ => '${filter}s', - }; - final items = result[key]; - if (items is! List) return const []; - - return items - .whereType>() - .map((item) => Map.from(item)) - .toList(growable: false); -} - List _metadataSearchProviderCandidates( BuildContext context, { String? sourceProviderId, @@ -142,10 +113,6 @@ List _metadataSearchProviderCandidates( addProvider(extension.id); } - for (final providerId in builtInSearchProviderIds) { - addProvider(providerId); - } - return candidates; } @@ -153,7 +120,6 @@ bool _canSearchMetadataProvider( String providerId, ExtensionState extensionState, ) { - if (isBuiltInSearchProvider(providerId)) return true; return extensionState.extensions.any( (ext) => ext.enabled && ext.hasCustomSearch && ext.id == providerId, ); @@ -333,8 +299,7 @@ void _pushArtistScreen( String? coverUrl, String? extensionId, }) { - final isExtension = - extensionId != null && !isBuiltInMetadataProvider(extensionId); + final isExtension = extensionId != null; final resolvedProviderId = extensionId; _pushViaPreferredNavigator( @@ -362,8 +327,7 @@ void _pushAlbumScreen( String? coverUrl, String? extensionId, }) { - final isExtension = - extensionId != null && !isBuiltInMetadataProvider(extensionId); + final isExtension = extensionId != null; final resolvedExtensionId = extensionId; _pushViaPreferredNavigator( @@ -677,19 +641,9 @@ bool _canNavigateArtistDirectly({ required String? extensionId, }) { if (extensionId != null) return true; - final providerPrefix = _resourceProviderPrefix(artistId); - if (providerPrefix != null && isBuiltInMetadataProvider(providerPrefix)) { - return true; - } return _spotifyArtistIdPattern.hasMatch(artistId); } -String? _resourceProviderPrefix(String resourceId) { - final colonIndex = resourceId.indexOf(':'); - if (colonIndex <= 0) return null; - return resourceId.substring(0, colonIndex).trim(); -} - final RegExp _spotifyArtistIdPattern = RegExp(r'^[A-Za-z0-9]{22}$'); class ClickableAlbumName extends StatelessWidget { diff --git a/lib/utils/provider_ui_utils.dart b/lib/utils/provider_ui_utils.dart deleted file mode 100644 index d883269d..00000000 --- a/lib/utils/provider_ui_utils.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:spotiflac_android/providers/extension_provider.dart'; - -IconData resolveProviderIcon( - String providerId, { - IconData tidalIcon = Icons.music_note, - IconData builtInDefaultIcon = Icons.album, - IconData deezerIcon = Icons.graphic_eq, - IconData fallbackIcon = Icons.extension, -}) { - final builtIn = builtInProviderSpecForId(providerId); - if (builtIn != null) { - if (providerId == 'tidal') { - return tidalIcon; - } - return builtInDefaultIcon; - } - - if (providerId == 'deezer') { - return deezerIcon; - } - - return fallbackIcon; -} diff --git a/lib/widgets/download_service_picker.dart b/lib/widgets/download_service_picker.dart index c1e63644..e4549ea6 100644 --- a/lib/widgets/download_service_picker.dart +++ b/lib/widgets/download_service_picker.dart @@ -5,67 +5,6 @@ import 'package:spotiflac_android/providers/extension_provider.dart'; import 'package:spotiflac_android/providers/settings_provider.dart'; import 'package:spotiflac_android/l10n/l10n.dart'; -class BuiltInService { - final String id; - final String label; - final List qualityOptions; - final bool isDisabled; // If true, service is grayed out (fallback only) - final String? disabledReason; - - const BuiltInService({ - required this.id, - required this.label, - required this.qualityOptions, - this.isDisabled = false, - this.disabledReason, - }); -} - -const _builtInServiceCatalog = { - 'tidal': BuiltInService( - id: 'tidal', - label: 'Tidal', - qualityOptions: [ - QualityOption( - id: 'LOSSLESS', - label: 'FLAC Lossless', - description: '16-bit / 44.1kHz', - ), - QualityOption( - id: 'HI_RES', - label: 'Hi-Res FLAC', - description: '24-bit / up to 96kHz', - ), - QualityOption( - id: 'HI_RES_LOSSLESS', - label: 'Hi-Res FLAC Max', - description: '24-bit / up to 192kHz', - ), - ], - ), - 'qobuz': BuiltInService( - id: 'qobuz', - label: 'Qobuz', - qualityOptions: [ - QualityOption( - id: 'LOSSLESS', - label: 'FLAC Lossless', - description: '16-bit / 44.1kHz', - ), - QualityOption( - id: 'HI_RES', - label: 'Hi-Res FLAC', - description: '24-bit / up to 96kHz', - ), - QualityOption( - id: 'HI_RES_LOSSLESS', - label: 'Hi-Res FLAC Max', - description: '24-bit / up to 192kHz', - ), - ], - ), -}; - class DownloadServicePicker extends ConsumerStatefulWidget { final String? trackName; final String? artistName; @@ -118,18 +57,6 @@ class DownloadServicePicker extends ConsumerStatefulWidget { class _DownloadServicePickerState extends ConsumerState { late String _selectedService; - List _availableBuiltInServices() { - final availableIds = builtInDownloadProviderIds.toSet(); - final services = []; - for (final id in availableIds) { - final service = _builtInServiceCatalog[id]; - if (service != null) { - services.add(service); - } - } - return services; - } - List _downloadExtensions() { final extensionState = ref.read(extensionProvider); return extensionState.extensions @@ -137,13 +64,8 @@ class _DownloadServicePickerState extends ConsumerState { .toList(growable: false); } - bool _serviceExists( - String serviceId, - List builtInServices, - List downloadExtensions, - ) { + bool _serviceExists(String serviceId, List downloadExtensions) { if (serviceId.isEmpty) return false; - if (builtInServices.any((service) => service.id == serviceId)) return true; return downloadExtensions.any((ext) => ext.id == serviceId); } @@ -154,39 +76,21 @@ class _DownloadServicePickerState extends ConsumerState { if (!mounted) return; ref.read(extensionProvider.notifier).refreshEnabledExtensionHealth(); }); - final builtInServices = _availableBuiltInServices(); final downloadExtensions = _downloadExtensions(); final recommended = widget.recommendedService; - if (recommended != null && - _serviceExists(recommended, builtInServices, downloadExtensions)) { + if (recommended != null && _serviceExists(recommended, downloadExtensions)) { _selectedService = recommended; } else { _selectedService = ref.read(settingsProvider).defaultService; } - if (!_serviceExists( - _selectedService, - builtInServices, - downloadExtensions, - )) { - _selectedService = builtInServices.isNotEmpty - ? builtInServices.first.id - : downloadExtensions.isNotEmpty + if (!_serviceExists(_selectedService, downloadExtensions)) { + _selectedService = downloadExtensions.isNotEmpty ? downloadExtensions.first.id : ''; } } - List _getQualityOptions( - List builtInServices, - List downloadExtensions, - ) { - final builtIn = builtInServices - .where((service) => service.id == _selectedService) - .firstOrNull; - if (builtIn != null) { - return builtIn.qualityOptions; - } - + List _getQualityOptions(List downloadExtensions) { final ext = downloadExtensions .where((e) => e.id == _selectedService) .firstOrNull; @@ -201,14 +105,9 @@ class _DownloadServicePickerState extends ConsumerState { Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; final extensionState = ref.watch(extensionProvider); - final builtInServices = _availableBuiltInServices(); final downloadExtensions = _downloadExtensions(); - final hasProviders = - builtInServices.isNotEmpty || downloadExtensions.isNotEmpty; - final qualityOptions = _getQualityOptions( - builtInServices, - downloadExtensions, - ); + final hasProviders = downloadExtensions.isNotEmpty; + final qualityOptions = _getQualityOptions(downloadExtensions); return SafeArea( child: SingleChildScrollView( @@ -257,21 +156,6 @@ class _DownloadServicePickerState extends ConsumerState { spacing: 8, runSpacing: 8, children: [ - for (final service in builtInServices) - _ServiceChip( - label: service.isDisabled - ? '${service.label} (${service.disabledReason})' - : widget.recommendedService == service.id - ? '${service.label} (Recommended)' - : service.label, - isSelected: _selectedService == service.id, - isDisabled: service.isDisabled, - onTap: service.isDisabled - ? null - : () => setState( - () => _selectedService = service.id, - ), - ), for (final ext in downloadExtensions) _ServiceChip( label: widget.recommendedService == ext.id @@ -302,19 +186,6 @@ class _DownloadServicePickerState extends ConsumerState { ), ), ), - if (builtInServices.any( - (service) => service.id == _selectedService, - )) - Padding( - padding: const EdgeInsets.fromLTRB(24, 0, 24, 12), - child: Text( - context.l10n.qualityNote, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: colorScheme.onSurfaceVariant, - fontStyle: FontStyle.italic, - ), - ), - ), for (final quality in qualityOptions) _QualityOption( title: _localizedQualityLabel(context, quality), @@ -429,7 +300,6 @@ class _ServiceChip extends StatelessWidget { final VoidCallback? onTap; final String? iconPath; final String? healthStatus; - final bool isDisabled; const _ServiceChip({ required this.label, @@ -437,21 +307,18 @@ class _ServiceChip extends StatelessWidget { required this.onTap, this.iconPath, this.healthStatus, - this.isDisabled = false, }); @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; return GestureDetector( - onTap: isDisabled ? null : onTap, + onTap: onTap, child: AnimatedContainer( duration: const Duration(milliseconds: 200), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), decoration: BoxDecoration( - color: isDisabled - ? colorScheme.surfaceContainerHighest.withValues(alpha: 0.5) - : isSelected + color: isSelected ? colorScheme.primaryContainer : colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(12), @@ -465,7 +332,7 @@ class _ServiceChip extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ if (healthStatus != null) ...[ - _ServiceHealthDot(status: healthStatus!, isDisabled: isDisabled), + _ServiceHealthDot(status: healthStatus!), const SizedBox(width: 8), ], if (iconPath != null) ...[ @@ -479,9 +346,7 @@ class _ServiceChip extends StatelessWidget { errorBuilder: (context, error, stackTrace) => Icon( Icons.extension, size: 18, - color: isDisabled - ? colorScheme.onSurfaceVariant.withValues(alpha: 0.4) - : isSelected + color: isSelected ? colorScheme.onPrimaryContainer : colorScheme.onSurfaceVariant, ), @@ -493,9 +358,7 @@ class _ServiceChip extends StatelessWidget { label, style: TextStyle( fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal, - color: isDisabled - ? colorScheme.onSurfaceVariant.withValues(alpha: 0.4) - : isSelected + color: isSelected ? colorScheme.onPrimaryContainer : colorScheme.onSurfaceVariant, ), @@ -509,15 +372,12 @@ class _ServiceChip extends StatelessWidget { class _ServiceHealthDot extends StatelessWidget { final String status; - final bool isDisabled; - const _ServiceHealthDot({required this.status, required this.isDisabled}); + const _ServiceHealthDot({required this.status}); @override Widget build(BuildContext context) { - final color = isDisabled - ? Theme.of(context).colorScheme.onSurfaceVariant.withValues(alpha: 0.3) - : _serviceHealthColor(status); + final color = _serviceHealthColor(status); return Tooltip( message: _serviceHealthTooltip(status), child: Container(