mirror of
https://github.com/spotiflacapp/SpotiFLAC-Mobile.git
synced 2026-06-01 03:15:17 +07:00
fix: preserve local convert format and library entries
This commit is contained in:
parent
ce4be0ba97
commit
f0013fac16
4 changed files with 182 additions and 18 deletions
|
|
@ -1631,7 +1631,12 @@ class _LocalAlbumScreenState extends ConsumerState<LocalAlbumScreen> {
|
||||||
try {
|
try {
|
||||||
await PlatformBridge.safDelete(item.filePath);
|
await PlatformBridge.safDelete(item.filePath);
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
await localDb.deleteByPath(item.filePath);
|
await localDb.replaceWithConvertedItem(
|
||||||
|
item: item,
|
||||||
|
newFilePath: safUri,
|
||||||
|
targetFormat: targetFormat,
|
||||||
|
bitrate: bitrate,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -1643,8 +1648,12 @@ class _LocalAlbumScreenState extends ConsumerState<LocalAlbumScreen> {
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Regular file: just remove old entry, rescan will find the new one
|
await localDb.replaceWithConvertedItem(
|
||||||
await localDb.deleteByPath(item.filePath);
|
item: item,
|
||||||
|
newFilePath: newPath,
|
||||||
|
targetFormat: targetFormat,
|
||||||
|
bitrate: bitrate,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
successCount++;
|
successCount++;
|
||||||
|
|
|
||||||
|
|
@ -5853,13 +5853,27 @@ class _QueueTabState extends ConsumerState<QueueTab> {
|
||||||
final baseName = dotIdx > 0
|
final baseName = dotIdx > 0
|
||||||
? oldFileName.substring(0, dotIdx)
|
? oldFileName.substring(0, dotIdx)
|
||||||
: oldFileName;
|
: oldFileName;
|
||||||
final newExt = targetFormat.toLowerCase() == 'opus'
|
String newExt;
|
||||||
? '.opus'
|
String mimeType;
|
||||||
: '.mp3';
|
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 newFileName = '$baseName$newExt';
|
||||||
final mimeType = targetFormat.toLowerCase() == 'opus'
|
|
||||||
? 'audio/opus'
|
|
||||||
: 'audio/mpeg';
|
|
||||||
|
|
||||||
final safUri = await PlatformBridge.createSafFileFromPath(
|
final safUri = await PlatformBridge.createSafFileFromPath(
|
||||||
treeUri: treeUri,
|
treeUri: treeUri,
|
||||||
|
|
@ -5884,7 +5898,12 @@ class _QueueTabState extends ConsumerState<QueueTab> {
|
||||||
try {
|
try {
|
||||||
await PlatformBridge.safDelete(item.filePath);
|
await PlatformBridge.safDelete(item.filePath);
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
await LibraryDatabase.instance.deleteByPath(item.filePath);
|
await LibraryDatabase.instance.replaceWithConvertedItem(
|
||||||
|
item: item.localItem!,
|
||||||
|
newFilePath: safUri,
|
||||||
|
targetFormat: targetFormat,
|
||||||
|
bitrate: bitrate,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -5903,7 +5922,12 @@ class _QueueTabState extends ConsumerState<QueueTab> {
|
||||||
clearAudioSpecs: true,
|
clearAudioSpecs: true,
|
||||||
);
|
);
|
||||||
} else if (item.localItem != null) {
|
} 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++;
|
successCount++;
|
||||||
|
|
|
||||||
|
|
@ -3929,8 +3929,50 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
|
||||||
final newQuality = _buildConvertedQualityLabel(targetFormat, bitrate);
|
final newQuality = _buildConvertedQualityLabel(targetFormat, bitrate);
|
||||||
|
|
||||||
if (isSaf) {
|
if (isSaf) {
|
||||||
final treeUri = _downloadItem?.downloadTreeUri;
|
String? treeUri;
|
||||||
final relativeDir = _downloadItem?.safRelativeDir ?? '';
|
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) {
|
if (treeUri == null || treeUri.isEmpty) {
|
||||||
try {
|
try {
|
||||||
await File(newPath).delete();
|
await File(newPath).delete();
|
||||||
|
|
@ -3949,11 +3991,6 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final oldFileName =
|
|
||||||
(_downloadItem?.safFileName != null &&
|
|
||||||
_downloadItem!.safFileName!.isNotEmpty)
|
|
||||||
? _downloadItem!.safFileName!
|
|
||||||
: _extractFileNameFromPathOrUri(cleanFilePath);
|
|
||||||
final dotIdx = oldFileName.lastIndexOf('.');
|
final dotIdx = oldFileName.lastIndexOf('.');
|
||||||
final baseName = dotIdx > 0
|
final baseName = dotIdx > 0
|
||||||
? oldFileName.substring(0, dotIdx)
|
? oldFileName.substring(0, dotIdx)
|
||||||
|
|
@ -4022,6 +4059,14 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
|
||||||
clearAudioSpecs: true,
|
clearAudioSpecs: true,
|
||||||
);
|
);
|
||||||
await ref.read(downloadHistoryProvider.notifier).reloadFromStorage();
|
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 {
|
try {
|
||||||
|
|
@ -4041,6 +4086,14 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
|
||||||
clearAudioSpecs: true,
|
clearAudioSpecs: true,
|
||||||
);
|
);
|
||||||
await ref.read(downloadHistoryProvider.notifier).reloadFromStorage();
|
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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -431,6 +431,45 @@ class LibraryDatabase {
|
||||||
await db.delete('library', where: 'file_path = ?', whereArgs: [filePath]);
|
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 {
|
Future<void> delete(String id) async {
|
||||||
final db = await database;
|
final db = await database;
|
||||||
await db.delete('library', where: 'id = ?', whereArgs: [id]);
|
await db.delete('library', where: 'id = ?', whereArgs: [id]);
|
||||||
|
|
@ -602,4 +641,43 @@ class LibraryDatabase {
|
||||||
}
|
}
|
||||||
return totalDeleted;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue