diff --git a/.gitignore b/.gitignore
index 3c57112b..699e248c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -67,6 +67,7 @@ AGENTS.md
# Temp/misc
nul
+network_requests.txt
# Log files
*.log
diff --git a/README.md b/README.md
index b52224b2..9ccd9abc 100644
--- a/README.md
+++ b/README.md
@@ -25,7 +25,7 @@
-[](https://github.com/zarzet/SpotiFLAC-Mobile/releases)
+[](https://github.com/zarzet/SpotiFLAC-Mobile/releases)
[](https://www.virustotal.com/gui/file/0a2bd2a033551983fc9fcd83f82fd912c83914fd1094cd8d1c7c6a68eb23233f)
[](https://crowdin.com/project/spotiflac-mobile)
diff --git a/go_backend/exports.go b/go_backend/exports.go
index 2e833c01..5a8a7840 100644
--- a/go_backend/exports.go
+++ b/go_backend/exports.go
@@ -2602,6 +2602,28 @@ func HandleURLWithExtensionJSON(url string) (string, error) {
artistResponse["albums"] = albums
}
+ if len(result.Artist.Releases) > 0 {
+ releases := make([]map[string]interface{}, len(result.Artist.Releases))
+ for i, release := range result.Artist.Releases {
+ releaseType := release.AlbumType
+ if releaseType == "" {
+ releaseType = "album"
+ }
+ releases[i] = map[string]interface{}{
+ "id": release.ID,
+ "name": release.Name,
+ "artists": release.Artists,
+ "images": release.CoverURL,
+ "cover_url": release.CoverURL,
+ "release_date": release.ReleaseDate,
+ "total_tracks": release.TotalTracks,
+ "album_type": releaseType,
+ "provider_id": release.ProviderID,
+ }
+ }
+ artistResponse["releases"] = releases
+ }
+
if len(result.Artist.TopTracks) > 0 {
topTracks := make([]map[string]interface{}, len(result.Artist.TopTracks))
for i, track := range result.Artist.TopTracks {
@@ -2851,6 +2873,27 @@ func GetArtistWithExtensionJSON(extensionID, artistID string) (string, error) {
"provider_id": artist.ProviderID,
}
+ if len(artist.Releases) > 0 {
+ releases := make([]map[string]interface{}, len(artist.Releases))
+ for i, release := range artist.Releases {
+ releaseType := release.AlbumType
+ if releaseType == "" {
+ releaseType = "album"
+ }
+ releases[i] = map[string]interface{}{
+ "id": release.ID,
+ "name": release.Name,
+ "artists": release.Artists,
+ "cover_url": release.CoverURL,
+ "release_date": release.ReleaseDate,
+ "total_tracks": release.TotalTracks,
+ "album_type": releaseType,
+ "provider_id": release.ProviderID,
+ }
+ }
+ response["releases"] = releases
+ }
+
if artist.HeaderImage != "" {
response["header_image"] = artist.HeaderImage
}
diff --git a/go_backend/extension_providers.go b/go_backend/extension_providers.go
index 13d759af..1866543f 100644
--- a/go_backend/extension_providers.go
+++ b/go_backend/extension_providers.go
@@ -70,6 +70,7 @@ type ExtArtistMetadata struct {
HeaderImage string `json:"header_image,omitempty"`
Listeners int `json:"listeners,omitempty"`
Albums []ExtAlbumMetadata `json:"albums,omitempty"`
+ Releases []ExtAlbumMetadata `json:"releases,omitempty"`
TopTracks []ExtTrackMetadata `json:"top_tracks,omitempty"`
ProviderID string `json:"provider_id"`
}
@@ -327,6 +328,12 @@ func (p *ExtensionProviderWrapper) GetArtist(artistID string) (*ExtArtistMetadat
}
artist.ProviderID = p.extension.ID
+ for i := range artist.Releases {
+ artist.Releases[i].ProviderID = p.extension.ID
+ for j := range artist.Releases[i].Tracks {
+ artist.Releases[i].Tracks[j].ProviderID = p.extension.ID
+ }
+ }
return &artist, nil
}
@@ -970,6 +977,24 @@ func DownloadWithExtensionFallback(req DownloadRequest) (*DownloadResponse, erro
if enrichedTrack.Artists != "" {
req.ArtistName = enrichedTrack.Artists
}
+ if enrichedTrack.AlbumName != "" && req.AlbumName == "" {
+ GoLog("[DownloadWithExtensionFallback] AlbumName from enrichment: %s\n", enrichedTrack.AlbumName)
+ req.AlbumName = enrichedTrack.AlbumName
+ }
+ if enrichedTrack.AlbumArtist != "" && req.AlbumArtist == "" {
+ req.AlbumArtist = enrichedTrack.AlbumArtist
+ }
+ if enrichedTrack.DurationMS > 0 && req.DurationMS == 0 {
+ GoLog("[DownloadWithExtensionFallback] DurationMS from enrichment: %d\n", enrichedTrack.DurationMS)
+ req.DurationMS = enrichedTrack.DurationMS
+ }
+ if enrichedTrack.CoverURL != "" && req.CoverURL == "" {
+ req.CoverURL = enrichedTrack.CoverURL
+ }
+ if enrichedTrack.ID != "" && req.SpotifyID == "" {
+ GoLog("[DownloadWithExtensionFallback] Track ID from enrichment: %s\n", enrichedTrack.ID)
+ req.SpotifyID = enrichedTrack.ID
+ }
if enrichedTrack.Label != "" && req.Label == "" {
GoLog("[DownloadWithExtensionFallback] Label from enrichment: %s\n", enrichedTrack.Label)
req.Label = enrichedTrack.Label
@@ -990,6 +1015,73 @@ func DownloadWithExtensionFallback(req DownloadRequest) (*DownloadResponse, erro
}
}
+ // If key metadata is still missing after extension enrichment, search
+ // configured metadata providers (Spotify/Deezer/Tidal/Qobuz) — same
+ // logic that ReEnrichFile uses.
+ if req.Source != "" && !isBuiltInProvider(strings.ToLower(req.Source)) &&
+ req.TrackName != "" && req.ArtistName != "" &&
+ (req.AlbumName == "" || req.ReleaseDate == "" || req.ISRC == "") {
+
+ searchQuery := req.TrackName + " " + req.ArtistName
+ GoLog("[DownloadWithExtensionFallback] Metadata incomplete, searching providers for: %s\n", searchQuery)
+
+ tracks, searchErr := extManager.SearchTracksWithMetadataProviders(searchQuery, 5, true)
+ if searchErr == nil && len(tracks) > 0 {
+ track := tracks[0]
+ GoLog("[DownloadWithExtensionFallback] Metadata match (%s): %s - %s (album: %s, date: %s, isrc: %s)\n",
+ track.ProviderID, track.Name, track.Artists, track.AlbumName, track.ReleaseDate, track.ISRC)
+
+ if track.AlbumName != "" && req.AlbumName == "" {
+ req.AlbumName = track.AlbumName
+ }
+ if track.AlbumArtist != "" && req.AlbumArtist == "" {
+ req.AlbumArtist = track.AlbumArtist
+ }
+ if track.ReleaseDate != "" && req.ReleaseDate == "" {
+ req.ReleaseDate = track.ReleaseDate
+ }
+ if track.ISRC != "" && req.ISRC == "" {
+ req.ISRC = track.ISRC
+ }
+ if track.TrackNumber > 0 && req.TrackNumber == 0 {
+ req.TrackNumber = track.TrackNumber
+ }
+ if track.DiscNumber > 0 && req.DiscNumber == 0 {
+ req.DiscNumber = track.DiscNumber
+ }
+ if track.CoverURL != "" && req.CoverURL == "" {
+ req.CoverURL = track.CoverURL
+ }
+ if track.Genre != "" && req.Genre == "" {
+ req.Genre = track.Genre
+ }
+ if track.Label != "" && req.Label == "" {
+ req.Label = track.Label
+ }
+ if track.Copyright != "" && req.Copyright == "" {
+ req.Copyright = track.Copyright
+ }
+ } else if searchErr != nil {
+ GoLog("[DownloadWithExtensionFallback] Metadata provider search failed (non-fatal): %v\n", searchErr)
+ }
+
+ // Try Deezer extended metadata for genre/label if we have ISRC
+ if req.ISRC != "" && (req.Genre == "" || req.Label == "") {
+ ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+ extMeta, err := GetDeezerClient().GetExtendedMetadataByISRC(ctx, req.ISRC)
+ cancel()
+ if err == nil && extMeta != nil {
+ if req.Genre == "" && extMeta.Genre != "" {
+ req.Genre = extMeta.Genre
+ }
+ if req.Label == "" && extMeta.Label != "" {
+ req.Label = extMeta.Label
+ }
+ GoLog("[DownloadWithExtensionFallback] Extended metadata from Deezer: genre=%s, label=%s\n", req.Genre, req.Label)
+ }
+ }
+ }
+
if req.Source != "" &&
!isBuiltInProvider(strings.ToLower(req.Source)) &&
(!strictMode || selectedProvider == "" || strings.EqualFold(selectedProvider, req.Source)) {
@@ -1083,6 +1175,30 @@ func DownloadWithExtensionFallback(req DownloadRequest) (*DownloadResponse, erro
}
}
+ // Always pass enriched metadata from req so Flutter can
+ // embed it — fills gaps from metadata provider search.
+ if req.AlbumName != "" && resp.Album == "" {
+ resp.Album = req.AlbumName
+ }
+ if req.AlbumArtist != "" && resp.AlbumArtist == "" {
+ resp.AlbumArtist = req.AlbumArtist
+ }
+ if req.ReleaseDate != "" && resp.ReleaseDate == "" {
+ resp.ReleaseDate = req.ReleaseDate
+ }
+ if req.ISRC != "" && resp.ISRC == "" {
+ resp.ISRC = req.ISRC
+ }
+ if req.TrackNumber > 0 && resp.TrackNumber == 0 {
+ resp.TrackNumber = req.TrackNumber
+ }
+ if req.DiscNumber > 0 && resp.DiscNumber == 0 {
+ resp.DiscNumber = req.DiscNumber
+ }
+ if req.CoverURL != "" && resp.CoverURL == "" {
+ resp.CoverURL = req.CoverURL
+ }
+
return resp, nil
}
@@ -1636,6 +1752,12 @@ func (p *ExtensionProviderWrapper) HandleURL(url string) (*ExtURLHandleResult, e
handleResult.Artist.Albums[i].Tracks[j].ProviderID = p.extension.ID
}
}
+ for i := range handleResult.Artist.Releases {
+ handleResult.Artist.Releases[i].ProviderID = p.extension.ID
+ for j := range handleResult.Artist.Releases[i].Tracks {
+ handleResult.Artist.Releases[i].Tracks[j].ProviderID = p.extension.ID
+ }
+ }
for i := range handleResult.Artist.TopTracks {
handleResult.Artist.TopTracks[i].ProviderID = p.extension.ID
}
diff --git a/lib/providers/download_queue_provider.dart b/lib/providers/download_queue_provider.dart
index 4761a22a..df46e084 100644
--- a/lib/providers/download_queue_provider.dart
+++ b/lib/providers/download_queue_provider.dart
@@ -2373,11 +2373,25 @@ class DownloadQueueNotifier extends Notifier
{
final backendAlbum = normalizeOptionalString(
backendResult['album'] as String?,
);
+ final backendIsrc = normalizeOptionalString(
+ backendResult['isrc'] as String?,
+ );
+ final backendCoverUrl = normalizeOptionalString(
+ backendResult['cover_url'] as String?,
+ );
+ final backendAlbumArtist = normalizeOptionalString(
+ backendResult['album_artist'] as String?,
+ );
- if (backendTrackNum == null &&
- backendDiscNum == null &&
- backendYear == null &&
- backendAlbum == null) {
+ final hasOverrides = backendTrackNum != null ||
+ backendDiscNum != null ||
+ backendYear != null ||
+ backendAlbum != null ||
+ backendIsrc != null ||
+ backendCoverUrl != null ||
+ backendAlbumArtist != null;
+
+ if (!hasOverrides) {
return baseTrack;
}
@@ -2386,12 +2400,12 @@ class DownloadQueueNotifier extends Notifier {
name: baseTrack.name,
artistName: baseTrack.artistName,
albumName: backendAlbum ?? baseTrack.albumName,
- albumArtist: resolvedAlbumArtist,
+ albumArtist: backendAlbumArtist ?? resolvedAlbumArtist,
artistId: baseTrack.artistId,
albumId: baseTrack.albumId,
- coverUrl: baseTrack.coverUrl,
+ coverUrl: backendCoverUrl ?? baseTrack.coverUrl,
duration: baseTrack.duration,
- isrc: baseTrack.isrc,
+ isrc: backendIsrc ?? baseTrack.isrc,
trackNumber: backendTrackNum ?? baseTrack.trackNumber,
discNumber: backendDiscNum ?? baseTrack.discNumber,
releaseDate: backendYear ?? baseTrack.releaseDate,
diff --git a/lib/providers/track_provider.dart b/lib/providers/track_provider.dart
index 8c1dd6a8..f0552eda 100644
--- a/lib/providers/track_provider.dart
+++ b/lib/providers/track_provider.dart
@@ -933,8 +933,10 @@ 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: data['spotify_id'] as String? ?? '',
+ id: spotifyId.isNotEmpty ? spotifyId : nativeId,
name: data['name'] as String? ?? '',
artistName: data['artists'] as String? ?? '',
albumName: data['album_name'] as String? ?? '',
diff --git a/lib/screens/artist_screen.dart b/lib/screens/artist_screen.dart
index 31ba546e..3efc2d88 100644
--- a/lib/screens/artist_screen.dart
+++ b/lib/screens/artist_screen.dart
@@ -38,12 +38,14 @@ class _ArtistCache {
static void set(
String artistId, {
required List albums,
+ List? releases,
List