fix(metadata): recognize featured artist credits consistently

Handle feature and featured separators across display, Android finalization and Rust tag writing. Split conjunction credits only when separate artist IDs confirm them, and offer a picker for multi-artist navigation.
This commit is contained in:
zarzet committed 2026-10-02 18:10:00 +07:00
1 parent 9ea2647c39
commit 4eef5542be
6 files changed
+105 -10

No files matched your search

+17 -4
View File
@@ -1,5 +1,5 @@
final RegExp _artistNameSplitPattern = RegExp(
r'\s*(?:,|;|&|\bx\b)\s*|\s+\b(?:feat(?:uring)?|ft|with)\.?(?=\s|$)\s*',
r'\s*(?:,|;|&|\bx\b)\s*|\s+\b(?:feat(?:uring|ured|ure)?|ft|with)\.?(?=\s|$)\s*',
caseSensitive: false,
);
@@ -17,20 +17,33 @@ const artistTagModes = {
// every write path stores the same primary artist. Unlike the display split
// above, a standalone "x" needs spaces on both sides ("Malcolm X" stays).
final RegExp _primaryArtistSeparator = RegExp(
r'\s*[,;&]\s*|\s+x\s+|\s+(?:feat(?:uring)?|ft|with)\.?(?:\s+|$)',
r'\s*[,;&]\s*|\s+x\s+|\s+(?:feat(?:uring|ured|ure)?|ft|with)\.?(?:\s+|$)',
caseSensitive: false,
);
final RegExp _metadataKeyPunctuation = RegExp(r'[^A-Z0-9]');
List<String> splitArtistNames(String rawArtists) {
List<String> splitArtistNames(String rawArtists, {int? creditedArtistCount}) {
final raw = rawArtists.trim();
if (raw.isEmpty) return const [];
return raw
final names = raw
.split(_artistNameSplitPattern)
.map((part) => part.trim())
.where((part) => part.isNotEmpty)
.toList(growable: false);
// "and" can be part of a band's name. Only treat it as a separator when
// the provider's separate artist IDs confirm that many credited artists.
if (creditedArtistCount != null && creditedArtistCount > names.length) {
final expanded = names
.expand(
(name) => name.split(RegExp(r'\s+and\s+', caseSensitive: false)),
)
.map((name) => name.trim())
.where((name) => name.isNotEmpty)
.toList(growable: false);
if (expanded.length == creditedArtistCount) return expanded;
}
return names;
}
String primaryArtistName(String artists, {String? albumArtist}) {
+42 -2
View File
@@ -13,6 +13,7 @@ import 'package:spotiflac_android/screens/home_tab.dart'
import 'package:spotiflac_android/services/shell_navigation_service.dart';
import 'package:spotiflac_android/utils/artist_utils.dart';
import 'package:spotiflac_android/utils/logger.dart';
import 'package:spotiflac_android/widgets/app_bottom_sheet.dart';
final _log = AppLogger('ClickableMetadata');
@@ -126,6 +127,42 @@ bool _canSearchMetadataProvider(
);
}
Future<void> navigateToArtistCredits(
BuildContext context, {
required String artistNames,
String? artistIds,
String? extensionId,
}) async {
final artists = _buildArtistTapTargets(artistNames, artistIds);
if (artists.isEmpty) return;
final selected = artists.length == 1
? artists.single
: await showAppBottomSheet<_ArtistTapTarget>(
context: context,
useRootNavigator: true,
title: context.l10n.mornyeGoToArtist,
maxHeightFactor: 0.6,
builder: (context) => ListView(
shrinkWrap: true,
children: [
for (final artist in artists)
AppSheetOption(
leading: const Icon(Icons.person_outline),
title: Text(artist.name),
onTap: () => Navigator.pop(context, artist),
),
],
),
);
if (!context.mounted || selected == null) return;
await navigateToArtist(
context,
artistName: selected.name,
artistId: selected.artistId,
extensionId: extensionId,
);
}
Future<void> navigateToArtist(
BuildContext context, {
required String artistName,
@@ -636,7 +673,11 @@ List<_ArtistTapTarget> _buildArtistTapTargets(
String rawArtistNames,
String? rawArtistIds,
) {
final parsedNames = splitArtistNames(rawArtistNames);
final parsedIds = _parseArtistIds(rawArtistIds);
final parsedNames = splitArtistNames(
rawArtistNames,
creditedArtistCount: parsedIds.length,
);
if (parsedNames.isEmpty) return const [];
final uniqueNames = <String>[];
@@ -657,7 +698,6 @@ List<_ArtistTapTarget> _buildArtistTapTargets(
];
}
final parsedIds = _parseArtistIds(rawArtistIds);
if (parsedIds.isEmpty || !parsedIds.any((id) => id != null)) {
return uniqueNames
.map((name) => _ArtistTapTarget(name: name))