diff --git a/CHANGELOG.md b/CHANGELOG.md index 1eae5805..b52a0f13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,16 +1,20 @@ # Changelog -## [3.0.1] - 2026-01-21 +## [Unreleased] + +## [3.1.0] - 2026-01-19 ### Added - **Year in Album Folder Name** ([#50](https://github.com/zarzet/SpotiFLAC-Mobile/issues/50)): New album folder structure options with release year + - `Artist / [Year] Album`: Albums/Coldplay/[2005] X&Y/ - `[Year] Album Only`: Albums/[2005] X&Y/ - Year extracted from release date metadata - Matches desktop SpotiFLAC folder structure - **Extension Album/Playlist/Artist Support**: Extensions can now return albums, playlists, and artists in search results + - Search results now properly separated into Albums, Playlists, Artists, and Songs sections - Albums, playlists, and artists show chevron icon (navigate to detail) instead of download button - Tap album/playlist to view track list and download @@ -28,6 +32,18 @@ ### Fixed +- Fixed search source chips still referencing removed badge props. +- Fixed extension artist album metadata to preserve provider IDs and cover URLs for correct navigation. +- Fixed extension playlist fetch to populate provider IDs and reject disabled extensions. +- Fixed extension collection screens calling setState after dispose during async loads. +- Fixed URL handler responses to include provider IDs for extension albums and artists. +- Fixed YTMusic extension not extracting album name and duration from search results. + - Album name is now extracted from flexColumns/subtitle when linked to album browseId. + - Duration is now extracted from fixedColumns/flexColumns in addition to existing sources. +- Fixed "Separate Singles" setting not working ([#54](https://github.com/zarzet/SpotiFLAC-Mobile/issues/54)) - singles were going to Albums folder. + - Root cause: `albumType` was not being extracted from Deezer API during metadata enrichment. + - Deezer track responses now correctly include `album_type` (single/ep/album/compilation). + - Track creation now preserves `albumType` and `source` fields throughout download flow. - Fixed PageView overscroll at edges (BouncingScrollPhysics → ClampingScrollPhysics) - Fixed settings item highlight on swipe (highlightColor: Colors.transparent) - Fixed extension duplicate load error (skip silently instead of throwing error) @@ -46,11 +62,12 @@ ## [3.0.0] - 2026-01-14 -### 🎉 Extension System (Major Feature) +### Extension System (Major Feature) SpotiFLAC 3.0 introduces a powerful extension system that allows third-party integrations for metadata, downloads, and more. #### Extension Store + - Browse and install extensions directly from the app - New "Store" tab in bottom navigation - Browse by category: Metadata, Download, Utility, Lyrics, Integration @@ -59,6 +76,7 @@ SpotiFLAC 3.0 introduces a powerful extension system that allows third-party int - Offline cache for browsing without internet #### Extension Capabilities + - **Custom Search Providers** - **Custom URL Handlers** - **Custom Thumbnail Ratios**: Square (1:1), Wide (16:9), Portrait (2:3) @@ -66,6 +84,7 @@ SpotiFLAC 3.0 introduces a powerful extension system that allows third-party int - **Quality Options**: Extensions can define custom quality settings #### Extension APIs + - Full HTTP support: GET, POST, PUT, DELETE, PATCH - Persistent cookie jar per extension - Browser-like polyfills: `fetch()`, `atob()`/`btoa()`, `TextEncoder`/`TextDecoder`, `URL`/`URLSearchParams` @@ -74,6 +93,7 @@ SpotiFLAC 3.0 introduces a powerful extension system that allows third-party int - HMAC-SHA1 utility for cryptographic operations #### Security + - Sandboxed JavaScript runtime (goja) - Permission-based access control - Network domain whitelisting @@ -82,14 +102,17 @@ SpotiFLAC 3.0 introduces a powerful extension system that allows third-party int ### Added - **Album Folder Structure Setting**: Option to remove artist folder from album path + - `Artist / Album` (default): `Albums/Artist Name/Album Name/` - `Album Only`: `Albums/Album Name/` - **Separate Singles Folder**: Organize downloads into Albums/ and Singles/ folders + - Based on `album_type` from Spotify/Deezer metadata - Toggle in Settings > Download > Separate Singles Folder - **Year in Album Folder Name**: New album folder structure options with release year + - `Artist / [Year] Album`: Albums/Coldplay/[2005] X&Y/ - `[Year] Album Only`: Albums/[2005] X&Y/ - Year extracted from release date metadata @@ -103,33 +126,42 @@ SpotiFLAC 3.0 introduces a powerful extension system that allows third-party int ### Fixed - **Back Gesture Freeze on Android 13+**: Fixed app freeze when using back gesture in settings + - Added `PopScope` with `canPop: true` to all settings pages - Changed navigation to use `PageRouteBuilder` with proper slide transition - **Bottom Overflow in Folder Organization Dialog**: Fixed overflow in portrait and landscape mode + - Made dialog scrollable with max height constraint - **Japanese Artist Name Order**: Fixed artist mismatch for Japanese names + - "Sawano Hiroyuki" vs "Hiroyuki Sawano" now correctly matches - **Multi-Artist Matching**: Fixed artist mismatch for collaboration tracks + - "RADWIMPS feat. Toko Miura" now matches when service only shows "Toko Miura" - **Max Resolution Cover Download**: Fixed cover not upgrading to max resolution on mobile + - Mobile now correctly upgrades 300x300 → 640x640 → max resolution (~2000x2000) - **EXISTS: Prefix in File Path**: Fixed "File not found" error in metadata screen + - Duplicate detection prefix now stripped before saving to history - **Extension Search Result Parsing**: Fixed "cannot unmarshal array" error + - Go backend now handles both array and object formats from extensions - **Store Tab Unmount Crash**: Fixed "Using ref when widget is unmounted" error - **Duplicate History Entries**: Fixed duplicate entries when re-downloading same track + - Detects existing entries by Spotify ID, Deezer ID, or ISRC - **Permission Error Message**: Fixed download showing "Song not found" when actually permission error + - Now shows proper message: "Cannot write to folder, check storage permission" - **Android 13+ Storage Permission**: Fixed storage permission not working on Android 13+ @@ -167,62 +199,74 @@ SpotiFLAC 3.0 introduces a powerful extension system that allows third-party int ### Fixed - **Back Gesture Freeze on OnePlus/Android 13+**: Fixed app freeze when using back gesture in settings + - Added `PopScope` with `canPop: true` to all settings pages - Changed navigation to use `PageRouteBuilder` with proper slide transition - Fixes predictive back gesture conflict on devices with gesture navigation - Affected pages: Download, Appearance, Options, Extensions, About, Logs, Extension Detail - **Extension Search Result Parsing**: Fixed "cannot unmarshal array into Go value" error + - Go backend now handles both array and object formats from extensions - Extensions returning `[{track}, {track}]` now work correctly - Extensions returning `{tracks: [...], total: N}` still work as before - **Max Resolution Cover Download**: Fixed cover not upgrading to max resolution on mobile + - Added missing `spotifySize300` constant (300x300 size code) - Mobile now correctly upgrades 300x300 → 640x640 → max resolution (~2000x2000) - Added `_upgradeToMaxQualityCover()` helper in Flutter for M4A conversion path - Go backend `cover.go` now directly replaces URL without HEAD verification - **Extension Search Provider Reset**: Fixed search provider not resetting to default when disabled + - `copyWith` in `AppSettings` couldn't set `searchProvider` to `null` - Added `clearSearchProvider` boolean parameter to properly clear the value - Settings menu now correctly switches back to default provider - **Extension Disabled Search Fallback**: Fixed error when extension is disabled but still called + - `_performSearch` now checks if extension is still enabled before calling custom search - Automatically falls back to Deezer/Spotify search if extension was disabled - Clears `searchProvider` setting if extension no longer available - **Store Tab Unmount Crash**: Fixed "Using ref when widget is unmounted" error + - Added `mounted` check after async operation in `_initialize()` - Prevents crash when navigating away from Store tab during initialization - **EXISTS: Prefix in File Path**: Fixed "File not found" error in metadata screen after download + - Duplicate detection was adding `EXISTS:` prefix to file paths - Prefix now stripped before saving to download history - Legacy history items with prefix are handled gracefully - **History Error Badge**: Fixed error badge showing on history items even when file exists + - `queue_tab.dart` now strips `EXISTS:` prefix before checking file existence - File open and delete operations also use cleaned path - **Extension Artist URL Handler**: Fixed artist pages showing "0 releases" from extensions + - Extension `fetchArtist` now returns correct format: `{ type: "artist", artist: { albums } }` - Go backend `HandleURLWithExtensionJSON` now includes albums in artist response - Added `AlbumType` field to `ExtAlbumMetadata` struct - **Extension Artist Name in Logs**: Fixed empty artist name in extension track logs + - Now uses `firstArtist` + `otherArtists` instead of deprecated `artists.items` - Logs correctly show "Fetched track: {title} by {artist}" - **Japanese Artist Name Order**: Fixed artist mismatch for Japanese names with different order + - "Sawano Hiroyuki" vs "Hiroyuki Sawano" now correctly matches - Added `sameWordsUnordered` check to both Tidal and Qobuz artist matching - Handles Japanese name order (family name first) vs Western name order (given name first) - **Multi-Artist Matching**: Fixed artist mismatch for collaboration tracks + - "RADWIMPS feat. Toko Miura" now matches when Qobuz/Tidal only shows "Toko Miura" - - Split artists by separators (`, `, ` feat. `, ` ft. `, ` & `, ` and `, ` x `) + - Split artists by separators (`, `, `feat.`, `ft.`, `&`, `and`, `x`) - Match if ANY expected artist matches ANY found artist - **Cover Download Logging**: Improved cover download logs for debugging @@ -263,6 +307,7 @@ SpotiFLAC 3.0 introduces a powerful extension system that allows third-party int ### Added - **Extension Store**: Browse and install extensions directly from the app + - New "Store" tab in bottom navigation - Browse extensions by category (Metadata, Download, Utility, Lyrics, Integration) - Search extensions by name, description, or tags @@ -271,6 +316,7 @@ SpotiFLAC 3.0 introduces a powerful extension system that allows third-party int - Extensions hosted at github.com/zarzet/SpotiFLAC-Extension - **Custom URL Handler for Extensions**: Extensions can now register custom URL patterns + - Handle URLs from YouTube Music, SoundCloud, Bandcamp, etc. - Manifest config: `urlHandler: { enabled: true, patterns: ["music.youtube.com"] }` - Implement `handleUrl(url)` function in extension to parse and return track metadata @@ -278,6 +324,7 @@ SpotiFLAC 3.0 introduces a powerful extension system that allows third-party int - Supports share intents and paste from clipboard - **Artist URL Handler Support**: Extensions can now return artist data from URL handlers + - Added `type: "artist"` handling in track_provider.dart - Navigate to artist screen with albums list from extension @@ -355,7 +402,7 @@ SpotiFLAC 3.0 introduces a powerful extension system that allows third-party int - **Full HTTP Method Support**: New shortcut methods for all common HTTP verbs - `http.put(url, body, headers)` - PUT requests - - `http.delete(url, headers)` - DELETE requests + - `http.delete(url, headers)` - DELETE requests - `http.patch(url, body, headers)` - PATCH requests - `http.clearCookies()` - Clear all cookies for the extension - **Persistent Cookie Jar**: Each extension now has its own cookie jar @@ -397,6 +444,7 @@ SpotiFLAC 3.0 introduces a powerful extension system that allows third-party int ## [3.0.0-alpha.1] - 2026-01-11 #### Extension System + - **Custom Search Providers**: Extensions can now provide custom search functionality - YouTube, SoundCloud, and other platforms via extensions - Custom search placeholder text per extension diff --git a/go_backend/deezer.go b/go_backend/deezer.go index 42231e26..38dae095 100644 --- a/go_backend/deezer.go +++ b/go_backend/deezer.go @@ -89,11 +89,9 @@ type deezerAlbumSimple struct { CoverBig string `json:"cover_big"` CoverXL string `json:"cover_xl"` ReleaseDate string `json:"release_date"` // Sometimes at album level + RecordType string `json:"record_type"` // album, single, ep, compile } -// ... (skip other structs as they are fine/unchanged) ... - -// ... (in convertTrack) ... func (c *DeezerClient) convertTrack(track deezerTrack) TrackMetadata { artistName := track.Artist.Name if len(track.Contributors) > 0 { diff --git a/go_backend/exports.go b/go_backend/exports.go index 090d09d8..e1d869b7 100644 --- a/go_backend/exports.go +++ b/go_backend/exports.go @@ -1638,15 +1638,17 @@ func HandleURLWithExtensionJSON(url string) (string, error) { "release_date": result.Album.ReleaseDate, "total_tracks": result.Album.TotalTracks, "album_type": result.Album.AlbumType, + "provider_id": result.Album.ProviderID, } } // Add artist info if present if result.Artist != nil { artistResponse := map[string]interface{}{ - "id": result.Artist.ID, - "name": result.Artist.Name, - "image_url": result.Artist.ImageURL, + "id": result.Artist.ID, + "name": result.Artist.Name, + "image_url": result.Artist.ImageURL, + "provider_id": result.Artist.ProviderID, } // Add albums if present @@ -1662,9 +1664,11 @@ func HandleURLWithExtensionJSON(url string) (string, error) { "name": album.Name, "artists": album.Artists, "images": album.CoverURL, + "cover_url": album.CoverURL, "release_date": album.ReleaseDate, "total_tracks": album.TotalTracks, "album_type": albumType, + "provider_id": album.ProviderID, } } artistResponse["albums"] = albums @@ -1703,6 +1707,9 @@ func GetAlbumWithExtensionJSON(extensionID, albumID string) (string, error) { if !ext.Manifest.IsMetadataProvider() { return "", fmt.Errorf("extension '%s' is not a metadata provider", extensionID) } + if !ext.Enabled { + return "", fmt.Errorf("extension '%s' is disabled", extensionID) + } provider := NewExtensionProviderWrapper(ext) album, err := provider.GetAlbum(albumID) @@ -1807,6 +1814,10 @@ func GetPlaylistWithExtensionJSON(extensionID, playlistID string) (string, error if err := json.Unmarshal(jsonBytes, &album); err != nil { return "", fmt.Errorf("failed to parse playlist: %w", err) } + album.ProviderID = ext.ID + for i := range album.Tracks { + album.Tracks[i].ProviderID = ext.ID + } // Convert tracks to map format tracks := make([]map[string]interface{}, len(album.Tracks)) diff --git a/go_backend/extension_providers.go b/go_backend/extension_providers.go index c2c5fcfd..c983165e 100644 --- a/go_backend/extension_providers.go +++ b/go_backend/extension_providers.go @@ -1213,6 +1213,21 @@ func (p *ExtensionProviderWrapper) HandleURL(url string) (*ExtURLHandleResult, e for i := range handleResult.Tracks { handleResult.Tracks[i].ProviderID = p.extension.ID } + if handleResult.Album != nil { + handleResult.Album.ProviderID = p.extension.ID + for i := range handleResult.Album.Tracks { + handleResult.Album.Tracks[i].ProviderID = p.extension.ID + } + } + if handleResult.Artist != nil { + handleResult.Artist.ProviderID = p.extension.ID + for i := range handleResult.Artist.Albums { + handleResult.Artist.Albums[i].ProviderID = p.extension.ID + for j := range handleResult.Artist.Albums[i].Tracks { + handleResult.Artist.Albums[i].Tracks[j].ProviderID = p.extension.ID + } + } + } return &handleResult, nil } diff --git a/lib/constants/app_info.dart b/lib/constants/app_info.dart index 5049a914..1f0a11fd 100644 --- a/lib/constants/app_info.dart +++ b/lib/constants/app_info.dart @@ -1,8 +1,8 @@ /// App version and info constants /// Update version here only - all other files will reference this class AppInfo { - static const String version = '3.0.1'; - static const String buildNumber = '58'; + static const String version = '3.1.0'; + static const String buildNumber = '59'; static const String fullVersion = '$version+$buildNumber'; diff --git a/lib/providers/download_queue_provider.dart b/lib/providers/download_queue_provider.dart index 6a0355af..03d6c808 100644 --- a/lib/providers/download_queue_provider.dart +++ b/lib/providers/download_queue_provider.dart @@ -1461,6 +1461,7 @@ class DownloadQueueNotifier extends Notifier { final data = trackData; _log.d('Track data keys: ${data.keys.toList()}'); _log.d('ISRC from API: ${data['isrc']}'); + _log.d('album_type from API: ${data['album_type']}'); trackToDownload = Track( id: (data['spotify_id'] as String?) ?? trackToDownload.id, name: (data['name'] as String?) ?? trackToDownload.name, @@ -1482,9 +1483,12 @@ class DownloadQueueNotifier extends Notifier { releaseDate: data['release_date'] as String?, deezerId: rawId, availability: trackToDownload.availability, + // Preserve albumType from API response or original track + albumType: (data['album_type'] as String?) ?? trackToDownload.albumType, + source: trackToDownload.source, ); _log.d( - 'Metadata enriched: Track ${trackToDownload.trackNumber}, Disc ${trackToDownload.discNumber}, ISRC ${trackToDownload.isrc}', + 'Metadata enriched: Track ${trackToDownload.trackNumber}, Disc ${trackToDownload.discNumber}, ISRC ${trackToDownload.isrc}, AlbumType ${trackToDownload.albumType}', ); } else { _log.w('Unexpected track data type: ${trackData.runtimeType}'); @@ -1720,6 +1724,8 @@ class DownloadQueueNotifier extends Notifier { releaseDate: backendYear ?? trackToDownload.releaseDate, deezerId: trackToDownload.deezerId, availability: trackToDownload.availability, + albumType: trackToDownload.albumType, + source: trackToDownload.source, ); } diff --git a/lib/providers/track_provider.dart b/lib/providers/track_provider.dart index 61651414..29e57250 100644 --- a/lib/providers/track_provider.dart +++ b/lib/providers/track_provider.dart @@ -455,6 +455,8 @@ class TrackNotifier extends Notifier { trackNumber: track.trackNumber, discNumber: track.discNumber, releaseDate: track.releaseDate, + albumType: track.albumType, + source: track.source, availability: ServiceAvailability( tidal: availability['tidal'] as bool? ?? false, qobuz: availability['qobuz'] as bool? ?? false, @@ -552,9 +554,10 @@ class TrackNotifier extends Notifier { name: data['name'] as String? ?? '', releaseDate: data['release_date'] as String? ?? '', totalTracks: data['total_tracks'] as int? ?? 0, - coverUrl: data['images'] as String?, + coverUrl: (data['cover_url'] ?? data['images'])?.toString(), albumType: data['album_type'] as String? ?? 'album', artists: data['artists'] as String? ?? '', + providerId: data['provider_id']?.toString(), ); } diff --git a/lib/screens/home_tab.dart b/lib/screens/home_tab.dart index 388f41fa..bc0cab6b 100644 --- a/lib/screens/home_tab.dart +++ b/lib/screens/home_tab.dart @@ -1431,6 +1431,7 @@ class _ExtensionAlbumScreenState extends ConsumerState { widget.extensionId, widget.albumId, ); + if (!mounted) return; if (result == null) { setState(() { @@ -1457,6 +1458,7 @@ class _ExtensionAlbumScreenState extends ConsumerState { _isLoading = false; }); } catch (e) { + if (!mounted) return; setState(() { _error = 'Error: $e'; _isLoading = false; @@ -1567,6 +1569,7 @@ class _ExtensionPlaylistScreenState extends ConsumerState { widget.extensionId, widget.artistId, ); + if (!mounted) return; if (result == null) { setState(() { @@ -1728,6 +1733,7 @@ class _ExtensionArtistScreenState extends ConsumerState { _isLoading = false; }); } catch (e) { + if (!mounted) return; setState(() { _error = 'Error: $e'; _isLoading = false; diff --git a/lib/screens/settings/options_settings_page.dart b/lib/screens/settings/options_settings_page.dart index da666cd8..081c9fb9 100644 --- a/lib/screens/settings/options_settings_page.dart +++ b/lib/screens/settings/options_settings_page.dart @@ -845,8 +845,6 @@ class _MetadataSourceSelector extends ConsumerWidget { _SourceChip( icon: Icons.graphic_eq, label: 'Deezer', - badge: 'Free', - badgeColor: colorScheme.tertiary, // Not selected if extension is active isSelected: currentSource == 'deezer' && !hasExtensionSearch, onTap: () { @@ -861,8 +859,6 @@ class _MetadataSourceSelector extends ConsumerWidget { _SourceChip( icon: Icons.music_note, label: 'Spotify', - badge: 'API Key', - badgeColor: colorScheme.secondary, // Not selected if extension is active isSelected: currentSource == 'spotify' && !hasExtensionSearch, onTap: () { diff --git a/pubspec.yaml b/pubspec.yaml index 9900e1ce..539637e0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: spotiflac_android description: Download Spotify tracks in FLAC from Tidal, Qobuz & Amazon Music publish_to: "none" -version: 3.0.1+58 +version: 3.1.0+59 environment: sdk: ^3.10.0 diff --git a/pubspec_ios.yaml b/pubspec_ios.yaml index 2864bbe2..9d369cc5 100644 --- a/pubspec_ios.yaml +++ b/pubspec_ios.yaml @@ -1,7 +1,7 @@ name: spotiflac_android description: Download Spotify tracks in FLAC from Tidal, Qobuz & Amazon Music publish_to: "none" -version: 3.0.0-beta.2+56 +version: 3.1.0+59 environment: sdk: ^3.10.0