import 'package:spotiflac_android/utils/int_utils.dart'; import 'package:spotiflac_android/utils/string_utils.dart'; /// Audio format/quality helpers shared by the download queue and history /// providers. int? readPositiveBitrateKbps(dynamic value) { final parsed = readPositiveInt(value); if (parsed == null) return null; final kbps = parsed >= 10000 ? (parsed / 1000).round() : parsed; return kbps >= 16 ? kbps : null; } String? audioFormatForPath(String? filePath, {String? fileName}) { final candidates = [?filePath, ?fileName]; for (final candidate in candidates) { final lower = candidate.trim().toLowerCase(); if (lower.endsWith('.opus') || lower.endsWith('.ogg')) return 'OPUS'; if (lower.endsWith('.mp3')) return 'MP3'; if (lower.endsWith('.aac')) return 'AAC'; if (lower.endsWith('.m4a') || lower.endsWith('.mp4')) return 'M4A'; } return null; } String? nonPlaceholderQuality(String? quality) { final normalized = normalizeOptionalString(quality); if (normalized == null || isPlaceholderQualityLabel(normalized)) { return null; } final bitrateMatch = RegExp( r'\b(\d+)\s*kbps\b', caseSensitive: false, ).firstMatch(normalized); if (bitrateMatch != null) { final bitrate = int.tryParse(bitrateMatch.group(1) ?? ''); if (bitrate != null && bitrate < 16) return null; } final lower = normalized.toLowerCase().replaceAll(RegExp(r'[^a-z0-9]+'), '_'); const requestedLosslessLabels = { 'hi_res_lossless', 'hires_lossless', 'hi_res', 'hires', 'flac_best_available', }; if (requestedLosslessLabels.contains(lower)) return null; return normalized; } String? normalizeAudioFormatValue(String? value) { final normalized = normalizeOptionalString( value, )?.toLowerCase().replaceAll('-', '_'); return switch (normalized) { 'flac' => 'flac', 'alac' => 'alac', 'wav' || 'wave' => 'wav', 'aiff' || 'aif' || 'aifc' => 'aiff', 'aac' || 'mp4a' => 'aac', 'eac3' || 'ec_3' => 'eac3', 'ac3' || 'ac_3' => 'ac3', 'ac4' || 'ac_4' => 'ac4', 'mp3' => 'mp3', 'opus' || 'ogg' => 'opus', 'm4a' || 'mp4' => 'm4a', _ => null, }; } /// Resolves the actual audio codec reported by native metadata probing, while /// falling back to the container format when the codec is absent or generic. /// /// This distinction matters for MP4/M4A files: the same container may hold /// AAC, ALAC, Dolby, or other codecs. String? detectedAudioFormatFromMetadata(Map metadata) { final codec = normalizeAudioFormatValue(metadata['audio_codec']?.toString()); if (codec != null) return codec; return normalizeAudioFormatValue(metadata['format']?.toString()); } bool isLossyAudioFormat(String? value) { return const { 'aac', 'eac3', 'ac3', 'ac4', 'mp3', 'opus', 'm4a', }.contains(normalizeAudioFormatValue(value)); } /// Returns a provider-independent quality label suitable for a filename. /// /// Requested labels such as LOSSLESS and HI_RES are intentionally ignored: /// they describe provider intent, not the audio that was actually written. String? buildQualityVariantFilenameLabel({ String? detectedFormat, int? bitDepth, int? sampleRate, int? bitrateKbps, String? measuredQuality, }) { if (isLossyAudioFormat(detectedFormat)) { final effectiveBitrate = bitrateKbps ?? _bitrateFromQuality(measuredQuality); return effectiveBitrate != null && effectiveBitrate >= 16 ? '${effectiveBitrate}kbps' : null; } final measured = _losslessSpecsFromQuality(measuredQuality); final effectiveBitDepth = bitDepth ?? measured?.$1; final effectiveSampleRate = sampleRate ?? measured?.$2; if (effectiveBitDepth == null || effectiveBitDepth <= 0 || effectiveSampleRate == null || effectiveSampleRate <= 0) { return null; } return '${effectiveBitDepth}bit-${formatSampleRateKHz(effectiveSampleRate)}'; } int? _bitrateFromQuality(String? quality) { final match = RegExp( r'\b(\d+)\s*kbps\b', caseSensitive: false, ).firstMatch(quality ?? ''); final value = int.tryParse(match?.group(1) ?? ''); return value != null && value >= 16 ? value : null; } (int, int)? _losslessSpecsFromQuality(String? quality) { final match = RegExp( r'\b(\d+)\s*(?:-|\s)?bit\s*[/_-]\s*(\d+(?:\.\d+)?)\s*k?hz\b', caseSensitive: false, ).firstMatch(quality ?? ''); final bitDepth = int.tryParse(match?.group(1) ?? ''); final rate = double.tryParse(match?.group(2) ?? ''); if (bitDepth == null || bitDepth <= 0 || rate == null || rate <= 0) { return null; } final sampleRate = rate < 1000 ? (rate * 1000).round() : rate.round(); return (bitDepth, sampleRate); } String qualityVariantStagingLabel(String itemId) { var hash = 0x811c9dc5; for (final byte in itemId.codeUnits) { hash ^= byte; hash = (hash * 0x01000193) & 0xffffffff; } return 'qv_${hash.toRadixString(16).padLeft(8, '0')}'; } String applyQualityVariantFilenameLabel({ required String fileName, required String stagingLabel, required String qualityLabel, }) { if (stagingLabel.isNotEmpty && fileName.contains(stagingLabel)) { return fileName.replaceAll(stagingLabel, qualityLabel); } if (fileName.contains(qualityLabel)) { return fileName; } final dotIndex = fileName.lastIndexOf('.'); final hasExtension = dotIndex > 0; final stem = hasExtension ? fileName.substring(0, dotIndex) : fileName; final extension = hasExtension ? fileName.substring(dotIndex) : ''; return '$stem - $qualityLabel$extension'; } String lossyFormatForSetting(String value) { final normalized = value.trim().toLowerCase(); if (normalized.startsWith('opus')) return 'opus'; if (normalized.startsWith('aac') || normalized.startsWith('m4a')) { return 'aac'; } return 'mp3'; } String lossyExtensionForFormat(String format) { return switch (format) { 'opus' => '.opus', 'aac' => '.m4a', _ => '.mp3', }; } String metadataFormatForLossyFormat(String format) { return format == 'aac' ? 'm4a' : format; } String displayFormatForLossyFormat(String format) { return format == 'aac' ? 'AAC' : format.toUpperCase(); } String? resolveDisplayQuality({ required String? filePath, String? fileName, String? detectedFormat, int? bitDepth, int? sampleRate, int? bitrateKbps, String? storedQuality, }) { final format = displayFormatForCodec(detectedFormat) ?? audioFormatForPath(filePath, fileName: fileName); if (format == 'OPUS' || format == 'MP3' || format == 'AAC' || format == 'EAC3' || format == 'AC3' || format == 'AC4' || (format == 'M4A' && (bitDepth == null || bitDepth <= 0))) { return buildDisplayAudioQuality(bitrateKbps: bitrateKbps, format: format) ?? nonPlaceholderQuality(storedQuality) ?? format; } return buildDisplayAudioQuality( bitDepth: bitDepth, sampleRate: sampleRate, storedQuality: nonPlaceholderQuality(storedQuality) ?? storedQuality, ); } String? displayFormatForCodec(String? value) { final normalized = normalizeOptionalString( value, )?.toLowerCase().replaceAll('-', '_'); return switch (normalized) { 'flac' => 'FLAC', 'alac' => 'ALAC', 'aac' || 'mp4a' => 'AAC', 'eac3' || 'ec_3' => 'EAC3', 'ac3' || 'ac_3' => 'AC3', 'ac4' || 'ac_4' => 'AC4', 'mp3' => 'MP3', 'opus' => 'OPUS', _ => null, }; }