From 547e6493dd8b35044bb552d776e8fe2fb6e3e9df Mon Sep 17 00:00:00 2001 From: zarzet Date: Tue, 14 Jul 2026 07:13:09 +0700 Subject: [PATCH] perf(net): fetch embed covers once per release instead of per track --- lib/providers/download_queue_provider.dart | 5 + .../download_queue_provider_embedding.dart | 104 +++++++++++------- 2 files changed, 72 insertions(+), 37 deletions(-) diff --git a/lib/providers/download_queue_provider.dart b/lib/providers/download_queue_provider.dart index 00220d79..20b8920e 100644 --- a/lib/providers/download_queue_provider.dart +++ b/lib/providers/download_queue_provider.dart @@ -170,6 +170,10 @@ class DownloadQueueNotifier extends Notifier { 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> _embedCoverCache = {}; late final ProgressStreamPoller> _progressPoller = ProgressStreamPoller>( streamProvider: PlatformBridge.downloadProgressStream, @@ -2121,6 +2125,7 @@ class DownloadQueueNotifier extends Notifier { stoppedWhilePaused && _networkPausedByWifiOnly; _stopProgressPolling(); + _clearEmbedCoverCache(); if (!keepConnectivityMonitoring) { _stopConnectivityMonitoring(); } diff --git a/lib/providers/download_queue_provider_embedding.dart b/lib/providers/download_queue_provider_embedding.dart index 0d2483b7..e5d70e89 100644 --- a/lib/providers/download_queue_provider_embedding.dart +++ b/lib/providers/download_queue_provider_embedding.dart @@ -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 _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 _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); } } }