feat: add stable cover cache keys, Qobuz album-search fallback, metadata filters and extended sort options

- Introduce coverCacheKey parameter through Go backend and Kotlin bridge for stable SAF cover caching
- Add MetadataFromFilename flag to skip filename-only metadata and retry via temp-file copy
- Add Qobuz album-search fallback between API search and store scraping
- Extract buildReEnrichFFmpegMetadata to skip empty metadata fields
- Add metadata completeness filter (complete, missing year/genre/album artist)
- Add sort modes: artist, album, release date, genre (asc/desc)
- Prune stale library cover cache files after full scan
- Skip empty values and zero track/disc numbers in FFmpeg metadata
- Add new l10n keys for metadata filter and sort options
This commit is contained in:
zarzet
2026-03-30 11:41:11 +07:00
parent fb90c73f42
commit fabaf0a3ff
28 changed files with 1784 additions and 81 deletions
+41
View File
@@ -339,6 +339,7 @@ class LocalLibraryNotifier extends Notifier<LocalLibraryState> {
scanWasCancelled: false,
excludedDownloadedCount: skippedDownloads,
);
await _pruneLibraryCoverCache(persistedItems);
_log.i(
'Full scan complete: ${persistedItems.length} tracks found, '
@@ -815,6 +816,46 @@ class LocalLibraryNotifier extends Notifier<LocalLibraryState> {
_log.i('Library cleared');
}
Future<void> _pruneLibraryCoverCache(Iterable<LocalLibraryItem> items) async {
try {
final appSupportDir = await getApplicationSupportDirectory();
final libraryCoverDir = Directory('${appSupportDir.path}/library_covers');
if (!await libraryCoverDir.exists()) {
return;
}
final referencedCoverPaths = items
.map((item) => item.coverPath)
.whereType<String>()
.where((path) => path.isNotEmpty)
.toSet();
var deletedCount = 0;
await for (final entity in libraryCoverDir.list(
recursive: true,
followLinks: false,
)) {
if (entity is! File || referencedCoverPaths.contains(entity.path)) {
continue;
}
try {
await entity.delete();
deletedCount++;
} catch (e) {
_log.w(
'Failed deleting stale library cover cache ${entity.path}: $e',
);
}
}
if (deletedCount > 0) {
_log.i('Pruned $deletedCount stale library cover cache files');
}
} catch (e) {
_log.w('Failed pruning library cover cache: $e');
}
}
Future<void> removeItem(String id) async {
await _db.delete(id);
state = state.copyWith(