mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-08-16 08:00:23 +02:00
perf: parallel I/O, caching, and chunked DB operations (batch 3)
- Orphan cleanup: parallel file existence checks (chunk 16) - LocalLibraryState: O(1) findByTrackAndArtist via _byTrackKey map - Local library load: parallel DB + SharedPreferences fetch - Legacy mod-time backfill: chunked parallel File.stat (chunk 24) - Downloaded album screen: cache disc groups, quality, cover path - Local album screen: cache common quality, map-based batch delete - Cache management: parallel async init, chunked directory cleanup - Cover resolver: throttled preview exists check (2.2s interval) - History/Library DB: chunked SQL DELETE (500 per batch) - Batch delete screens: O(1) item lookup via tracksById map
This commit is contained in:
@@ -21,6 +21,7 @@ class _EmbeddedCoverCacheEntry {
|
||||
class DownloadedEmbeddedCoverResolver {
|
||||
static const int _maxCacheEntries = 160;
|
||||
static const int _minModCheckIntervalMs = 1200;
|
||||
static const int _minPreviewExistsCheckIntervalMs = 2200;
|
||||
|
||||
static final LinkedHashMap<String, _EmbeddedCoverCacheEntry> _cache =
|
||||
LinkedHashMap<String, _EmbeddedCoverCacheEntry>();
|
||||
@@ -28,6 +29,8 @@ class DownloadedEmbeddedCoverResolver {
|
||||
static final Set<String> _pendingModCheck = <String>{};
|
||||
static final Set<String> _failedExtract = <String>{};
|
||||
static final Map<String, int> _lastModCheckMillis = <String, int>{};
|
||||
static final Map<String, int> _lastPreviewExistsCheckMillis =
|
||||
<String, int>{};
|
||||
|
||||
static String cleanFilePath(String? filePath) {
|
||||
if (filePath == null) return '';
|
||||
@@ -64,12 +67,21 @@ class DownloadedEmbeddedCoverResolver {
|
||||
|
||||
final cached = _cache[cleanPath];
|
||||
if (cached != null) {
|
||||
if (File(cached.previewPath).existsSync()) {
|
||||
final now = DateTime.now().millisecondsSinceEpoch;
|
||||
final lastPreviewCheck = _lastPreviewExistsCheckMillis[cleanPath] ?? 0;
|
||||
final shouldVerifyExists =
|
||||
now - lastPreviewCheck >= _minPreviewExistsCheckIntervalMs;
|
||||
|
||||
if (!shouldVerifyExists || File(cached.previewPath).existsSync()) {
|
||||
if (shouldVerifyExists) {
|
||||
_lastPreviewExistsCheckMillis[cleanPath] = now;
|
||||
}
|
||||
_touch(cleanPath, cached);
|
||||
_scheduleModCheck(cleanPath, onChanged: onChanged);
|
||||
return cached.previewPath;
|
||||
}
|
||||
_cache.remove(cleanPath);
|
||||
_lastPreviewExistsCheckMillis.remove(cleanPath);
|
||||
_cleanupTempCoverPathSync(cached.previewPath);
|
||||
}
|
||||
|
||||
@@ -107,6 +119,7 @@ class DownloadedEmbeddedCoverResolver {
|
||||
_pendingModCheck.remove(cleanPath);
|
||||
_failedExtract.remove(cleanPath);
|
||||
_lastModCheckMillis.remove(cleanPath);
|
||||
_lastPreviewExistsCheckMillis.remove(cleanPath);
|
||||
if (cached != null) {
|
||||
_cleanupTempCoverPathSync(cached.previewPath);
|
||||
}
|
||||
@@ -129,6 +142,7 @@ class DownloadedEmbeddedCoverResolver {
|
||||
_pendingModCheck.remove(oldestKey);
|
||||
_failedExtract.remove(oldestKey);
|
||||
_lastModCheckMillis.remove(oldestKey);
|
||||
_lastPreviewExistsCheckMillis.remove(oldestKey);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,6 +218,8 @@ class DownloadedEmbeddedCoverResolver {
|
||||
);
|
||||
_touch(cleanPath, next);
|
||||
_failedExtract.remove(cleanPath);
|
||||
_lastPreviewExistsCheckMillis[cleanPath] =
|
||||
DateTime.now().millisecondsSinceEpoch;
|
||||
_trimCacheIfNeeded();
|
||||
|
||||
if (previous != null && previous.previewPath != outputPath) {
|
||||
|
||||
@@ -525,12 +525,18 @@ class HistoryDatabase {
|
||||
if (ids.isEmpty) return 0;
|
||||
|
||||
final db = await database;
|
||||
final placeholders = List.filled(ids.length, '?').join(',');
|
||||
final count = await db.rawDelete(
|
||||
'DELETE FROM history WHERE id IN ($placeholders)',
|
||||
ids,
|
||||
);
|
||||
_log.i('Deleted $count orphaned entries');
|
||||
return count;
|
||||
var totalDeleted = 0;
|
||||
const chunkSize = 500;
|
||||
for (var i = 0; i < ids.length; i += chunkSize) {
|
||||
final end = (i + chunkSize < ids.length) ? i + chunkSize : ids.length;
|
||||
final chunk = ids.sublist(i, end);
|
||||
final placeholders = List.filled(chunk.length, '?').join(',');
|
||||
totalDeleted += await db.rawDelete(
|
||||
'DELETE FROM history WHERE id IN ($placeholders)',
|
||||
chunk,
|
||||
);
|
||||
}
|
||||
_log.i('Deleted $totalDeleted orphaned entries');
|
||||
return totalDeleted;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,6 +229,7 @@ class LibraryDatabase {
|
||||
}
|
||||
|
||||
Future<void> upsertBatch(List<Map<String, dynamic>> items) async {
|
||||
if (items.isEmpty) return;
|
||||
final db = await database;
|
||||
final batch = db.batch();
|
||||
|
||||
@@ -350,16 +351,46 @@ class LibraryDatabase {
|
||||
Future<int> cleanupMissingFiles() async {
|
||||
final db = await database;
|
||||
final rows = await db.query('library', columns: ['id', 'file_path']);
|
||||
|
||||
int removed = 0;
|
||||
for (final row in rows) {
|
||||
final filePath = row['file_path'] as String;
|
||||
if (!await fileExists(filePath)) {
|
||||
await db.delete('library', where: 'id = ?', whereArgs: [row['id']]);
|
||||
removed++;
|
||||
|
||||
final missingIds = <String>[];
|
||||
const checkChunkSize = 16;
|
||||
for (var i = 0; i < rows.length; i += checkChunkSize) {
|
||||
final end = (i + checkChunkSize < rows.length)
|
||||
? i + checkChunkSize
|
||||
: rows.length;
|
||||
final chunk = rows.sublist(i, end);
|
||||
final checks = await Future.wait<MapEntry<String, bool>>(
|
||||
chunk.map((row) async {
|
||||
final id = row['id'] as String;
|
||||
final filePath = row['file_path'] as String;
|
||||
return MapEntry(id, await fileExists(filePath));
|
||||
}),
|
||||
);
|
||||
for (final check in checks) {
|
||||
if (!check.value) {
|
||||
missingIds.add(check.key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (missingIds.isEmpty) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
var removed = 0;
|
||||
const deleteChunkSize = 500;
|
||||
for (var i = 0; i < missingIds.length; i += deleteChunkSize) {
|
||||
final end = (i + deleteChunkSize < missingIds.length)
|
||||
? i + deleteChunkSize
|
||||
: missingIds.length;
|
||||
final idChunk = missingIds.sublist(i, end);
|
||||
final placeholders = List.filled(idChunk.length, '?').join(',');
|
||||
removed += await db.rawDelete(
|
||||
'DELETE FROM library WHERE id IN ($placeholders)',
|
||||
idChunk,
|
||||
);
|
||||
}
|
||||
|
||||
if (removed > 0) {
|
||||
_log.i('Cleaned up $removed missing files from library');
|
||||
}
|
||||
@@ -440,14 +471,22 @@ class LibraryDatabase {
|
||||
Future<int> deleteByPaths(List<String> filePaths) async {
|
||||
if (filePaths.isEmpty) return 0;
|
||||
final db = await database;
|
||||
final placeholders = List.filled(filePaths.length, '?').join(',');
|
||||
final result = await db.rawDelete(
|
||||
'DELETE FROM library WHERE file_path IN ($placeholders)',
|
||||
filePaths,
|
||||
);
|
||||
if (result > 0) {
|
||||
_log.i('Deleted $result items from library');
|
||||
var totalDeleted = 0;
|
||||
const chunkSize = 500;
|
||||
for (var i = 0; i < filePaths.length; i += chunkSize) {
|
||||
final end = (i + chunkSize < filePaths.length)
|
||||
? i + chunkSize
|
||||
: filePaths.length;
|
||||
final chunk = filePaths.sublist(i, end);
|
||||
final placeholders = List.filled(chunk.length, '?').join(',');
|
||||
totalDeleted += await db.rawDelete(
|
||||
'DELETE FROM library WHERE file_path IN ($placeholders)',
|
||||
chunk,
|
||||
);
|
||||
}
|
||||
return result;
|
||||
if (totalDeleted > 0) {
|
||||
_log.i('Deleted $totalDeleted items from library');
|
||||
}
|
||||
return totalDeleted;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user