fix(library): preserve cached covers across directory aliases

This commit is contained in:
zarzet
2026-09-15 21:44:36 +07:00
parent ae78519490
commit 347208b3d9
3 changed files with 75 additions and 17 deletions
+5 -17
View File
@@ -4,6 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:path_provider/path_provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:spotiflac_android/providers/settings_provider.dart';
import 'package:spotiflac_android/services/library_cleanup.dart';
import 'package:spotiflac_android/services/library_database.dart';
import 'package:spotiflac_android/services/notification_service.dart';
import 'package:spotiflac_android/services/platform_bridge.dart';
@@ -1203,23 +1204,10 @@ class LocalLibraryNotifier extends Notifier<LocalLibraryState> {
offset += pageSize;
}
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',
);
}
}
final deletedCount = await pruneUnreferencedLibraryCovers(
libraryCoverDir,
referencedCoverPaths,
);
if (deletedCount > 0) {
_log.i('Pruned $deletedCount stale library cover cache files');
+34
View File
@@ -1,5 +1,39 @@
import 'dart:io';
import 'package:sqflite/sqflite.dart';
import 'package:spotiflac_android/utils/file_access.dart';
import 'package:spotiflac_android/utils/logger.dart';
Future<int> pruneUnreferencedLibraryCovers(
Directory directory,
Set<String> referencedPaths,
) async {
// Native scans may return canonical paths while Dart lists a directory alias.
final retainedPaths = {...referencedPaths};
for (final path in referencedPaths) {
try {
retainedPaths.add(await File(path).resolveSymbolicLinks());
} on FileSystemException {
// Missing cache files can remain referenced until the next full scan.
}
}
var deleted = 0;
await for (final entity in directory.list(
recursive: true,
followLinks: false,
)) {
if (entity is! File || retainedPaths.contains(entity.path)) continue;
try {
if (retainedPaths.contains(await entity.resolveSymbolicLinks())) continue;
await entity.delete();
deleted++;
} catch (error) {
final message =
'Failed deleting stale library cover cache ${entity.path}: $error';
AppLogger('LocalLibrary').w(message);
}
}
return deleted;
}
/// Pages by stable ID (not OFFSET, since rows are removed during traversal).
/// A high-water mark bounds the run if a concurrent scan adds new rows.
+36
View File
@@ -102,6 +102,42 @@ void main() {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger;
tearDown(() => messenger.setMockMethodCallHandler(channel, null));
test(
'cover cleanup preserves references through directory aliases',
() async {
final root = await Directory.systemTemp.createTemp(
'library-cover-cleanup-',
);
addTearDown(() => root.delete(recursive: true));
final directory = await Directory('${root.path}/covers').create();
final alias = await Link('${root.path}/alias').create(directory.path);
final canonical = await File(
'${directory.path}/canonical.jpg',
).writeAsBytes([1]);
final legacy = await File(
'${directory.path}/legacy.jpg',
).writeAsBytes([2]);
final orphan = await File(
'${directory.path}/orphan.jpg',
).writeAsBytes([3]);
final outside = await File('${root.path}/outside.jpg').writeAsBytes([4]);
await Link('${directory.path}/outside.jpg').create(outside.path);
final deleted =
await pruneUnreferencedLibraryCovers(Directory(alias.path), {
await canonical.resolveSymbolicLinks(),
'${alias.path}/legacy.jpg',
'${alias.path}/already-missing.jpg',
});
expect(await canonical.exists(), isTrue);
expect(await legacy.exists(), isTrue);
expect(await orphan.exists(), isFalse);
expect(await outside.readAsBytes(), [4]);
expect(deleted, 1);
},
);
test(
'cleanup pages without skipping after deletions and preserves unknown',
() async {