mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-08-02 17:18:36 +02:00
fix(download): suffix only colliding quality variants
This commit is contained in:
@@ -6769,7 +6769,7 @@ abstract class AppLocalizations {
|
||||
/// Description for retaining multiple quality versions
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Add the selected quality to the filename and keep each version in download history'**
|
||||
/// **'Keep every quality version; add its measured quality to the filename only when the name is already used'**
|
||||
String get downloadQualityVariantsDescription;
|
||||
|
||||
/// Track menu action to download another quality version
|
||||
|
||||
@@ -4136,7 +4136,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get downloadQualityVariantsDescription =>
|
||||
'Add the selected quality to the filename and keep each version in download history';
|
||||
'Jede Qualitätsversion behalten; die gemessene Qualität nur dann zum Dateinamen hinzufügen, wenn der Name bereits verwendet wird';
|
||||
|
||||
@override
|
||||
String get trackOptionDownloadQualityVariant => 'Download another quality';
|
||||
|
||||
@@ -4090,7 +4090,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get downloadQualityVariantsDescription =>
|
||||
'Add the selected quality to the filename and keep each version in download history';
|
||||
'Keep every quality version; add its measured quality to the filename only when the name is already used';
|
||||
|
||||
@override
|
||||
String get trackOptionDownloadQualityVariant => 'Download another quality';
|
||||
|
||||
@@ -4084,7 +4084,7 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get downloadQualityVariantsDescription =>
|
||||
'Add the selected quality to the filename and keep each version in download history';
|
||||
'Keep every quality version; add its measured quality to the filename only when the name is already used';
|
||||
|
||||
@override
|
||||
String get trackOptionDownloadQualityVariant => 'Download another quality';
|
||||
@@ -8790,7 +8790,7 @@ class AppLocalizationsEsEs extends AppLocalizationsEs {
|
||||
|
||||
@override
|
||||
String get downloadQualityVariantsDescription =>
|
||||
'Add the selected quality to the filename and keep each version in download history';
|
||||
'Conservar cada versión de calidad; añadir la calidad medida al nombre solo cuando el nombre ya esté en uso';
|
||||
|
||||
@override
|
||||
String get trackOptionDownloadQualityVariant => 'Download another quality';
|
||||
|
||||
@@ -4196,7 +4196,7 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get downloadQualityVariantsDescription =>
|
||||
'Ajouter la qualité sélectionnée au nom du fichier et conserver chaque version dans l\'historique des téléchargements';
|
||||
'Conserver chaque version de qualité ; ajouter la qualité mesurée au nom du fichier uniquement si le nom est déjà utilisé';
|
||||
|
||||
@override
|
||||
String get trackOptionDownloadQualityVariant =>
|
||||
|
||||
@@ -4087,7 +4087,7 @@ class AppLocalizationsId extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get downloadQualityVariantsDescription =>
|
||||
'Tambahkan kualitas yang dipilih ke nama file dan simpan setiap versi di riwayat unduhan';
|
||||
'Simpan setiap versi kualitas; tambahkan kualitas terukur ke nama file hanya jika namanya sudah digunakan';
|
||||
|
||||
@override
|
||||
String get trackOptionDownloadQualityVariant => 'Unduh kualitas lain';
|
||||
|
||||
@@ -4079,7 +4079,7 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get downloadQualityVariantsDescription =>
|
||||
'Add the selected quality to the filename and keep each version in download history';
|
||||
'各品質版を保持し、同じ名前が既に使用されている場合のみ測定品質をファイル名に追加します';
|
||||
|
||||
@override
|
||||
String get trackOptionDownloadQualityVariant => 'Download another quality';
|
||||
|
||||
@@ -3968,7 +3968,7 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get downloadQualityVariantsDescription =>
|
||||
'선택된 음질을 파일 이름에 추가하고, 각 버전을 다운로드 기록에 유지합니다';
|
||||
'각 음질 버전을 유지하고, 같은 이름이 이미 사용 중일 때만 측정된 음질을 파일 이름에 추가합니다';
|
||||
|
||||
@override
|
||||
String get trackOptionDownloadQualityVariant => '다른 음질 다운로드';
|
||||
|
||||
@@ -4084,7 +4084,7 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get downloadQualityVariantsDescription =>
|
||||
'Add the selected quality to the filename and keep each version in download history';
|
||||
'Keep every quality version; add its measured quality to the filename only when the name is already used';
|
||||
|
||||
@override
|
||||
String get trackOptionDownloadQualityVariant => 'Download another quality';
|
||||
@@ -8755,7 +8755,7 @@ class AppLocalizationsPtPt extends AppLocalizationsPt {
|
||||
|
||||
@override
|
||||
String get downloadQualityVariantsDescription =>
|
||||
'Add the selected quality to the filename and keep each version in download history';
|
||||
'Manter todas as versões de qualidade; adicionar a qualidade medida ao nome apenas quando o nome já estiver em uso';
|
||||
|
||||
@override
|
||||
String get trackOptionDownloadQualityVariant => 'Download another quality';
|
||||
|
||||
@@ -4121,7 +4121,7 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get downloadQualityVariantsDescription =>
|
||||
'Add the selected quality to the filename and keep each version in download history';
|
||||
'Сохранять каждую версию качества; добавлять измеренное качество к имени файла, только если имя уже занято';
|
||||
|
||||
@override
|
||||
String get trackOptionDownloadQualityVariant => 'Download another quality';
|
||||
|
||||
@@ -4120,7 +4120,7 @@ class AppLocalizationsTr extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get downloadQualityVariantsDescription =>
|
||||
'Add the selected quality to the filename and keep each version in download history';
|
||||
'Her kalite sürümünü sakla; ölçülen kaliteyi yalnızca ad zaten kullanılıyorsa dosya adına ekle';
|
||||
|
||||
@override
|
||||
String get trackOptionDownloadQualityVariant => 'Download another quality';
|
||||
|
||||
@@ -4138,7 +4138,7 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get downloadQualityVariantsDescription =>
|
||||
'Add the selected quality to the filename and keep each version in download history';
|
||||
'Зберігати кожну версію якості; додавати виміряну якість до назви файлу лише тоді, коли назва вже використовується';
|
||||
|
||||
@override
|
||||
String get trackOptionDownloadQualityVariant => 'Download another quality';
|
||||
|
||||
@@ -6006,7 +6006,7 @@
|
||||
"@downloadQualityVariants": {
|
||||
"description": "Setting to retain multiple quality versions of the same track"
|
||||
},
|
||||
"downloadQualityVariantsDescription": "Add the selected quality to the filename and keep each version in download history",
|
||||
"downloadQualityVariantsDescription": "Jede Qualitätsversion behalten; die gemessene Qualität nur dann zum Dateinamen hinzufügen, wenn der Name bereits verwendet wird",
|
||||
"@downloadQualityVariantsDescription": {
|
||||
"description": "Description for retaining multiple quality versions"
|
||||
},
|
||||
|
||||
@@ -5320,7 +5320,7 @@
|
||||
"@downloadQualityVariants": {
|
||||
"description": "Setting to retain multiple quality versions of the same track"
|
||||
},
|
||||
"downloadQualityVariantsDescription": "Add the selected quality to the filename and keep each version in download history",
|
||||
"downloadQualityVariantsDescription": "Keep every quality version; add its measured quality to the filename only when the name is already used",
|
||||
"@downloadQualityVariantsDescription": {
|
||||
"description": "Description for retaining multiple quality versions"
|
||||
},
|
||||
|
||||
@@ -6006,7 +6006,7 @@
|
||||
"@downloadQualityVariants": {
|
||||
"description": "Setting to retain multiple quality versions of the same track"
|
||||
},
|
||||
"downloadQualityVariantsDescription": "Add the selected quality to the filename and keep each version in download history",
|
||||
"downloadQualityVariantsDescription": "Conservar cada versión de calidad; añadir la calidad medida al nombre solo cuando el nombre ya esté en uso",
|
||||
"@downloadQualityVariantsDescription": {
|
||||
"description": "Description for retaining multiple quality versions"
|
||||
},
|
||||
|
||||
@@ -6006,7 +6006,7 @@
|
||||
"@downloadQualityVariants": {
|
||||
"description": "Setting to retain multiple quality versions of the same track"
|
||||
},
|
||||
"downloadQualityVariantsDescription": "Ajouter la qualité sélectionnée au nom du fichier et conserver chaque version dans l'historique des téléchargements",
|
||||
"downloadQualityVariantsDescription": "Conserver chaque version de qualité ; ajouter la qualité mesurée au nom du fichier uniquement si le nom est déjà utilisé",
|
||||
"@downloadQualityVariantsDescription": {
|
||||
"description": "Description for retaining multiple quality versions"
|
||||
},
|
||||
|
||||
@@ -5962,7 +5962,7 @@
|
||||
"@downloadQualityVariants": {
|
||||
"description": "Pengaturan untuk menyimpan beberapa versi kualitas dari lagu yang sama"
|
||||
},
|
||||
"downloadQualityVariantsDescription": "Tambahkan kualitas yang dipilih ke nama file dan simpan setiap versi di riwayat unduhan",
|
||||
"downloadQualityVariantsDescription": "Simpan setiap versi kualitas; tambahkan kualitas terukur ke nama file hanya jika namanya sudah digunakan",
|
||||
"@downloadQualityVariantsDescription": {
|
||||
"description": "Deskripsi penyimpanan beberapa versi kualitas"
|
||||
},
|
||||
|
||||
@@ -6006,7 +6006,7 @@
|
||||
"@downloadQualityVariants": {
|
||||
"description": "Setting to retain multiple quality versions of the same track"
|
||||
},
|
||||
"downloadQualityVariantsDescription": "Add the selected quality to the filename and keep each version in download history",
|
||||
"downloadQualityVariantsDescription": "各品質版を保持し、同じ名前が既に使用されている場合のみ測定品質をファイル名に追加します",
|
||||
"@downloadQualityVariantsDescription": {
|
||||
"description": "Description for retaining multiple quality versions"
|
||||
},
|
||||
|
||||
@@ -5120,7 +5120,7 @@
|
||||
"@downloadQualityVariants": {
|
||||
"description": "Setting to retain multiple quality versions of the same track"
|
||||
},
|
||||
"downloadQualityVariantsDescription": "선택된 음질을 파일 이름에 추가하고, 각 버전을 다운로드 기록에 유지합니다",
|
||||
"downloadQualityVariantsDescription": "각 음질 버전을 유지하고, 같은 이름이 이미 사용 중일 때만 측정된 음질을 파일 이름에 추가합니다",
|
||||
"@downloadQualityVariantsDescription": {
|
||||
"description": "Description for retaining multiple quality versions"
|
||||
},
|
||||
|
||||
@@ -6006,7 +6006,7 @@
|
||||
"@downloadQualityVariants": {
|
||||
"description": "Setting to retain multiple quality versions of the same track"
|
||||
},
|
||||
"downloadQualityVariantsDescription": "Add the selected quality to the filename and keep each version in download history",
|
||||
"downloadQualityVariantsDescription": "Manter todas as versões de qualidade; adicionar a qualidade medida ao nome apenas quando o nome já estiver em uso",
|
||||
"@downloadQualityVariantsDescription": {
|
||||
"description": "Description for retaining multiple quality versions"
|
||||
},
|
||||
|
||||
@@ -6006,7 +6006,7 @@
|
||||
"@downloadQualityVariants": {
|
||||
"description": "Setting to retain multiple quality versions of the same track"
|
||||
},
|
||||
"downloadQualityVariantsDescription": "Add the selected quality to the filename and keep each version in download history",
|
||||
"downloadQualityVariantsDescription": "Сохранять каждую версию качества; добавлять измеренное качество к имени файла, только если имя уже занято",
|
||||
"@downloadQualityVariantsDescription": {
|
||||
"description": "Description for retaining multiple quality versions"
|
||||
},
|
||||
|
||||
@@ -6006,7 +6006,7 @@
|
||||
"@downloadQualityVariants": {
|
||||
"description": "Setting to retain multiple quality versions of the same track"
|
||||
},
|
||||
"downloadQualityVariantsDescription": "Add the selected quality to the filename and keep each version in download history",
|
||||
"downloadQualityVariantsDescription": "Her kalite sürümünü sakla; ölçülen kaliteyi yalnızca ad zaten kullanılıyorsa dosya adına ekle",
|
||||
"@downloadQualityVariantsDescription": {
|
||||
"description": "Description for retaining multiple quality versions"
|
||||
},
|
||||
|
||||
@@ -6006,7 +6006,7 @@
|
||||
"@downloadQualityVariants": {
|
||||
"description": "Setting to retain multiple quality versions of the same track"
|
||||
},
|
||||
"downloadQualityVariantsDescription": "Add the selected quality to the filename and keep each version in download history",
|
||||
"downloadQualityVariantsDescription": "Зберігати кожну версію якості; додавати виміряну якість до назви файлу лише тоді, коли назва вже використовується",
|
||||
"@downloadQualityVariantsDescription": {
|
||||
"description": "Description for retaining multiple quality versions"
|
||||
},
|
||||
|
||||
@@ -126,6 +126,10 @@ final _qualityFilenameTokenPattern = RegExp(
|
||||
r'\{quality_variant\}',
|
||||
caseSensitive: false,
|
||||
);
|
||||
final _explicitQualityFilenameTokenPattern = RegExp(
|
||||
r'\{(?:quality|quality_variant)\}',
|
||||
caseSensitive: false,
|
||||
);
|
||||
|
||||
class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
Timer? _queuePersistDebounce;
|
||||
@@ -176,9 +180,31 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
int _queueItemSequence = 0;
|
||||
bool _isLoaded = false;
|
||||
final Set<String> _ensuredDirs = {};
|
||||
final Map<String, Future<void>> _qualityVariantFileLocks = {};
|
||||
Future<String>? _appFolderStorageFallback;
|
||||
final Set<String> _rejectedAppFolderRoots = {};
|
||||
int _idleProgressPollTick = 0;
|
||||
|
||||
Future<T> _withQualityVariantFileLock<T>(
|
||||
String path,
|
||||
Future<T> Function() action,
|
||||
) async {
|
||||
final key = path.toLowerCase();
|
||||
final previous = _qualityVariantFileLocks[key];
|
||||
final completer = Completer<void>();
|
||||
final current = completer.future;
|
||||
_qualityVariantFileLocks[key] = current;
|
||||
if (previous != null) await previous;
|
||||
try {
|
||||
return await action();
|
||||
} finally {
|
||||
completer.complete();
|
||||
if (identical(_qualityVariantFileLocks[key], current)) {
|
||||
_qualityVariantFileLocks.remove(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool _networkPausedByWifiOnly = false;
|
||||
List<ConnectivityResult>? _lastConnectivityResults;
|
||||
DateTime _lastConnectionCleanupAt = DateTime.fromMillisecondsSinceEpoch(0);
|
||||
@@ -444,6 +470,7 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
String? label,
|
||||
String? copyright,
|
||||
bool stageSafForDeferredPublish = false,
|
||||
bool qualityVariantCollisionOnly = false,
|
||||
}) {
|
||||
final resolvedAlbumArtist = _resolveAlbumArtistForMetadata(track, settings);
|
||||
final postProcessingEnabled =
|
||||
@@ -529,6 +556,7 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
qualityVariant: item.preserveQualityVariant
|
||||
? qualityVariantStagingLabel(item.id)
|
||||
: '',
|
||||
qualityVariantCollisionOnly: qualityVariantCollisionOnly,
|
||||
songLinkRegion: settings.songLinkRegion,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -193,6 +193,35 @@ extension _DownloadQueueFinalization on DownloadQueueNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
Future<({String uri, String fileName})?> _writeTempToSafCollisionAware({
|
||||
required String treeUri,
|
||||
required String relativeDir,
|
||||
required String cleanFileName,
|
||||
required String variantFileName,
|
||||
required String mimeType,
|
||||
required String srcPath,
|
||||
String preservedSuffix = '',
|
||||
}) async {
|
||||
try {
|
||||
final result = await PlatformBridge.createCollisionAwareSafFileFromPath(
|
||||
treeUri: treeUri,
|
||||
relativeDir: relativeDir,
|
||||
cleanFileName: cleanFileName,
|
||||
variantFileName: variantFileName,
|
||||
mimeType: mimeType,
|
||||
srcPath: srcPath,
|
||||
preservedSuffix: preservedSuffix,
|
||||
);
|
||||
final uri = (result['uri'] as String? ?? '').trim();
|
||||
final publishedName = (result['file_name'] as String? ?? '').trim();
|
||||
if (uri.isEmpty || publishedName.isEmpty) return null;
|
||||
return (uri: uri, fileName: publishedName);
|
||||
} catch (e) {
|
||||
_log.w('Failed to write collision-aware temp file to SAF: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _writeLrcToSaf({
|
||||
required String treeUri,
|
||||
required String relativeDir,
|
||||
@@ -251,6 +280,8 @@ extension _DownloadQueueFinalization on DownloadQueueNotifier {
|
||||
op,
|
||||
bool avoidOverwrite = false,
|
||||
String preservedSuffix = '',
|
||||
String? collisionCleanFileName,
|
||||
String? collisionVariantFileName,
|
||||
void Function(String fileName)? onPublishedFileName,
|
||||
}) async {
|
||||
final tempPath = await _copySafToTemp(uri);
|
||||
@@ -267,7 +298,21 @@ extension _DownloadQueueFinalization on DownloadQueueNotifier {
|
||||
final dotIndex = fileName.lastIndexOf('.');
|
||||
final ext = dotIndex >= 0 ? fileName.substring(dotIndex) : '';
|
||||
String? newUri;
|
||||
if (avoidOverwrite) {
|
||||
if (collisionCleanFileName != null && collisionVariantFileName != null) {
|
||||
final published = await _writeTempToSafCollisionAware(
|
||||
treeUri: treeUri,
|
||||
relativeDir: relativeDir,
|
||||
cleanFileName: collisionCleanFileName,
|
||||
variantFileName: collisionVariantFileName,
|
||||
mimeType: _mimeTypeForExt(ext),
|
||||
srcPath: outPath,
|
||||
preservedSuffix: preservedSuffix,
|
||||
);
|
||||
newUri = published?.uri;
|
||||
if (published != null) {
|
||||
onPublishedFileName?.call(published.fileName);
|
||||
}
|
||||
} else if (avoidOverwrite) {
|
||||
final published = await _writeTempToSafUnique(
|
||||
treeUri: treeUri,
|
||||
relativeDir: relativeDir,
|
||||
@@ -321,6 +366,7 @@ extension _DownloadQueueFinalization on DownloadQueueNotifier {
|
||||
String? downloadTreeUri,
|
||||
String? safRelativeDir,
|
||||
String? fileName,
|
||||
bool collisionOnly = false,
|
||||
}) async {
|
||||
if (!item.preserveQualityVariant || result['already_exists'] == true) {
|
||||
return _QualityVariantFileOutcome(filePath: filePath, fileName: fileName);
|
||||
@@ -391,11 +437,16 @@ extension _DownloadQueueFinalization on DownloadQueueNotifier {
|
||||
final currentFileName = storageMode == 'saf' && isContentUri(filePath)
|
||||
? (fileName ?? result['file_name']?.toString() ?? '')
|
||||
: (localPathSegments.isEmpty ? '' : localPathSegments.last);
|
||||
final preferredFileName = applyQualityVariantFilenameLabel(
|
||||
final variantFileName = applyQualityVariantFilenameLabel(
|
||||
fileName: currentFileName,
|
||||
stagingLabel: stagingLabel,
|
||||
qualityLabel: qualityLabel,
|
||||
);
|
||||
final cleanFileName = removeQualityVariantStagingLabel(
|
||||
fileName: currentFileName,
|
||||
stagingLabel: stagingLabel,
|
||||
);
|
||||
final preferredFileName = variantFileName;
|
||||
if (preferredFileName == currentFileName) {
|
||||
return _QualityVariantFileOutcome(
|
||||
filePath: filePath,
|
||||
@@ -417,8 +468,10 @@ extension _DownloadQueueFinalization on DownloadQueueNotifier {
|
||||
uri: filePath,
|
||||
treeUri: downloadTreeUri,
|
||||
relativeDir: safRelativeDir ?? '',
|
||||
avoidOverwrite: true,
|
||||
avoidOverwrite: !collisionOnly,
|
||||
preservedSuffix: qualityLabel,
|
||||
collisionCleanFileName: collisionOnly ? cleanFileName : null,
|
||||
collisionVariantFileName: collisionOnly ? variantFileName : null,
|
||||
onPublishedFileName: (name) => publishedFileName = name,
|
||||
op: (tempPath, addCleanup) async => (tempPath, preferredFileName),
|
||||
);
|
||||
@@ -441,44 +494,70 @@ extension _DownloadQueueFinalization on DownloadQueueNotifier {
|
||||
|
||||
final source = File(filePath);
|
||||
final parent = source.parent;
|
||||
var target = File(
|
||||
'${parent.path}${Platform.pathSeparator}$preferredFileName',
|
||||
final cleanTarget = File(
|
||||
'${parent.path}${Platform.pathSeparator}$cleanFileName',
|
||||
);
|
||||
var counter = 2;
|
||||
while (await target.exists() && target.path != source.path) {
|
||||
final dotIndex = preferredFileName.lastIndexOf('.');
|
||||
final hasExtension = dotIndex > 0;
|
||||
final stem = hasExtension
|
||||
? preferredFileName.substring(0, dotIndex)
|
||||
final lockTarget = collisionOnly
|
||||
? cleanTarget
|
||||
: File('${parent.path}${Platform.pathSeparator}$preferredFileName');
|
||||
return _withQualityVariantFileLock(lockTarget.path, () async {
|
||||
final selectedFileName = collisionOnly
|
||||
? resolveQualityVariantFilename(
|
||||
fileName: currentFileName,
|
||||
stagingLabel: stagingLabel,
|
||||
qualityLabel: qualityLabel,
|
||||
collisionOnly: true,
|
||||
cleanNameExists:
|
||||
cleanTarget.path != source.path && await cleanTarget.exists(),
|
||||
)
|
||||
: preferredFileName;
|
||||
final extension = hasExtension
|
||||
? preferredFileName.substring(dotIndex)
|
||||
: '';
|
||||
target = File(
|
||||
'${parent.path}${Platform.pathSeparator}$stem ($counter)$extension',
|
||||
if (selectedFileName == currentFileName) {
|
||||
return _QualityVariantFileOutcome(
|
||||
filePath: filePath,
|
||||
fileName: fileName,
|
||||
metadata: metadata,
|
||||
);
|
||||
}
|
||||
|
||||
var target = File(
|
||||
'${parent.path}${Platform.pathSeparator}$selectedFileName',
|
||||
);
|
||||
counter++;
|
||||
}
|
||||
try {
|
||||
final renamed = await source.rename(target.path);
|
||||
result['file_path'] = renamed.path;
|
||||
final renamedSegments = renamed.uri.pathSegments;
|
||||
result['file_name'] = renamedSegments.isEmpty
|
||||
? null
|
||||
: renamedSegments.last;
|
||||
return _QualityVariantFileOutcome(
|
||||
filePath: renamed.path,
|
||||
fileName: result['file_name'] as String?,
|
||||
metadata: metadata,
|
||||
);
|
||||
} catch (e) {
|
||||
_log.w('Failed to apply measured quality filename: $e');
|
||||
return _QualityVariantFileOutcome(
|
||||
filePath: filePath,
|
||||
fileName: fileName,
|
||||
metadata: metadata,
|
||||
);
|
||||
}
|
||||
var counter = 2;
|
||||
while (await target.exists() && target.path != source.path) {
|
||||
final dotIndex = selectedFileName.lastIndexOf('.');
|
||||
final hasExtension = dotIndex > 0;
|
||||
final stem = hasExtension
|
||||
? selectedFileName.substring(0, dotIndex)
|
||||
: selectedFileName;
|
||||
final extension = hasExtension
|
||||
? selectedFileName.substring(dotIndex)
|
||||
: '';
|
||||
target = File(
|
||||
'${parent.path}${Platform.pathSeparator}$stem ($counter)$extension',
|
||||
);
|
||||
counter++;
|
||||
}
|
||||
try {
|
||||
final renamed = await source.rename(target.path);
|
||||
result['file_path'] = renamed.path;
|
||||
final renamedSegments = renamed.uri.pathSegments;
|
||||
result['file_name'] = renamedSegments.isEmpty
|
||||
? null
|
||||
: renamedSegments.last;
|
||||
return _QualityVariantFileOutcome(
|
||||
filePath: renamed.path,
|
||||
fileName: result['file_name'] as String?,
|
||||
metadata: metadata,
|
||||
);
|
||||
} catch (e) {
|
||||
_log.w('Failed to apply measured quality filename: $e');
|
||||
return _QualityVariantFileOutcome(
|
||||
filePath: filePath,
|
||||
fileName: fileName,
|
||||
metadata: metadata,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Shared decrypt finalize used by both the inline single-item pipeline
|
||||
|
||||
@@ -11,6 +11,7 @@ class _NativeWorkerRequestContext {
|
||||
final String? downloadTreeUri;
|
||||
final String? safRelativeDir;
|
||||
final String? safFileName;
|
||||
final bool qualityVariantCollisionOnly;
|
||||
|
||||
const _NativeWorkerRequestContext({
|
||||
required this.item,
|
||||
@@ -22,6 +23,7 @@ class _NativeWorkerRequestContext {
|
||||
this.downloadTreeUri,
|
||||
this.safRelativeDir,
|
||||
this.safFileName,
|
||||
this.qualityVariantCollisionOnly = false,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -730,6 +732,9 @@ extension _DownloadQueueNativeWorker on DownloadQueueNotifier {
|
||||
item,
|
||||
baseFilenameFormat,
|
||||
);
|
||||
final qualityVariantCollisionOnly =
|
||||
item.preserveQualityVariant &&
|
||||
!_explicitQualityFilenameTokenPattern.hasMatch(baseFilenameFormat);
|
||||
if (isSafMode) {
|
||||
safFileName = await _buildSafFileNameForItem(
|
||||
item,
|
||||
@@ -775,6 +780,7 @@ extension _DownloadQueueNativeWorker on DownloadQueueNotifier {
|
||||
label: extendedMetadata?.label,
|
||||
copyright: extendedMetadata?.copyright,
|
||||
stageSafForDeferredPublish: isSafMode,
|
||||
qualityVariantCollisionOnly: qualityVariantCollisionOnly,
|
||||
).withStrategy(useExtensions: true, useFallback: state.autoFallback);
|
||||
|
||||
return _NativeWorkerRequestContext(
|
||||
@@ -787,6 +793,7 @@ extension _DownloadQueueNativeWorker on DownloadQueueNotifier {
|
||||
downloadTreeUri: isSafMode ? settings.downloadTreeUri : null,
|
||||
safRelativeDir: isSafMode ? outputDir : null,
|
||||
safFileName: safFileName,
|
||||
qualityVariantCollisionOnly: qualityVariantCollisionOnly,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1177,6 +1184,7 @@ extension _DownloadQueueNativeWorker on DownloadQueueNotifier {
|
||||
downloadTreeUri: context.downloadTreeUri,
|
||||
safRelativeDir: context.safRelativeDir,
|
||||
fileName: result['file_name'] as String? ?? context.safFileName,
|
||||
collisionOnly: context.qualityVariantCollisionOnly,
|
||||
);
|
||||
filePath = variantOutcome.filePath;
|
||||
actualBitDepth = readPositiveInt(result['actual_bit_depth']);
|
||||
|
||||
@@ -100,6 +100,7 @@ class _DownloadRun {
|
||||
String effectiveOutputDir = '';
|
||||
String? appOutputDir;
|
||||
String? safFileName;
|
||||
bool qualityVariantCollisionOnly = false;
|
||||
String? safBaseName;
|
||||
String? finalSafFileName;
|
||||
|
||||
@@ -419,6 +420,9 @@ class _DownloadRun {
|
||||
item,
|
||||
baseFilenameFormat,
|
||||
);
|
||||
qualityVariantCollisionOnly =
|
||||
item.preserveQualityVariant &&
|
||||
!_explicitQualityFilenameTokenPattern.hasMatch(baseFilenameFormat);
|
||||
if (isSafMode) {
|
||||
final builtSafFileName = await n._buildSafFileNameForItem(
|
||||
item,
|
||||
@@ -549,6 +553,7 @@ class _DownloadRun {
|
||||
genre: genre,
|
||||
label: label,
|
||||
copyright: copyright,
|
||||
qualityVariantCollisionOnly: qualityVariantCollisionOnly,
|
||||
);
|
||||
|
||||
return PlatformBridge.downloadByStrategy(
|
||||
@@ -705,6 +710,7 @@ class _DownloadRun {
|
||||
downloadTreeUri: settings.downloadTreeUri,
|
||||
safRelativeDir: effectiveOutputDir,
|
||||
fileName: finalSafFileName ?? safFileName,
|
||||
collisionOnly: qualityVariantCollisionOnly,
|
||||
);
|
||||
filePath = variantOutcome.filePath;
|
||||
if (variantOutcome.fileName != null) {
|
||||
|
||||
@@ -50,6 +50,7 @@ class DownloadRequestPayload {
|
||||
final bool requiresContainerConversion;
|
||||
final bool allowQualityVariant;
|
||||
final String qualityVariant;
|
||||
final bool qualityVariantCollisionOnly;
|
||||
final String songLinkRegion;
|
||||
|
||||
const DownloadRequestPayload({
|
||||
@@ -102,6 +103,7 @@ class DownloadRequestPayload {
|
||||
this.requiresContainerConversion = false,
|
||||
this.allowQualityVariant = false,
|
||||
this.qualityVariant = '',
|
||||
this.qualityVariantCollisionOnly = false,
|
||||
this.songLinkRegion = 'US',
|
||||
});
|
||||
|
||||
@@ -156,6 +158,7 @@ class DownloadRequestPayload {
|
||||
'requires_container_conversion': requiresContainerConversion,
|
||||
'allow_quality_variant': allowQualityVariant,
|
||||
'quality_variant': qualityVariant,
|
||||
'quality_variant_collision_only': qualityVariantCollisionOnly,
|
||||
'songlink_region': songLinkRegion,
|
||||
};
|
||||
}
|
||||
@@ -214,6 +217,7 @@ class DownloadRequestPayload {
|
||||
requiresContainerConversion: requiresContainerConversion,
|
||||
allowQualityVariant: allowQualityVariant,
|
||||
qualityVariant: qualityVariant,
|
||||
qualityVariantCollisionOnly: qualityVariantCollisionOnly,
|
||||
songLinkRegion: songLinkRegion,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -678,6 +678,26 @@ class PlatformBridge {
|
||||
});
|
||||
}
|
||||
|
||||
static Future<Map<String, dynamic>> createCollisionAwareSafFileFromPath({
|
||||
required String treeUri,
|
||||
required String relativeDir,
|
||||
required String cleanFileName,
|
||||
required String variantFileName,
|
||||
required String mimeType,
|
||||
required String srcPath,
|
||||
String preservedSuffix = '',
|
||||
}) {
|
||||
return _invokeMap('safCreateCollisionAwareFromPath', {
|
||||
'tree_uri': treeUri,
|
||||
'relative_dir': relativeDir,
|
||||
'clean_file_name': cleanFileName,
|
||||
'variant_file_name': variantFileName,
|
||||
'mime_type': mimeType,
|
||||
'src_path': srcPath,
|
||||
'preserved_suffix': preservedSuffix,
|
||||
});
|
||||
}
|
||||
|
||||
static Future<void> openContentUri(String uri, {String mimeType = ''}) async {
|
||||
await _channel.invokeMethod('openContentUri', {
|
||||
'uri': uri,
|
||||
@@ -1069,9 +1089,10 @@ class PlatformBridge {
|
||||
static Future<Map<String, dynamic>> getNativeDownloadWorkerSnapshot({
|
||||
int sinceStateSerial = 0,
|
||||
}) async {
|
||||
final result = await _channel.invokeMethod('getNativeDownloadWorkerSnapshot', {
|
||||
'since_state_serial': sinceStateSerial,
|
||||
});
|
||||
final result = await _channel.invokeMethod(
|
||||
'getNativeDownloadWorkerSnapshot',
|
||||
{'since_state_serial': sinceStateSerial},
|
||||
);
|
||||
return _decodeMapResult(result);
|
||||
}
|
||||
|
||||
@@ -1212,9 +1233,7 @@ class PlatformBridge {
|
||||
});
|
||||
}
|
||||
|
||||
static Future<Map<String, dynamic>> loadExtensionsFromDir(
|
||||
String dirPath,
|
||||
) {
|
||||
static Future<Map<String, dynamic>> loadExtensionsFromDir(String dirPath) {
|
||||
_log.d('loadExtensionsFromDir: $dirPath');
|
||||
return _invokeMap('loadExtensionsFromDir', {'dir_path': dirPath});
|
||||
}
|
||||
@@ -1249,9 +1268,7 @@ class PlatformBridge {
|
||||
return _invokeMap('upgradeExtension', {'file_path': filePath});
|
||||
}
|
||||
|
||||
static Future<Map<String, dynamic>> checkExtensionUpgrade(
|
||||
String filePath,
|
||||
) {
|
||||
static Future<Map<String, dynamic>> checkExtensionUpgrade(String filePath) {
|
||||
_log.d('checkExtensionUpgrade: $filePath');
|
||||
return _invokeMap('checkExtensionUpgrade', {'file_path': filePath});
|
||||
}
|
||||
@@ -1311,15 +1328,11 @@ class PlatformBridge {
|
||||
return _decodeStringListResult(result, 'getMetadataProviderPriority');
|
||||
}
|
||||
|
||||
static Future<Map<String, dynamic>> getExtensionSettings(
|
||||
String extensionId,
|
||||
) {
|
||||
static Future<Map<String, dynamic>> getExtensionSettings(String extensionId) {
|
||||
return _invokeMap('getExtensionSettings', {'extension_id': extensionId});
|
||||
}
|
||||
|
||||
static Future<Map<String, dynamic>> checkExtensionHealth(
|
||||
String extensionId,
|
||||
) {
|
||||
static Future<Map<String, dynamic>> checkExtensionHealth(String extensionId) {
|
||||
return _invokeMap('checkExtensionHealth', {'extension_id': extensionId});
|
||||
}
|
||||
|
||||
|
||||
@@ -222,6 +222,44 @@ String applyQualityVariantFilenameLabel({
|
||||
return '$stem - $qualityLabel$extension';
|
||||
}
|
||||
|
||||
String removeQualityVariantStagingLabel({
|
||||
required String fileName,
|
||||
required String stagingLabel,
|
||||
}) {
|
||||
if (stagingLabel.isEmpty || !fileName.contains(stagingLabel)) {
|
||||
return fileName;
|
||||
}
|
||||
final dotIndex = fileName.lastIndexOf('.');
|
||||
final hasExtension = dotIndex > 0;
|
||||
final stem = hasExtension ? fileName.substring(0, dotIndex) : fileName;
|
||||
final extension = hasExtension ? fileName.substring(dotIndex) : '';
|
||||
final cleanedStem = stem
|
||||
.replaceAll(stagingLabel, '')
|
||||
.replaceFirst(RegExp(r'[\s_-]+$'), '')
|
||||
.trim();
|
||||
return '${cleanedStem.isEmpty ? 'track' : cleanedStem}$extension';
|
||||
}
|
||||
|
||||
String resolveQualityVariantFilename({
|
||||
required String fileName,
|
||||
required String stagingLabel,
|
||||
required String qualityLabel,
|
||||
required bool collisionOnly,
|
||||
required bool cleanNameExists,
|
||||
}) {
|
||||
if (!collisionOnly || cleanNameExists) {
|
||||
return applyQualityVariantFilenameLabel(
|
||||
fileName: fileName,
|
||||
stagingLabel: stagingLabel,
|
||||
qualityLabel: qualityLabel,
|
||||
);
|
||||
}
|
||||
return removeQualityVariantStagingLabel(
|
||||
fileName: fileName,
|
||||
stagingLabel: stagingLabel,
|
||||
);
|
||||
}
|
||||
|
||||
String lossyFormatForSetting(String value) {
|
||||
final normalized = value.trim().toLowerCase();
|
||||
if (normalized.startsWith('opus')) return 'opus';
|
||||
|
||||
Reference in New Issue
Block a user