fix: preserve local convert format and library entries

This commit is contained in:
zarzet
2026-04-04 21:29:20 +07:00
parent 15d2c3b465
commit 9c3e934395
4 changed files with 182 additions and 18 deletions
+12 -3
View File
@@ -1631,7 +1631,12 @@ class _LocalAlbumScreenState extends ConsumerState<LocalAlbumScreen> {
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<LocalAlbumScreen> {
} 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++;
+32 -8
View File
@@ -5853,13 +5853,27 @@ class _QueueTabState extends ConsumerState<QueueTab> {
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<QueueTab> {
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<QueueTab> {
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++;
+60 -7
View File
@@ -3929,8 +3929,50 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
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<TrackMetadataScreen> {
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<TrackMetadataScreen> {
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<TrackMetadataScreen> {
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();
}
}
+78
View File
@@ -431,6 +431,45 @@ class LibraryDatabase {
await db.delete('library', where: 'file_path = ?', whereArgs: [filePath]);
}
Future<void> 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<void> 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;
}
}