diff --git a/go_backend/exports_reenrich.go b/go_backend/exports_reenrich.go index 51784dbd..c297cce6 100644 --- a/go_backend/exports_reenrich.go +++ b/go_backend/exports_reenrich.go @@ -43,9 +43,9 @@ type reEnrichRequest struct { DurationMs int64 `json:"duration_ms"` SearchOnline bool `json:"search_online"` UpdateFields []string `json:"update_fields,omitempty"` - // ReplaceReleaseMetadata lets an explicit user re-enrich action repair a - // stale album identity (for example, a playlist name stored as ALBUM). - // Older callers keep the conservative mismatch-preservation behavior. + // ReplaceReleaseMetadata lets a deliberate single-file re-enrich action + // repair a stale album identity (for example, a playlist name stored as + // ALBUM). Batch and older callers keep the conservative mismatch guard. ReplaceReleaseMetadata bool `json:"replace_release_metadata,omitempty"` } diff --git a/lib/screens/local_album_screen.dart b/lib/screens/local_album_screen.dart index 218528d8..4d8cb257 100644 --- a/lib/screens/local_album_screen.dart +++ b/lib/screens/local_album_screen.dart @@ -14,6 +14,7 @@ import 'package:spotiflac_android/utils/file_access.dart'; import 'package:spotiflac_android/utils/image_cache_utils.dart'; import 'package:spotiflac_android/utils/lyrics_metadata_helper.dart'; import 'package:spotiflac_android/utils/nav_bar_inset.dart'; +import 'package:spotiflac_android/utils/re_enrich_release_policy.dart'; import 'package:spotiflac_android/services/library_database.dart'; import 'package:spotiflac_android/services/batch_track_actions.dart'; import 'package:spotiflac_android/models/unified_library_item.dart'; @@ -454,7 +455,9 @@ class _LocalAlbumScreenState extends ConsumerState 'copyright': '', 'duration_ms': durationMs, 'search_online': true, - 'replace_release_metadata': true, + 'replace_release_metadata': allowsReleaseIdentityReplacement( + ReEnrichOperationScope.batch, + ), // ignore: use_null_aware_elements if (updateFields != null) 'update_fields': updateFields, }; diff --git a/lib/screens/queue_tab.dart b/lib/screens/queue_tab.dart index 9fd1dc38..0348158b 100644 --- a/lib/screens/queue_tab.dart +++ b/lib/screens/queue_tab.dart @@ -10,6 +10,7 @@ import 'package:spotiflac_android/l10n/l10n.dart'; import 'package:spotiflac_android/utils/adaptive_layout.dart'; import 'package:spotiflac_android/utils/app_bar_layout.dart'; import 'package:spotiflac_android/utils/nav_bar_inset.dart'; +import 'package:spotiflac_android/utils/re_enrich_release_policy.dart'; import 'package:spotiflac_android/widgets/settings_group.dart'; import 'package:spotiflac_android/utils/ffmpeg_reenrich.dart'; import 'package:spotiflac_android/utils/file_access.dart'; diff --git a/lib/screens/queue_tab_batch_actions.dart b/lib/screens/queue_tab_batch_actions.dart index ee89630d..67cfd63d 100644 --- a/lib/screens/queue_tab_batch_actions.dart +++ b/lib/screens/queue_tab_batch_actions.dart @@ -30,7 +30,9 @@ extension _QueueTabBatchActions on _QueueTabState { 'copyright': '', 'duration_ms': durationMs, 'search_online': true, - 'replace_release_metadata': true, + 'replace_release_metadata': allowsReleaseIdentityReplacement( + ReEnrichOperationScope.batch, + ), // ignore: use_null_aware_elements if (updateFields != null) 'update_fields': updateFields, }; diff --git a/lib/screens/track_metadata_lyrics.dart b/lib/screens/track_metadata_lyrics.dart index fc698219..75553a5c 100644 --- a/lib/screens/track_metadata_lyrics.dart +++ b/lib/screens/track_metadata_lyrics.dart @@ -970,7 +970,9 @@ extension _TrackMetadataLyricsAndSaving on _TrackMetadataScreenState { 'composer': composer ?? '', 'duration_ms': durationMs, 'search_online': true, - 'replace_release_metadata': true, + 'replace_release_metadata': allowsReleaseIdentityReplacement( + ReEnrichOperationScope.singleFile, + ), }; final result = await PlatformBridge.reEnrichFile(request); diff --git a/lib/screens/track_metadata_screen.dart b/lib/screens/track_metadata_screen.dart index 4b1c1a1e..da8e36a1 100644 --- a/lib/screens/track_metadata_screen.dart +++ b/lib/screens/track_metadata_screen.dart @@ -30,6 +30,7 @@ import 'package:spotiflac_android/utils/image_cache_utils.dart'; import 'package:spotiflac_android/utils/string_utils.dart'; import 'package:spotiflac_android/utils/int_utils.dart'; import 'package:spotiflac_android/utils/nav_bar_inset.dart'; +import 'package:spotiflac_android/utils/re_enrich_release_policy.dart'; import 'package:spotiflac_android/widgets/album_detail_header.dart' show HeaderMetaRow, HeaderMetaItem; import 'package:spotiflac_android/widgets/audio_analysis_widget.dart'; diff --git a/lib/utils/re_enrich_release_policy.dart b/lib/utils/re_enrich_release_policy.dart new file mode 100644 index 00000000..fd7a12c7 --- /dev/null +++ b/lib/utils/re_enrich_release_policy.dart @@ -0,0 +1,8 @@ +enum ReEnrichOperationScope { singleFile, batch } + +/// A single-file action may deliberately repair a stale release identity, +/// such as a playlist name stored in the album tag. Batch matching keeps the +/// conservative album-mismatch guard because one false match has a much larger +/// blast radius. +bool allowsReleaseIdentityReplacement(ReEnrichOperationScope scope) => + scope == ReEnrichOperationScope.singleFile; diff --git a/test/re_enrich_release_policy_test.dart b/test/re_enrich_release_policy_test.dart new file mode 100644 index 00000000..e9ea2d92 --- /dev/null +++ b/test/re_enrich_release_policy_test.dart @@ -0,0 +1,18 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:spotiflac_android/utils/re_enrich_release_policy.dart'; + +void main() { + test( + 'only deliberate single-file re-enrich can replace release identity', + () { + expect( + allowsReleaseIdentityReplacement(ReEnrichOperationScope.singleFile), + isTrue, + ); + expect( + allowsReleaseIdentityReplacement(ReEnrichOperationScope.batch), + isFalse, + ); + }, + ); +}