diff --git a/CHANGELOG.md b/CHANGELOG.md index de083e93..19c1ec95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,12 +7,40 @@ - "Use Primary Artist Only" setting: strips featured artists from folder names (e.g. "Justin Bieber, Quavo" becomes "Justin Bieber") for cleaner folder organization - Supports separators: `, ` `;` `&` `feat.` `ft.` `featuring` `with` `x` - Available in Settings > Download > below "Use Album Artist for folders" +- Unified download request contract (`DownloadRequestPayload`) for all providers/flows + - Includes full superset fields: lyrics mode, genre/label/copyright, provider IDs, SAF params, cover/quality settings + - Added strategy flags in payload: `use_extensions`, `use_fallback` +- New Go unified router entrypoint: `DownloadByStrategy(requestJSON)` + - Routing priority: YouTube service -> extension fallback -> built-in fallback -> direct service +- New Android method channel handler: `"downloadByStrategy"` -> `Gobackend.downloadByStrategy(...)` + +### Changed + +- Download queue execution now builds one payload and uses a single bridge entrypoint (`PlatformBridge.downloadByStrategy`) instead of branching into multiple bridge methods +- Dart `downloadByStrategy` now sends a single request to Go (`downloadByStrategy` channel); routing concern is centralized in Go backend +- Legacy Dart bridge methods (`downloadTrack`, `downloadWithFallback`, `downloadWithExtensions`, `downloadFromYouTube`) are now thin wrappers and marked `@Deprecated` ### Fixed - Fixed lyrics mode "External .LRC" still embedding lyrics into metadata - `lyrics_mode` was not being sent to Go backend for single-service downloads and YouTube provider, causing Go to default to "embed" - Fixed `flutter_local_notifications` v20 breaking changes - migrated all `initialize()`, `show()`, and `cancel()` calls from positional parameters to named parameters - Fixed SAF duplicate folder bug: concurrent batch downloads creating empty folders with `(1)`, `(2)`, `(3)` suffixes - added synchronized lock to `ensureDocumentDir` in Kotlin with duplicate detection and cleanup +- Inconsistent parameter parity across download paths + - `downloadWithExtensions` now carries `copyright` + - YouTube path now carries `embed_max_quality_cover` and metadata parity fields +- Inconsistent success response metadata between direct/fallback flows + - Added shared Go response builder for `DownloadTrack` and `DownloadWithFallback` + - Success responses now consistently include `genre`, `label`, `copyright`, and `lyrics_lrc` +- YouTube success response now also includes extended metadata fields (`cover_url`, `genre`, `label`, `copyright`) for parity with other providers + +### Technical + +- Centralized request serialization in `PlatformBridge` via shared invoke helper and unified payload model +- Go strategy router normalizes incoming service casing before dispatch +- Verified integration after AAR refresh with: + - `flutter analyze` + - `go test -v ./...` + - Android Kotlin compile check (`:app:compileDebugKotlin`) --- 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 52615fbe..365ad757 100644 --- a/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt +++ b/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt @@ -1294,24 +1294,6 @@ class MainActivity: FlutterFragmentActivity() { } result.success(response) } - "downloadTrack" -> { - val requestJson = call.arguments as String - val response = withContext(Dispatchers.IO) { - handleSafDownload(requestJson) { json -> - Gobackend.downloadTrack(json) - } - } - result.success(response) - } - "downloadWithFallback" -> { - val requestJson = call.arguments as String - val response = withContext(Dispatchers.IO) { - handleSafDownload(requestJson) { json -> - Gobackend.downloadWithFallback(json) - } - } - result.success(response) - } "downloadByStrategy" -> { val requestJson = call.arguments as String val response = withContext(Dispatchers.IO) { @@ -2120,24 +2102,6 @@ class MainActivity: FlutterFragmentActivity() { } result.success(response) } - "downloadWithExtensions" -> { - val requestJson = call.arguments as String - val response = withContext(Dispatchers.IO) { - handleSafDownload(requestJson) { json -> - Gobackend.downloadWithExtensionsJSON(json) - } - } - result.success(response) - } - "downloadFromYouTube" -> { - val requestJson = call.arguments as String - val response = withContext(Dispatchers.IO) { - handleSafDownload(requestJson) { json -> - Gobackend.downloadFromYouTube(json) - } - } - result.success(response) - } "enrichTrackWithExtension" -> { val extensionId = call.argument("extension_id") ?: "" val trackJson = call.argument("track") ?: "{}" diff --git a/lib/services/platform_bridge.dart b/lib/services/platform_bridge.dart index 74bcaed0..9e84c4bf 100644 --- a/lib/services/platform_bridge.dart +++ b/lib/services/platform_bridge.dart @@ -103,137 +103,7 @@ class PlatformBridge { return response; } - @Deprecated('Use downloadByStrategy with DownloadRequestPayload.') - static Future> downloadTrack({ - required String isrc, - required String service, - required String spotifyId, - required String trackName, - required String artistName, - required String albumName, - String? albumArtist, - String? coverUrl, - required String outputDir, - required String filenameFormat, - String quality = 'LOSSLESS', - bool embedLyrics = true, - bool embedMaxQualityCover = true, - int trackNumber = 1, - int discNumber = 1, - int totalTracks = 1, - String? releaseDate, - String? itemId, - int durationMs = 0, - String? source, - String? genre, - String? label, - String? copyright, - String lyricsMode = 'embed', - String storageMode = 'app', - String safTreeUri = '', - String safRelativeDir = '', - String safFileName = '', - String safOutputExt = '', - }) async { - final payload = DownloadRequestPayload( - isrc: isrc, - service: service, - spotifyId: spotifyId, - trackName: trackName, - artistName: artistName, - albumName: albumName, - albumArtist: albumArtist ?? artistName, - coverUrl: coverUrl ?? '', - outputDir: outputDir, - filenameFormat: filenameFormat, - quality: quality, - embedLyrics: embedLyrics, - embedMaxQualityCover: embedMaxQualityCover, - trackNumber: trackNumber, - discNumber: discNumber, - totalTracks: totalTracks, - releaseDate: releaseDate ?? '', - itemId: itemId ?? '', - durationMs: durationMs, - source: source ?? '', - genre: genre ?? '', - label: label ?? '', - copyright: copyright ?? '', - lyricsMode: lyricsMode, - storageMode: storageMode, - safTreeUri: safTreeUri, - safRelativeDir: safRelativeDir, - safFileName: safFileName, - safOutputExt: safOutputExt, - ); - return downloadByStrategy(payload: payload); - } - - @Deprecated('Use downloadByStrategy with DownloadRequestPayload.') - static Future> downloadWithFallback({ - required String isrc, - required String spotifyId, - required String trackName, - required String artistName, - required String albumName, - String? albumArtist, - String? coverUrl, - required String outputDir, - required String filenameFormat, - String quality = 'LOSSLESS', - bool embedLyrics = true, - bool embedMaxQualityCover = true, - int trackNumber = 1, - int discNumber = 1, - int totalTracks = 1, - String? releaseDate, - String preferredService = 'tidal', - String? itemId, - int durationMs = 0, - String? genre, - String? label, - String? copyright, - String lyricsMode = 'embed', - String storageMode = 'app', - String safTreeUri = '', - String safRelativeDir = '', - String safFileName = '', - String safOutputExt = '', - }) async { - final payload = DownloadRequestPayload( - isrc: isrc, - service: preferredService, - spotifyId: spotifyId, - trackName: trackName, - artistName: artistName, - albumName: albumName, - albumArtist: albumArtist ?? artistName, - coverUrl: coverUrl ?? '', - outputDir: outputDir, - filenameFormat: filenameFormat, - quality: quality, - embedLyrics: embedLyrics, - embedMaxQualityCover: embedMaxQualityCover, - trackNumber: trackNumber, - discNumber: discNumber, - totalTracks: totalTracks, - releaseDate: releaseDate ?? '', - itemId: itemId ?? '', - durationMs: durationMs, - genre: genre ?? '', - label: label ?? '', - copyright: copyright ?? '', - lyricsMode: lyricsMode, - storageMode: storageMode, - safTreeUri: safTreeUri, - safRelativeDir: safRelativeDir, - safFileName: safFileName, - safOutputExt: safOutputExt, - ); - - return downloadByStrategy(payload: payload, useFallback: true); - } static Future> getDownloadProgress() async { final result = await _channel.invokeMethod('getDownloadProgress'); @@ -849,78 +719,7 @@ class PlatformBridge { return list.map((e) => e as Map).toList(); } - @Deprecated('Use downloadByStrategy with DownloadRequestPayload.') - static Future> downloadWithExtensions({ - required String isrc, - required String spotifyId, - required String trackName, - required String artistName, - required String albumName, - String? albumArtist, - String? coverUrl, - required String outputDir, - required String filenameFormat, - String quality = 'LOSSLESS', - bool embedLyrics = true, - bool embedMaxQualityCover = true, - int trackNumber = 1, - int discNumber = 1, - int totalTracks = 1, - String? releaseDate, - String? itemId, - int durationMs = 0, - String? source, - String? genre, - String? label, - String? copyright, - String? tidalId, - String? qobuzId, - String? deezerId, - String lyricsMode = 'embed', - String? preferredService, - String storageMode = 'app', - String safTreeUri = '', - String safRelativeDir = '', - String safFileName = '', - String safOutputExt = '', - }) async { - final payload = DownloadRequestPayload( - isrc: isrc, - service: preferredService ?? '', - spotifyId: spotifyId, - trackName: trackName, - artistName: artistName, - albumName: albumName, - albumArtist: albumArtist ?? artistName, - coverUrl: coverUrl ?? '', - outputDir: outputDir, - filenameFormat: filenameFormat, - quality: quality, - embedLyrics: embedLyrics, - embedMaxQualityCover: embedMaxQualityCover, - trackNumber: trackNumber, - discNumber: discNumber, - totalTracks: totalTracks, - releaseDate: releaseDate ?? '', - itemId: itemId ?? '', - durationMs: durationMs, - source: source ?? '', - genre: genre ?? '', - label: label ?? '', - copyright: copyright ?? '', - tidalId: tidalId ?? '', - qobuzId: qobuzId ?? '', - deezerId: deezerId ?? '', - lyricsMode: lyricsMode, - storageMode: storageMode, - safTreeUri: safTreeUri, - safRelativeDir: safRelativeDir, - safFileName: safFileName, - safOutputExt: safOutputExt, - ); - return downloadByStrategy(payload: payload, useExtensions: true); - } static Future cleanupExtensions() async { _log.d('cleanupExtensions'); @@ -1332,72 +1131,4 @@ class PlatformBridge { // ==================== YOUTUBE / COBALT ==================== - /// Download a track from YouTube using the Cobalt API. - /// YouTube is a lossy-only provider (Opus 256kbps or MP3 320kbps). - /// It does NOT participate in the lossless fallback chain. - @Deprecated('Use downloadByStrategy with DownloadRequestPayload.') - static Future> downloadFromYouTube({ - required String trackName, - required String artistName, - required String albumName, - String? albumArtist, - String? coverUrl, - required String outputDir, - required String filenameFormat, - String quality = 'opus_256', - int trackNumber = 1, - int discNumber = 1, - String? releaseDate, - String? itemId, - int durationMs = 0, - String? isrc, - String? spotifyId, - String? deezerId, - bool embedLyrics = true, - bool embedMaxQualityCover = true, - int totalTracks = 1, - String? genre, - String? label, - String? copyright, - String lyricsMode = 'embed', - String storageMode = 'app', - String safTreeUri = '', - String safRelativeDir = '', - String safFileName = '', - String safOutputExt = '', - }) async { - final payload = DownloadRequestPayload( - isrc: isrc ?? '', - service: 'youtube', - spotifyId: spotifyId ?? '', - trackName: trackName, - artistName: artistName, - albumName: albumName, - albumArtist: albumArtist ?? artistName, - coverUrl: coverUrl ?? '', - outputDir: outputDir, - filenameFormat: filenameFormat, - quality: quality, - embedLyrics: embedLyrics, - embedMaxQualityCover: embedMaxQualityCover, - trackNumber: trackNumber, - discNumber: discNumber, - totalTracks: totalTracks, - releaseDate: releaseDate ?? '', - itemId: itemId ?? '', - durationMs: durationMs, - deezerId: deezerId ?? '', - genre: genre ?? '', - label: label ?? '', - copyright: copyright ?? '', - lyricsMode: lyricsMode, - storageMode: storageMode, - safTreeUri: safTreeUri, - safRelativeDir: safRelativeDir, - safFileName: safFileName, - safOutputExt: safOutputExt, - ); - - return downloadByStrategy(payload: payload); - } }