From 9c3e934395ab780e9e893a5ac49cd349c9e5da99 Mon Sep 17 00:00:00 2001 From: zarzet Date: Sat, 4 Apr 2026 21:29:20 +0700 Subject: [PATCH] fix: preserve local convert format and library entries --- lib/screens/local_album_screen.dart | 15 ++++- lib/screens/queue_tab.dart | 40 ++++++++++--- lib/screens/track_metadata_screen.dart | 67 +++++++++++++++++++--- lib/services/library_database.dart | 78 ++++++++++++++++++++++++++ 4 files changed, 182 insertions(+), 18 deletions(-) diff --git a/lib/screens/local_album_screen.dart b/lib/screens/local_album_screen.dart index 2a0aefa2..9ffeaca7 100644 --- a/lib/screens/local_album_screen.dart +++ b/lib/screens/local_album_screen.dart @@ -1631,7 +1631,12 @@ class _LocalAlbumScreenState extends ConsumerState { try { await PlatformBridge.safDelete(item.filePath); } catch (_) {} - await localDb.deleteByPath(item.filePath); + await localDb.replaceWithConvertedItem( + item: item, + newFilePath: safUri, + targetFormat: targetFormat, + bitrate: bitrate, + ); } try { @@ -1643,8 +1648,12 @@ class _LocalAlbumScreenState extends ConsumerState { } catch (_) {} } } else { - // Regular file: just remove old entry, rescan will find the new one - await localDb.deleteByPath(item.filePath); + await localDb.replaceWithConvertedItem( + item: item, + newFilePath: newPath, + targetFormat: targetFormat, + bitrate: bitrate, + ); } successCount++; diff --git a/lib/screens/queue_tab.dart b/lib/screens/queue_tab.dart index a6b4b409..954bd0d6 100644 --- a/lib/screens/queue_tab.dart +++ b/lib/screens/queue_tab.dart @@ -5853,13 +5853,27 @@ class _QueueTabState extends ConsumerState { final baseName = dotIdx > 0 ? oldFileName.substring(0, dotIdx) : oldFileName; - final newExt = targetFormat.toLowerCase() == 'opus' - ? '.opus' - : '.mp3'; + String newExt; + String mimeType; + switch (targetFormat.toLowerCase()) { + case 'opus': + newExt = '.opus'; + mimeType = 'audio/opus'; + break; + case 'alac': + newExt = '.m4a'; + mimeType = 'audio/mp4'; + break; + case 'flac': + newExt = '.flac'; + mimeType = 'audio/flac'; + break; + default: + newExt = '.mp3'; + mimeType = 'audio/mpeg'; + break; + } final newFileName = '$baseName$newExt'; - final mimeType = targetFormat.toLowerCase() == 'opus' - ? 'audio/opus' - : 'audio/mpeg'; final safUri = await PlatformBridge.createSafFileFromPath( treeUri: treeUri, @@ -5884,7 +5898,12 @@ class _QueueTabState extends ConsumerState { try { await PlatformBridge.safDelete(item.filePath); } catch (_) {} - await LibraryDatabase.instance.deleteByPath(item.filePath); + await LibraryDatabase.instance.replaceWithConvertedItem( + item: item.localItem!, + newFilePath: safUri, + targetFormat: targetFormat, + bitrate: bitrate, + ); } try { @@ -5903,7 +5922,12 @@ class _QueueTabState extends ConsumerState { clearAudioSpecs: true, ); } else if (item.localItem != null) { - await LibraryDatabase.instance.deleteByPath(item.filePath); + await LibraryDatabase.instance.replaceWithConvertedItem( + item: item.localItem!, + newFilePath: newPath, + targetFormat: targetFormat, + bitrate: bitrate, + ); } successCount++; diff --git a/lib/screens/track_metadata_screen.dart b/lib/screens/track_metadata_screen.dart index 88e7a541..d6464c4f 100644 --- a/lib/screens/track_metadata_screen.dart +++ b/lib/screens/track_metadata_screen.dart @@ -3929,8 +3929,50 @@ class _TrackMetadataScreenState extends ConsumerState { final newQuality = _buildConvertedQualityLabel(targetFormat, bitrate); if (isSaf) { - final treeUri = _downloadItem?.downloadTreeUri; - final relativeDir = _downloadItem?.safRelativeDir ?? ''; + String? treeUri; + String relativeDir = ''; + String oldFileName = ''; + if (_isLocalItem) { + final uri = Uri.parse(cleanFilePath); + final pathSegments = uri.pathSegments; + final treeIdx = pathSegments.indexOf('tree'); + final docIdx = pathSegments.indexOf('document'); + if (treeIdx >= 0 && treeIdx + 1 < pathSegments.length) { + final treeId = pathSegments[treeIdx + 1]; + treeUri = + 'content://${uri.authority}/tree/${Uri.encodeComponent(treeId)}'; + } + if (docIdx >= 0 && docIdx + 1 < pathSegments.length) { + final docPath = Uri.decodeFull(pathSegments[docIdx + 1]); + final slashIdx = docPath.lastIndexOf('/'); + if (slashIdx >= 0) { + oldFileName = docPath.substring(slashIdx + 1); + final treeId = treeIdx >= 0 && treeIdx + 1 < pathSegments.length + ? Uri.decodeFull(pathSegments[treeIdx + 1]) + : ''; + if (treeId.isNotEmpty && docPath.startsWith(treeId)) { + final afterTree = docPath.substring(treeId.length); + final trimmed = afterTree.startsWith('/') + ? afterTree.substring(1) + : afterTree; + final lastSlash = trimmed.lastIndexOf('/'); + relativeDir = lastSlash >= 0 + ? trimmed.substring(0, lastSlash) + : ''; + } + } else { + oldFileName = docPath; + } + } + } else { + treeUri = _downloadItem?.downloadTreeUri; + relativeDir = _downloadItem?.safRelativeDir ?? ''; + oldFileName = + (_downloadItem?.safFileName != null && + _downloadItem!.safFileName!.isNotEmpty) + ? _downloadItem!.safFileName! + : _extractFileNameFromPathOrUri(cleanFilePath); + } if (treeUri == null || treeUri.isEmpty) { try { await File(newPath).delete(); @@ -3949,11 +3991,6 @@ class _TrackMetadataScreenState extends ConsumerState { return; } - final oldFileName = - (_downloadItem?.safFileName != null && - _downloadItem!.safFileName!.isNotEmpty) - ? _downloadItem!.safFileName! - : _extractFileNameFromPathOrUri(cleanFilePath); final dotIdx = oldFileName.lastIndexOf('.'); final baseName = dotIdx > 0 ? oldFileName.substring(0, dotIdx) @@ -4022,6 +4059,14 @@ class _TrackMetadataScreenState extends ConsumerState { clearAudioSpecs: true, ); await ref.read(downloadHistoryProvider.notifier).reloadFromStorage(); + } else { + await LibraryDatabase.instance.replaceWithConvertedItem( + item: _localLibraryItem!, + newFilePath: safUri, + targetFormat: targetFormat, + bitrate: bitrate, + ); + await ref.read(localLibraryProvider.notifier).reloadFromStorage(); } try { @@ -4041,6 +4086,14 @@ class _TrackMetadataScreenState extends ConsumerState { clearAudioSpecs: true, ); await ref.read(downloadHistoryProvider.notifier).reloadFromStorage(); + } else { + await LibraryDatabase.instance.replaceWithConvertedItem( + item: _localLibraryItem!, + newFilePath: newPath, + targetFormat: targetFormat, + bitrate: bitrate, + ); + await ref.read(localLibraryProvider.notifier).reloadFromStorage(); } } diff --git a/lib/services/library_database.dart b/lib/services/library_database.dart index cf8ffd4a..f6b51f4d 100644 --- a/lib/services/library_database.dart +++ b/lib/services/library_database.dart @@ -431,6 +431,45 @@ class LibraryDatabase { await db.delete('library', where: 'file_path = ?', whereArgs: [filePath]); } + Future replaceWithConvertedItem({ + required LocalLibraryItem item, + required String newFilePath, + required String targetFormat, + required String bitrate, + }) async { + final db = await database; + final stat = await fileStat(newFilePath); + final now = DateTime.now(); + final normalizedFormat = _normalizeConvertedFormat(targetFormat); + final updated = item.toJson() + ..['id'] = _generateLibraryId(newFilePath) + ..['filePath'] = newFilePath + ..['scannedAt'] = now.toIso8601String() + ..['fileModTime'] = stat?.modified?.millisecondsSinceEpoch + ..['format'] = normalizedFormat + ..['bitrate'] = _convertedBitrate( + targetFormat: targetFormat, + bitrate: bitrate, + ); + + if (normalizedFormat == 'mp3' || normalizedFormat == 'opus') { + updated['bitDepth'] = null; + } + + await db.transaction((txn) async { + await txn.delete( + 'library', + where: 'id = ? OR file_path = ?', + whereArgs: [item.id, item.filePath], + ); + await txn.insert( + 'library', + _jsonToDbRow(updated), + conflictAlgorithm: ConflictAlgorithm.replace, + ); + }); + } + Future delete(String id) async { final db = await database; await db.delete('library', where: 'id = ?', whereArgs: [id]); @@ -602,4 +641,43 @@ class LibraryDatabase { } return totalDeleted; } + + String _normalizeConvertedFormat(String targetFormat) { + switch (targetFormat.trim().toLowerCase()) { + case 'alac': + return 'm4a'; + case 'flac': + return 'flac'; + case 'opus': + return 'opus'; + default: + return 'mp3'; + } + } + + int? _convertedBitrate({ + required String targetFormat, + required String bitrate, + }) { + switch (targetFormat.trim().toLowerCase()) { + case 'mp3': + case 'opus': + final match = RegExp(r'(\d+)').firstMatch(bitrate); + return match != null ? int.tryParse(match.group(1)!) : null; + default: + return null; + } + } + + String _generateLibraryId(String filePath) { + return 'lib_${_hashString(filePath).toRadixString(16)}'; + } + + int _hashString(String input) { + var hash = 5381; + for (final codeUnit in input.codeUnits) { + hash = (((hash << 5) + hash) + codeUnit) & 0xffffffff; + } + return hash; + } }