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 bd364ad3..aa366741 100644 --- a/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt +++ b/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt @@ -3387,6 +3387,27 @@ class MainActivity: FlutterFragmentActivity() { } result.success(response) } + "fetchMusicBrainzTags" -> { + val isrc = call.argument("isrc") ?: "" + val albumName = call.argument("album_name") ?: "" + val response = withContext(Dispatchers.IO) { + val genre = try { + Gobackend.fetchMusicBrainzGenreByISRC(isrc) + } catch (_: Exception) { + "" + } + val albumArtist = try { + Gobackend.fetchMusicBrainzAlbumArtistByISRC(isrc, albumName) + } catch (_: Exception) { + "" + } + JSONObject() + .put("genre", genre) + .put("album_artist", albumArtist) + .toString() + } + result.success(response) + } "runPostProcessingV2" -> { val inputJson = call.argument("input") ?: "" val metadataJson = call.argument("metadata") ?: "" diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index 9edd338e..24d7ee94 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -959,6 +959,21 @@ import Gobackend let response = GobackendGetTrackPlatformLinksJSON(spotifyId, isrc, &error) if let error = error { throw error } return response + + case "fetchMusicBrainzTags": + let args = call.arguments as! [String: Any] + let isrc = args["isrc"] as? String ?? "" + let albumName = args["album_name"] as? String ?? "" + var genreError: NSError? + let genre = GobackendFetchMusicBrainzGenreByISRC(isrc, &genreError) + var artistError: NSError? + let albumArtist = GobackendFetchMusicBrainzAlbumArtistByISRC(isrc, albumName, &artistError) + let payload: [String: Any] = [ + "genre": genreError == nil ? genre : "", + "album_artist": artistError == nil ? albumArtist : "", + ] + let data = try JSONSerialization.data(withJSONObject: payload) + return String(data: data, encoding: .utf8) ?? "{}" case "runPostProcessingV2": let args = call.arguments as! [String: Any] diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index c1acf0f4..7a648008 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -6294,6 +6294,30 @@ abstract class AppLocalizations { /// **'Download next'** String get queueDownloadNext; + /// Tag editor button that fills genre and album artist from MusicBrainz by ISRC + /// + /// In en, this message translates to: + /// **'Fetch from MusicBrainz'** + String get editMetadataMusicBrainzButton; + + /// Snackbar after MusicBrainz suggestions were applied to the tag editor fields + /// + /// In en, this message translates to: + /// **'Updated from MusicBrainz'** + String get editMetadataMusicBrainzFilled; + + /// Snackbar when the MusicBrainz lookup returns no data + /// + /// In en, this message translates to: + /// **'Nothing found on MusicBrainz'** + String get editMetadataMusicBrainzNothing; + + /// Snackbar when the MusicBrainz lookup is tapped without an ISRC + /// + /// In en, this message translates to: + /// **'Requires an ISRC tag'** + String get editMetadataMusicBrainzNeedsIsrc; + /// Repeat toggle tooltip when repeat is disabled /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 4c1de5c9..0adaed3b 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -3834,6 +3834,18 @@ class AppLocalizationsDe extends AppLocalizations { @override String get queueDownloadNext => 'Download next'; + @override + String get editMetadataMusicBrainzButton => 'Fetch from MusicBrainz'; + + @override + String get editMetadataMusicBrainzFilled => 'Updated from MusicBrainz'; + + @override + String get editMetadataMusicBrainzNothing => 'Nothing found on MusicBrainz'; + + @override + String get editMetadataMusicBrainzNeedsIsrc => 'Requires an ISRC tag'; + @override String get nowPlayingRepeatOff => 'Repeat off'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 2c1873e5..2964a5a3 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -3788,6 +3788,18 @@ class AppLocalizationsEn extends AppLocalizations { @override String get queueDownloadNext => 'Download next'; + @override + String get editMetadataMusicBrainzButton => 'Fetch from MusicBrainz'; + + @override + String get editMetadataMusicBrainzFilled => 'Updated from MusicBrainz'; + + @override + String get editMetadataMusicBrainzNothing => 'Nothing found on MusicBrainz'; + + @override + String get editMetadataMusicBrainzNeedsIsrc => 'Requires an ISRC tag'; + @override String get nowPlayingRepeatOff => 'Repeat off'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 06d4377a..a2cf80a3 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -3782,6 +3782,18 @@ class AppLocalizationsEs extends AppLocalizations { @override String get queueDownloadNext => 'Download next'; + @override + String get editMetadataMusicBrainzButton => 'Fetch from MusicBrainz'; + + @override + String get editMetadataMusicBrainzFilled => 'Updated from MusicBrainz'; + + @override + String get editMetadataMusicBrainzNothing => 'Nothing found on MusicBrainz'; + + @override + String get editMetadataMusicBrainzNeedsIsrc => 'Requires an ISRC tag'; + @override String get nowPlayingRepeatOff => 'Repeat off'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 32a71ca9..f1d93a1e 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -3889,6 +3889,18 @@ class AppLocalizationsFr extends AppLocalizations { @override String get queueDownloadNext => 'Download next'; + @override + String get editMetadataMusicBrainzButton => 'Fetch from MusicBrainz'; + + @override + String get editMetadataMusicBrainzFilled => 'Updated from MusicBrainz'; + + @override + String get editMetadataMusicBrainzNothing => 'Nothing found on MusicBrainz'; + + @override + String get editMetadataMusicBrainzNeedsIsrc => 'Requires an ISRC tag'; + @override String get nowPlayingRepeatOff => 'Repeat off'; diff --git a/lib/l10n/app_localizations_id.dart b/lib/l10n/app_localizations_id.dart index 1b64bbe2..0681ee79 100644 --- a/lib/l10n/app_localizations_id.dart +++ b/lib/l10n/app_localizations_id.dart @@ -3791,6 +3791,18 @@ class AppLocalizationsId extends AppLocalizations { @override String get queueDownloadNext => 'Download next'; + @override + String get editMetadataMusicBrainzButton => 'Fetch from MusicBrainz'; + + @override + String get editMetadataMusicBrainzFilled => 'Updated from MusicBrainz'; + + @override + String get editMetadataMusicBrainzNothing => 'Nothing found on MusicBrainz'; + + @override + String get editMetadataMusicBrainzNeedsIsrc => 'Requires an ISRC tag'; + @override String get nowPlayingRepeatOff => 'Repeat off'; diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index d5949119..797b2f5c 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -3777,6 +3777,18 @@ class AppLocalizationsJa extends AppLocalizations { @override String get queueDownloadNext => 'Download next'; + @override + String get editMetadataMusicBrainzButton => 'Fetch from MusicBrainz'; + + @override + String get editMetadataMusicBrainzFilled => 'Updated from MusicBrainz'; + + @override + String get editMetadataMusicBrainzNothing => 'Nothing found on MusicBrainz'; + + @override + String get editMetadataMusicBrainzNeedsIsrc => 'Requires an ISRC tag'; + @override String get nowPlayingRepeatOff => 'Repeat off'; diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index 82967688..e8df0dde 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -3678,6 +3678,18 @@ class AppLocalizationsKo extends AppLocalizations { @override String get queueDownloadNext => 'Download next'; + @override + String get editMetadataMusicBrainzButton => 'Fetch from MusicBrainz'; + + @override + String get editMetadataMusicBrainzFilled => 'Updated from MusicBrainz'; + + @override + String get editMetadataMusicBrainzNothing => 'Nothing found on MusicBrainz'; + + @override + String get editMetadataMusicBrainzNeedsIsrc => 'Requires an ISRC tag'; + @override String get nowPlayingRepeatOff => 'Repeat off'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 57ce9c02..7bf39e1c 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -3782,6 +3782,18 @@ class AppLocalizationsPt extends AppLocalizations { @override String get queueDownloadNext => 'Download next'; + @override + String get editMetadataMusicBrainzButton => 'Fetch from MusicBrainz'; + + @override + String get editMetadataMusicBrainzFilled => 'Updated from MusicBrainz'; + + @override + String get editMetadataMusicBrainzNothing => 'Nothing found on MusicBrainz'; + + @override + String get editMetadataMusicBrainzNeedsIsrc => 'Requires an ISRC tag'; + @override String get nowPlayingRepeatOff => 'Repeat off'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 987a0540..3da28565 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -3819,6 +3819,18 @@ class AppLocalizationsRu extends AppLocalizations { @override String get queueDownloadNext => 'Download next'; + @override + String get editMetadataMusicBrainzButton => 'Fetch from MusicBrainz'; + + @override + String get editMetadataMusicBrainzFilled => 'Updated from MusicBrainz'; + + @override + String get editMetadataMusicBrainzNothing => 'Nothing found on MusicBrainz'; + + @override + String get editMetadataMusicBrainzNeedsIsrc => 'Requires an ISRC tag'; + @override String get nowPlayingRepeatOff => 'Repeat off'; diff --git a/lib/l10n/app_localizations_tr.dart b/lib/l10n/app_localizations_tr.dart index 0cdf30c4..ff987f33 100644 --- a/lib/l10n/app_localizations_tr.dart +++ b/lib/l10n/app_localizations_tr.dart @@ -3818,6 +3818,18 @@ class AppLocalizationsTr extends AppLocalizations { @override String get queueDownloadNext => 'Download next'; + @override + String get editMetadataMusicBrainzButton => 'Fetch from MusicBrainz'; + + @override + String get editMetadataMusicBrainzFilled => 'Updated from MusicBrainz'; + + @override + String get editMetadataMusicBrainzNothing => 'Nothing found on MusicBrainz'; + + @override + String get editMetadataMusicBrainzNeedsIsrc => 'Requires an ISRC tag'; + @override String get nowPlayingRepeatOff => 'Repeat off'; diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index b156cf93..02206011 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -3836,6 +3836,18 @@ class AppLocalizationsUk extends AppLocalizations { @override String get queueDownloadNext => 'Download next'; + @override + String get editMetadataMusicBrainzButton => 'Fetch from MusicBrainz'; + + @override + String get editMetadataMusicBrainzFilled => 'Updated from MusicBrainz'; + + @override + String get editMetadataMusicBrainzNothing => 'Nothing found on MusicBrainz'; + + @override + String get editMetadataMusicBrainzNeedsIsrc => 'Requires an ISRC tag'; + @override String get nowPlayingRepeatOff => 'Repeat off'; diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 6aced480..4dd0af05 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -4922,6 +4922,22 @@ "@queueDownloadNext": { "description": "Tooltip on a queued download row; moves the item to the front of the queue so the next free slot downloads it" }, + "editMetadataMusicBrainzButton": "Fetch from MusicBrainz", + "@editMetadataMusicBrainzButton": { + "description": "Tag editor button that fills genre and album artist from MusicBrainz by ISRC" + }, + "editMetadataMusicBrainzFilled": "Updated from MusicBrainz", + "@editMetadataMusicBrainzFilled": { + "description": "Snackbar after MusicBrainz suggestions were applied to the tag editor fields" + }, + "editMetadataMusicBrainzNothing": "Nothing found on MusicBrainz", + "@editMetadataMusicBrainzNothing": { + "description": "Snackbar when the MusicBrainz lookup returns no data" + }, + "editMetadataMusicBrainzNeedsIsrc": "Requires an ISRC tag", + "@editMetadataMusicBrainzNeedsIsrc": { + "description": "Snackbar when the MusicBrainz lookup is tapped without an ISRC" + }, "nowPlayingRepeatOff": "Repeat off", "@nowPlayingRepeatOff": { "description": "Repeat toggle tooltip when repeat is disabled" diff --git a/lib/screens/track_metadata_edit_sheet.dart b/lib/screens/track_metadata_edit_sheet.dart index b88a656a..0862fa80 100644 --- a/lib/screens/track_metadata_edit_sheet.dart +++ b/lib/screens/track_metadata_edit_sheet.dart @@ -84,6 +84,7 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> { late final TextEditingController _copyrightCtrl; late final TextEditingController _composerCtrl; late final TextEditingController _commentCtrl; + bool _fetchingMusicBrainz = false; bool _hasValue(String? value) => value != null && value.trim().isNotEmpty; @@ -1518,11 +1519,82 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> { ), ), const SizedBox(height: 8), + SizedBox( + width: double.infinity, + child: OutlinedButton.icon( + onPressed: (_fetching || _fetchingMusicBrainz || _saving) + ? null + : _fetchFromMusicBrainz, + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + icon: _fetchingMusicBrainz + ? const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Icon(Icons.library_music_outlined), + label: Text(context.l10n.editMetadataMusicBrainzButton), + ), + ), + const SizedBox(height: 8), ], ], ); } + /// Fills genre and album artist from MusicBrainz (keyed by ISRC) as + /// editable suggestions; nothing is saved until the user taps Save. + Future _fetchFromMusicBrainz() async { + final isrc = _isrcCtrl.text.trim(); + if (isrc.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(context.l10n.editMetadataMusicBrainzNeedsIsrc)), + ); + return; + } + setState(() => _fetchingMusicBrainz = true); + try { + final tags = await PlatformBridge.fetchMusicBrainzTags( + isrc: isrc, + albumName: _albumCtrl.text.trim(), + ); + if (!mounted) return; + var filled = 0; + if (tags.genre.isNotEmpty) { + _genreCtrl.text = tags.genre; + filled++; + } + if (tags.albumArtist.isNotEmpty) { + _albumArtistCtrl.text = tags.albumArtist; + filled++; + } + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + filled > 0 + ? context.l10n.editMetadataMusicBrainzFilled + : context.l10n.editMetadataMusicBrainzNothing, + ), + ), + ); + } catch (_) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(context.l10n.editMetadataMusicBrainzNothing), + ), + ); + } + } finally { + if (mounted) setState(() => _fetchingMusicBrainz = false); + } + } + Widget _quickSelectButton({ required String label, required VoidCallback onTap, diff --git a/lib/services/platform_bridge.dart b/lib/services/platform_bridge.dart index 9719abc6..a4289adb 100644 --- a/lib/services/platform_bridge.dart +++ b/lib/services/platform_bridge.dart @@ -1645,6 +1645,24 @@ class PlatformBridge { return result as String; } + /// MusicBrainz genre and album artist for an ISRC; empty strings when a + /// lookup finds nothing. One channel call covers both fields to spare + /// MusicBrainz's 1 req/s budget. + static Future<({String genre, String albumArtist})> fetchMusicBrainzTags({ + required String isrc, + String albumName = '', + }) async { + final result = await _channel.invokeMethod('fetchMusicBrainzTags', { + 'isrc': isrc, + 'album_name': albumName, + }); + final decoded = _decodeMapResult(result); + return ( + genre: decoded['genre']?.toString() ?? '', + albumArtist: decoded['album_artist']?.toString() ?? '', + ); + } + /// Streaming-platform links for a track, keyed by song.link platform ID. /// Served from the Go-side memory cache; throws when the lookup fails. static Future> getTrackPlatformLinks({