diff --git a/lib/providers/download_history_provider.dart b/lib/providers/download_history_provider.dart index b4fc3136..2687d911 100644 --- a/lib/providers/download_history_provider.dart +++ b/lib/providers/download_history_provider.dart @@ -622,10 +622,7 @@ class DownloadHistoryNotifier extends Notifier { '.mp4', ]; - Future _findConvertedSibling( - String originalPath, { - bool includeAlternateExtensions = true, - }) async { + Future _findConvertedSibling(String originalPath) async { final dotIndex = originalPath.lastIndexOf('.'); if (dotIndex < 0) return null; final directoryPrefix = originalPath.substring( @@ -638,7 +635,7 @@ class DownloadHistoryNotifier extends Notifier { for (final candidateName in _conversionRenameCandidates( fileName, - includeAlternateExtensions: includeAlternateExtensions, + includeAlternateExtensions: true, )) { final candidatePath = '$directoryPrefix$candidateName'; if (candidatePath == originalPath) continue; @@ -649,67 +646,6 @@ class DownloadHistoryNotifier extends Notifier { return null; } - Future verifyOrRepairHistoryItem(DownloadHistoryItem item) async { - if (await fileExists(item.filePath)) return true; - - DownloadHistoryItem? repaired; - if (item.storageMode == 'saf' && - item.downloadTreeUri != null && - item.downloadTreeUri!.isNotEmpty) { - var fileName = (item.safFileName ?? '').trim(); - if (fileName.isEmpty && isContentUri(item.filePath)) { - fileName = _fileNameFromUri(item.filePath); - } - for (final candidate in _conversionRenameCandidates(fileName)) { - try { - final resolved = await PlatformBridge.resolveSafFile( - treeUri: item.downloadTreeUri!, - relativeDir: item.safRelativeDir ?? '', - fileName: candidate, - ); - final uri = (resolved['uri'] as String? ?? '').trim(); - if (uri.isEmpty || !await fileExists(uri)) continue; - final relativeDir = (resolved['relative_dir'] as String? ?? '') - .trim(); - repaired = item.copyWith( - filePath: uri, - safFileName: candidate, - safRelativeDir: relativeDir.isEmpty - ? item.safRelativeDir - : relativeDir, - safRepaired: true, - ); - break; - } catch (error) { - _historyLog.w('Failed to resolve renamed SAF file: $error'); - } - } - } else if (!isContentUri(item.filePath)) { - final sibling = await _findConvertedSibling( - item.filePath, - includeAlternateExtensions: false, - ); - if (sibling != null) repaired = item.copyWith(filePath: sibling); - } - - if (repaired == null) return false; - await _db.upsert(repaired.toJson()); - final updatedItems = state.items - .map((entry) => entry.id == repaired!.id ? repaired : entry) - .toList(growable: false); - final updatedLookupItems = state.lookupItems - .map((entry) => entry.id == repaired!.id ? repaired : entry) - .toList(growable: false); - state = state.copyWith( - items: updatedItems, - lookupItems: updatedLookupItems, - ); - _historyLog.i( - 'Reconciled renamed conversion: ${item.filePath} -> ${repaired.filePath}', - ); - return true; - } - Future< ({ List orphanedIds, @@ -1090,28 +1026,6 @@ final downloadHistoryProvider = DownloadHistoryNotifier.new, ); -class DownloadHistoryGroupedCounts { - final int albumCount; - final int singleTrackCount; - - const DownloadHistoryGroupedCounts({ - required this.albumCount, - required this.singleTrackCount, - }); -} - -final downloadHistoryGroupedCountsProvider = - FutureProvider((ref) async { - ref.watch( - downloadHistoryProvider.select((state) => state.loadedIndexVersion), - ); - final counts = await HistoryDatabase.instance.getGroupedCounts(); - return DownloadHistoryGroupedCounts( - albumCount: counts['albums'] ?? 0, - singleTrackCount: counts['singles'] ?? 0, - ); - }); - HistoryLookupRequest historyLookupForTrack(Track track) { return HistoryLookupRequest( spotifyId: track.id, @@ -1121,21 +1035,8 @@ HistoryLookupRequest historyLookupForTrack(Track track) { ); } -final downloadHistoryExistsProvider = FutureProvider.autoDispose - .family((ref, request) async { - ref.watch( - downloadHistoryProvider.select((state) => state.loadedIndexVersion), - ); - final notifier = ref.read(downloadHistoryProvider.notifier); - final row = await HistoryDatabase.instance.findExistingTrack(request); - if (row == null) return false; - return notifier.verifyOrRepairHistoryItem( - DownloadHistoryItem.fromJson(row), - ); - }); - -// Batch lookups deliberately avoid per-row SAF verification. Startup repair -// reconciles stale rows; the single-track provider above keeps strict checks. +// Batch lookups deliberately avoid per-row SAF verification; startup repair +// reconciles stale rows. final downloadHistoryBatchExistsProvider = FutureProvider.autoDispose .family, HistoryBatchLookupRequest>((ref, request) async { ref.watch( diff --git a/lib/services/history_database.dart b/lib/services/history_database.dart index d938b812..3c7b9ff7 100644 --- a/lib/services/history_database.dart +++ b/lib/services/history_database.dart @@ -1078,25 +1078,6 @@ class HistoryDatabase { return Sqflite.firstIntValue(result) ?? 0; } - Future> getGroupedCounts() async { - final db = await database; - final rows = await db.rawQuery(''' - SELECT - SUM(CASE WHEN track_count > 1 THEN 1 ELSE 0 END) AS albums, - SUM(CASE WHEN track_count = 1 THEN 1 ELSE 0 END) AS singles - FROM ( - SELECT COUNT(*) AS track_count - FROM history - GROUP BY album_key - ) - '''); - final row = rows.isEmpty ? const {} : rows.first; - return { - 'albums': (row['albums'] as num?)?.toInt() ?? 0, - 'singles': (row['singles'] as num?)?.toInt() ?? 0, - }; - } - Future?> findExisting({ String? spotifyId, String? isrc, diff --git a/lib/services/library_database.dart b/lib/services/library_database.dart index cbd0ae94..556d50ab 100644 --- a/lib/services/library_database.dart +++ b/lib/services/library_database.dart @@ -1006,52 +1006,6 @@ class LibraryDatabase { _log.i('Replaced library with ${items.length} items'); } - /// Atomically replaces the Library while consuming bounded scan batches. - /// The stream may represent tens of thousands of tracks without requiring a - /// second full list of models/maps on the Dart heap. - Future replaceAllStream( - Stream> items, { - int batchSize = 300, - }) async { - if (batchSize <= 0) { - throw ArgumentError.value(batchSize, 'batchSize', 'Must be positive'); - } - final db = await database; - var inserted = 0; - await db.transaction((txn) async { - await txn.delete('library_path_keys'); - await txn.delete('library'); - - var batch = txn.batch(); - var pending = 0; - Future flush() async { - if (pending == 0) return; - await batch.commit(noResult: true); - batch = txn.batch(); - pending = 0; - } - - await for (final json in items) { - final id = json['id'] as String?; - if (id == null || id.trim().isEmpty) { - throw const FormatException('Library scan row has no valid id'); - } - batch.insert( - 'library', - _jsonToDbRow(json), - conflictAlgorithm: ConflictAlgorithm.replace, - ); - _putPathKeysInBatch(batch, id, json['filePath'] as String?); - inserted++; - pending++; - if (pending >= batchSize) await flush(); - } - await flush(); - }); - _log.i('Stream-replaced library with $inserted items'); - return inserted; - } - /// Stages scan rows in bounded, independently committed batches, then swaps /// only this source in one short transaction. Download-history exclusion is /// an indexed SQLite anti-join, avoiding a full History path set in Dart.