mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-08-02 09:08:35 +02:00
feat(metadata): manual MusicBrainz lookup in the tag editor
The Go MusicBrainz genre and album-artist ISRC lookups only ran automatically at download time. The tag editor's auto-fill section gains a Fetch from MusicBrainz button that bridges both exports in one channel call (sparing the 1 req/s budget) and fills the fields as editable suggestions; nothing is written until Save.
This commit is contained in:
@@ -3387,6 +3387,27 @@ class MainActivity: FlutterFragmentActivity() {
|
||||
}
|
||||
result.success(response)
|
||||
}
|
||||
"fetchMusicBrainzTags" -> {
|
||||
val isrc = call.argument<String>("isrc") ?: ""
|
||||
val albumName = call.argument<String>("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<String>("input") ?: ""
|
||||
val metadataJson = call.argument<String>("metadata") ?: ""
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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<void> _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,
|
||||
|
||||
@@ -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<Map<String, String>> getTrackPlatformLinks({
|
||||
|
||||
Reference in New Issue
Block a user