import 'package:spotiflac_android/providers/download_history_provider.dart'; import 'package:spotiflac_android/services/history_database.dart'; import 'package:spotiflac_android/services/platform_bridge.dart'; import 'package:spotiflac_android/utils/audio_conversion_utils.dart'; class ConversionLibraryService { const ConversionLibraryService._(); static Future _retryTransientSaf( Future Function() operation, ) async { Object? lastError; for (var attempt = 0; attempt < 2; attempt++) { try { final result = await operation(); if (result != null) return result; } catch (error) { lastError = error; } if (attempt == 0) { await Future.delayed(const Duration(milliseconds: 250)); } } if (lastError != null) throw lastError; return null; } static Future copySafSourceToTemp(String uri) { return _retryTransientSaf(() => PlatformBridge.copyContentUriToTemp(uri)); } static Future<({String uri, String fileName})?> publishSafConversion({ required String treeUri, required String relativeDir, required String originalFileName, required String targetFormat, required String sourcePath, required bool keepOriginal, }) async { final target = convertTargetExtAndMime(targetFormat); final requestedName = convertedOutputFileName( originalFileName: originalFileName, targetFormat: targetFormat, ); final replacesSameNamedDocument = !keepOriginal && requestedName.toLowerCase() == originalFileName.toLowerCase(); if (!replacesSameNamedDocument) { final result = await _retryTransientSaf( () => PlatformBridge.createUniqueSafFileFromPath( treeUri: treeUri, relativeDir: relativeDir, fileName: requestedName, mimeType: target.mime, srcPath: sourcePath, ), ); if (result == null) return null; final uri = (result['uri'] as String? ?? '').trim(); final fileName = (result['file_name'] as String? ?? '').trim(); if (uri.isEmpty || fileName.isEmpty) return null; return (uri: uri, fileName: fileName); } final uri = await _retryTransientSaf( () => PlatformBridge.createSafFileFromPath( treeUri: treeUri, relativeDir: relativeDir, fileName: requestedName, mimeType: target.mime, srcPath: sourcePath, ), ); if (uri == null || uri.trim().isEmpty) return null; return (uri: uri, fileName: requestedName); } static Future persistHistoryConversion({ required DownloadHistoryItem source, required String newFilePath, required String newQuality, required String targetFormat, required String bitrate, required int? bitDepth, required int? sampleRate, required bool keepOriginal, String? newSafFileName, }) async { final convertedBitrate = convertedAudioBitrateKbps( targetFormat: targetFormat, bitrate: bitrate, ); final isLossless = isLosslessConversionTarget(targetFormat); if (!keepOriginal) { await HistoryDatabase.instance.updateFilePath( source.id, newFilePath, newSafFileName: newSafFileName, newQuality: newQuality, newFormat: normalizedConvertedAudioFormat(targetFormat), newBitrate: convertedBitrate, newBitDepth: bitDepth, newSampleRate: sampleRate, clearAudioSpecs: !isLossless, ); return; } final converted = source.toJson() ..['id'] = convertedLibraryItemId(source.id, newFilePath) ..['filePath'] = newFilePath ..['downloadedAt'] = DateTime.now().toIso8601String() ..['quality'] = newQuality ..['format'] = normalizedConvertedAudioFormat(targetFormat) ..['bitrate'] = convertedBitrate ..['bitDepth'] = isLossless ? bitDepth : null ..['sampleRate'] = isLossless ? sampleRate : null; if (newSafFileName != null) { converted['safFileName'] = newSafFileName; } await HistoryDatabase.instance.upsert(converted); } }