perf(net): fetch embed covers once per release instead of per track

This commit is contained in:
zarzet
2026-07-14 09:10:01 +07:00
parent 3cdeeb2f5f
commit 547e6493dd
2 changed files with 72 additions and 37 deletions
@@ -170,6 +170,10 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
static const _decryptStageSafWrite = 'safWrite';
final NotificationService _notificationService = NotificationService();
final AppStateDatabase _appStateDb = AppStateDatabase.instance;
// Shared across tracks in a batch: an album's tracks embed the same cover,
// so fetch it once instead of once per track. LRU-capped; files are deleted
// on eviction and when the queue drains.
final Map<String, Future<String?>> _embedCoverCache = {};
late final ProgressStreamPoller<Map<String, dynamic>> _progressPoller =
ProgressStreamPoller<Map<String, dynamic>>(
streamProvider: PlatformBridge.downloadProgressStream,
@@ -2121,6 +2125,7 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
stoppedWhilePaused && _networkPausedByWifiOnly;
_stopProgressPolling();
_clearEmbedCoverCache();
if (!keepConnectivityMonitoring) {
_stopConnectivityMonitoring();
}
@@ -611,37 +611,10 @@ extension _DownloadQueueEmbedding on DownloadQueueNotifier {
String? coverPath;
var coverUrl = normalizeRemoteHttpUrl(track.coverUrl);
if (coverUrl != null && coverUrl.isNotEmpty) {
try {
if (settings.maxQualityCover) {
coverUrl = _upgradeToMaxQualityCover(coverUrl);
_log.d('Cover URL upgraded to max quality for $format: $coverUrl');
}
final tempDir = await getTemporaryDirectory();
final uniqueId =
'${DateTime.now().millisecondsSinceEpoch}_${Random().nextInt(10000)}';
coverPath = '${tempDir.path}/cover_${format}_$uniqueId.jpg';
final httpClient = HttpClient();
final request = await httpClient.getUrl(Uri.parse(coverUrl));
final response = await request.close();
if (response.statusCode == 200) {
final file = File(coverPath);
final sink = file.openWrite();
await response.pipe(sink);
await sink.close();
_log.d('Cover downloaded for $format: $coverPath');
} else {
_log.w(
'Failed to download cover for $format: HTTP ${response.statusCode}',
);
coverPath = null;
}
httpClient.close();
} catch (e) {
_log.e('Failed to download cover for $format: $e');
coverPath = null;
if (settings.maxQualityCover) {
coverUrl = _upgradeToMaxQualityCover(coverUrl);
}
coverPath = await _sharedEmbedCover(coverUrl);
}
try {
@@ -898,15 +871,72 @@ extension _DownloadQueueEmbedding on DownloadQueueNotifier {
}
} catch (e) {
_log.e('Failed to embed metadata to $format: $e');
} finally {
if (coverPath != null) {
try {
final coverFile = File(coverPath);
if (await coverFile.exists()) await coverFile.delete();
} catch (e) {
_log.w('Failed to cleanup $format cover file: $e');
}
// coverPath is owned by _embedCoverCache: kept for the next track of the
// same release, deleted on LRU eviction or when the queue drains.
}
static const _embedCoverCacheMax = 8;
/// One cover fetch per URL, shared by every track in the batch.
Future<String?> _sharedEmbedCover(String coverUrl) {
final existing = _embedCoverCache.remove(coverUrl);
if (existing != null) {
_embedCoverCache[coverUrl] = existing; // LRU touch
return existing;
}
final fetch = _downloadEmbedCover(coverUrl).then((path) {
if (path == null) _embedCoverCache.remove(coverUrl); // allow retry
return path;
});
_embedCoverCache[coverUrl] = fetch;
while (_embedCoverCache.length > _embedCoverCacheMax) {
_evictEmbedCover(_embedCoverCache.keys.first);
}
return fetch;
}
Future<String?> _downloadEmbedCover(String coverUrl) async {
try {
final tempDir = await getTemporaryDirectory();
final uniqueId =
'${DateTime.now().millisecondsSinceEpoch}_${Random().nextInt(10000)}';
final coverPath = '${tempDir.path}/cover_embed_$uniqueId.jpg';
final httpClient = HttpClient();
try {
final request = await httpClient.getUrl(Uri.parse(coverUrl));
final response = await request.close();
if (response.statusCode != 200) {
_log.w('Failed to download cover: HTTP ${response.statusCode}');
return null;
}
final sink = File(coverPath).openWrite();
await response.pipe(sink);
await sink.close();
_log.d('Cover downloaded for embedding: $coverPath');
return coverPath;
} finally {
httpClient.close();
}
} catch (e) {
_log.e('Failed to download cover for embedding: $e');
return null;
}
}
void _evictEmbedCover(String coverUrl) {
final pending = _embedCoverCache.remove(coverUrl);
pending?.then((path) {
if (path != null) {
File(path).delete().catchError((_) => File(path));
}
});
}
void _clearEmbedCoverCache() {
for (final url in _embedCoverCache.keys.toList()) {
_evictEmbedCover(url);
}
}
}