// ignore_for_file: invalid_use_of_protected_member, invalid_use_of_visible_for_testing_member part of 'download_queue_provider.dart'; extension _DownloadQueuePaths on DownloadQueueNotifier { Directory _defaultDocumentsOutputDir(String documentsPath) { return Directory('$documentsPath/$_defaultOutputFolderName'); } Directory _defaultAndroidMusicOutputDir(String storageRootPath) { return Directory('$storageRootPath/$_defaultAndroidMusicSubpath'); } Future _ensureDefaultDocumentsOutputDir() async { final dir = await getApplicationDocumentsDirectory(); final musicDir = _defaultDocumentsOutputDir(dir.path); if (!await musicDir.exists()) { await musicDir.create(recursive: true); } return musicDir; } Future _ensureDefaultAndroidMusicOutputDir() async { final dir = await getExternalStorageDirectory(); if (dir == null) return null; final musicDir = _defaultAndroidMusicOutputDir( dir.parent.parent.parent.parent.path, ); if (!await musicDir.exists()) { await musicDir.create(recursive: true); } return musicDir; } Future _initOutputDir() async { if (state.outputDir.isEmpty) { try { if (Platform.isIOS) { final musicDir = await _ensureDefaultDocumentsOutputDir(); state = state.copyWith(outputDir: musicDir.path); } else { final musicDir = await _ensureDefaultAndroidMusicOutputDir() ?? await _ensureDefaultDocumentsOutputDir(); state = state.copyWith(outputDir: musicDir.path); } } catch (e) { final musicDir = await _ensureDefaultDocumentsOutputDir(); state = state.copyWith(outputDir: musicDir.path); } } } Future _ensureDirExists(String path, {String? label}) async { if (_ensuredDirs.contains(path)) return; final dir = Directory(path); if (!await dir.exists()) { await dir.create(recursive: true); if (label != null) { _log.d('Created $label: $path'); } else { _log.d('Created folder: $path'); } } _ensuredDirs.add(path); } bool _shouldTreatAsSingleRelease(Track track) { if (track.isSingle) { return true; } final normalizedAlbumType = normalizeOptionalString( track.albumType, )?.toLowerCase(); if (normalizedAlbumType != null && normalizedAlbumType.isNotEmpty) { return false; } final totalTracks = track.totalTracks; if (totalTracks == 1) { return true; } final normalizedAlbumName = normalizeOptionalString( track.albumName, )?.toLowerCase(); if (normalizedAlbumName == 'single' || normalizedAlbumName == 'singles') { return totalTracks == null || totalTracks <= 2; } return false; } Future _buildOutputDir( Track track, String folderOrganization, { bool separateSingles = false, String albumFolderStructure = 'artist_album', bool createPlaylistFolder = false, bool useAlbumArtistForFolders = true, bool usePrimaryArtistOnly = false, bool filterContributingArtistsInAlbumArtist = false, String? playlistName, }) async { final baseDir = state.outputDir; if (createPlaylistFolder && folderOrganization != 'playlist' && playlistName != null && playlistName.isNotEmpty) { final playlistFolder = _sanitizeFolderName(playlistName); if (playlistFolder.isNotEmpty) { await _ensureDirExists( '$baseDir${Platform.pathSeparator}$playlistFolder', label: 'Playlist folder', ); } } final relativeDir = await _buildRelativeOutputDir( track, folderOrganization, separateSingles: separateSingles, albumFolderStructure: albumFolderStructure, createPlaylistFolder: createPlaylistFolder, useAlbumArtistForFolders: useAlbumArtistForFolders, usePrimaryArtistOnly: usePrimaryArtistOnly, filterContributingArtistsInAlbumArtist: filterContributingArtistsInAlbumArtist, playlistName: playlistName, ); if (relativeDir.isEmpty) { return baseDir; } final fullPath = '$baseDir${Platform.pathSeparator}' '${relativeDir.replaceAll('/', Platform.pathSeparator)}'; await _ensureDirExists(fullPath); return fullPath; } String _sanitizeFolderName(String name) { final buffer = StringBuffer(); for (final rune in name.runes) { if (rune < 0x20 || rune == 0x7f) { continue; } final char = String.fromCharCode(rune); if (_invalidFolderChars.hasMatch(char)) { buffer.write(' '); continue; } buffer.write(char); } var sanitized = buffer.toString().trim(); sanitized = sanitized.replaceAll(_trimDotsAndSpacesRegex, ''); sanitized = sanitized.replaceAll(_multiWhitespaceRegex, ' '); sanitized = sanitized.replaceAll(_multiUnderscoreRegex, '_'); sanitized = sanitized.replaceAll(_trimUnderscoresAndSpacesRegex, ''); if (sanitized.isEmpty) { return 'Unknown'; } return sanitized; } String _truncateUtf8Bytes(String value, int maxBytes) { if (maxBytes <= 0 || utf8.encode(value).length <= maxBytes) { return value; } final buffer = StringBuffer(); var usedBytes = 0; for (final rune in value.runes) { final char = String.fromCharCode(rune); final charBytes = utf8.encode(char).length; if (usedBytes + charBytes > maxBytes) break; buffer.write(char); usedBytes += charBytes; } return buffer.toString(); } String _trimSafeName(String value) { var trimmed = value.trim(); trimmed = trimmed.replaceAll(_trimDotsAndSpacesRegex, ''); trimmed = trimmed.replaceAll(_trimUnderscoresAndSpacesRegex, ''); return trimmed.isEmpty ? 'Unknown' : trimmed; } String _sanitizeSafRelativeDir(String relativeDir) { if (relativeDir.trim().isEmpty) return ''; final parts = relativeDir .split('/') .map(_sanitizeFolderName) .map((part) { final truncated = _truncateUtf8Bytes( part, _maxSafDirSegmentUtf8Bytes, ); return _trimSafeName(truncated); }) .where((part) => part.isNotEmpty && part != '.' && part != '..') .toList(growable: false); return parts.join('/'); } Future _buildSafFileName(String baseName, String outputExt) async { final sanitized = await PlatformBridge.sanitizeFilename(baseName); final extBytes = utf8.encode(outputExt).length; final maxBaseBytes = max(1, _maxSafFilenameUtf8Bytes - extBytes); final truncated = _truncateUtf8Bytes(sanitized, maxBaseBytes); return '${_trimSafeName(truncated)}$outputExt'; } static final _featuredArtistPattern = RegExp( r'\s*[,;]\s*|\s+(?:feat\.?|ft\.?|featuring|with|x)\s+', caseSensitive: false, ); String _extractPrimaryArtist(String artist) { final match = _featuredArtistPattern.firstMatch(artist); if (match != null && match.start > 0) { return artist.substring(0, match.start).trim(); } return artist; } Future _buildRelativeOutputDir( Track track, String folderOrganization, { bool separateSingles = false, String albumFolderStructure = 'artist_album', bool createPlaylistFolder = false, bool useAlbumArtistForFolders = true, bool usePrimaryArtistOnly = false, bool filterContributingArtistsInAlbumArtist = false, String? playlistName, }) async { final playlistPrefix = createPlaylistFolder && folderOrganization != 'playlist' && playlistName != null && playlistName.isNotEmpty ? _sanitizeFolderName(playlistName) : ''; final normalizedAlbumArtist = normalizeOptionalString(track.albumArtist); var folderArtist = useAlbumArtistForFolders ? normalizedAlbumArtist ?? track.artistName : track.artistName; if (useAlbumArtistForFolders && filterContributingArtistsInAlbumArtist && normalizedAlbumArtist != null) { folderArtist = _extractPrimaryArtist(folderArtist); } if (usePrimaryArtistOnly) { folderArtist = _extractPrimaryArtist(folderArtist); } if (separateSingles) { final isSingle = _shouldTreatAsSingleRelease(track); final artistName = _sanitizeFolderName(folderArtist); if (albumFolderStructure == 'artist_album_singles') { if (isSingle) { return _joinRelativePath(playlistPrefix, '$artistName/Singles'); } final albumName = _sanitizeFolderName(track.albumName); return _joinRelativePath(playlistPrefix, '$artistName/$albumName'); } if (albumFolderStructure == 'artist_album_flat') { if (isSingle) { return _joinRelativePath(playlistPrefix, artistName); } final albumName = _sanitizeFolderName(track.albumName); return _joinRelativePath(playlistPrefix, '$artistName/$albumName'); } if (isSingle) { return _joinRelativePath(playlistPrefix, 'Singles'); } final albumName = _sanitizeFolderName(track.albumName); final year = _extractYear(track.releaseDate); switch (albumFolderStructure) { case 'album_only': return _joinRelativePath(playlistPrefix, 'Albums/$albumName'); case 'artist_year_album': final yearAlbum = year != null ? '[$year] $albumName' : albumName; return _joinRelativePath( playlistPrefix, 'Albums/$artistName/$yearAlbum', ); case 'year_album': final yearAlbum = year != null ? '[$year] $albumName' : albumName; return _joinRelativePath(playlistPrefix, 'Albums/$yearAlbum'); default: return _joinRelativePath( playlistPrefix, 'Albums/$artistName/$albumName', ); } } if (folderOrganization == 'none') { return playlistPrefix; } switch (folderOrganization) { case 'playlist': if (playlistName != null && playlistName.isNotEmpty) { return _sanitizeFolderName(playlistName); } return ''; case 'artist': return _joinRelativePath( playlistPrefix, _sanitizeFolderName(folderArtist), ); case 'album': return _joinRelativePath( playlistPrefix, _sanitizeFolderName(track.albumName), ); case 'artist_album': final artistName = _sanitizeFolderName(folderArtist); final albumName = _sanitizeFolderName(track.albumName); return _joinRelativePath(playlistPrefix, '$artistName/$albumName'); default: return playlistPrefix; } } String _joinRelativePath(String prefix, String suffix) { if (prefix.isEmpty) return suffix; if (suffix.isEmpty) return prefix; return '$prefix/$suffix'; } String? _extensionPreferredOutputExt(String service) { final normalizedService = service.trim().toLowerCase(); if (normalizedService.isEmpty) return null; final extensionState = ref.read(extensionProvider); for (final ext in extensionState.extensions) { if (!ext.enabled || !ext.hasDownloadProvider) continue; if (ext.id.toLowerCase() != normalizedService) continue; final preferred = ext.preferredDownloadOutputExtension; if (preferred == null) return null; final normalized = preferred.startsWith('.') ? preferred.toLowerCase() : '.${preferred.toLowerCase()}'; if (normalized == '.mp4') { return '.m4a'; } const allowed = {'.flac', '.m4a', '.mp3', '.opus'}; if (allowed.contains(normalized)) { return normalized; } return null; } return null; } bool _extensionPreservesNativeOutputExt(String service, String ext) { final normalizedService = service.trim().toLowerCase(); final normalizedExt = ext.trim().toLowerCase(); if (normalizedService.isEmpty || normalizedExt.isEmpty) return false; final extensionState = ref.read(extensionProvider); return extensionState.extensions.any( (ext) => ext.enabled && ext.hasDownloadProvider && ext.id.toLowerCase() == normalizedService && ext.preservedNativeOutputExtensions.contains(normalizedExt), ); } bool _extensionRequiresNativeContainerConversion(String service) { final normalizedService = service.trim().toLowerCase(); if (normalizedService.isEmpty) return false; final extensionState = ref.read(extensionProvider); return extensionState.extensions.any( (ext) => ext.enabled && ext.hasDownloadProvider && (ext.id.toLowerCase() == normalizedService || ext.replacesBuiltInProviders.contains(normalizedService)) && ext.requiresNativeContainerConversion, ); } bool _shouldRequestContainerConversion(String service, String outputExt) { return outputExt.trim().toLowerCase() == '.flac' && _extensionRequiresNativeContainerConversion(service); } String _determineOutputExt(String quality, String service) { final extensionPreferred = _extensionPreferredOutputExt(service); if (extensionPreferred != null) { return extensionPreferred; } if (_downloadProviderReplacesLegacyProvider(service, 'tidal') && quality == 'HIGH') { return '.m4a'; } final q = quality.toLowerCase(); if (q == 'alac' || q.startsWith('aac')) return '.m4a'; if (q.startsWith('opus')) return '.opus'; if (q.startsWith('mp3')) return '.mp3'; return '.flac'; } bool _downloadProviderReplacesLegacyProvider( String service, String legacyProviderId, ) { return ref .read(extensionProvider.notifier) .downloadProviderReplacesLegacyProvider(service, legacyProviderId); } String _mimeTypeForExt(String ext) { switch (ext.toLowerCase()) { case '.m4a': case '.mp4': return 'audio/mp4'; case '.mp3': return 'audio/mpeg'; case '.opus': return 'audio/ogg'; case '.flac': return 'audio/flac'; case '.lrc': return 'application/octet-stream'; default: return 'application/octet-stream'; } } String? _normalizeAudioExt(Object? value) { final raw = value?.toString().trim().toLowerCase(); if (raw == null || raw.isEmpty) return null; final normalized = raw.startsWith('.') ? raw : '.$raw'; const allowed = {'.flac', '.m4a', '.mp4', '.mp3', '.opus', '.ogg', '.aac'}; return allowed.contains(normalized) ? normalized : null; } String? _downloadResultOutputExt( Map result, { String? filePath, }) { final explicit = _normalizeAudioExt(result['actual_extension']) ?? _normalizeAudioExt(result['output_extension']) ?? _normalizeAudioExt(result['actual_container']) ?? _normalizeAudioExt(result['container']); if (explicit != null) return explicit; for (final candidate in [ result['file_name'] as String?, filePath, result['file_path'] as String?, ]) { if (candidate == null) continue; final lower = candidate.trim().toLowerCase(); for (final ext in const [ '.flac', '.m4a', '.mp4', '.mp3', '.opus', '.ogg', '.aac', ]) { if (lower.endsWith(ext)) return ext; } } // Generic safety net: when neither an explicit extension field nor a // recognizable path suffix is available (e.g. SAF content URIs that drop // the suffix), fall back to the actual audio codec reported by the backend // probe. This keeps any extension that returns a non-FLAC container (Opus, // MP3, AAC) from being mislabeled as FLAC. final codec = normalizeAudioFormatValue( result['audio_codec']?.toString() ?? result['actual_audio_codec']?.toString() ?? result['format']?.toString(), ); switch (codec) { case 'opus': return '.opus'; case 'mp3': return '.mp3'; case 'aac': case 'alac': case 'm4a': return '.m4a'; case 'flac': return '.flac'; } return null; } Future _getSafMimeType(String uri) async { try { final stat = await PlatformBridge.safStat(uri); return stat['mime_type'] as String?; } catch (_) { return null; } } String? _extractYear(String? releaseDate) { if (releaseDate == null || releaseDate.isEmpty) return null; final match = _yearRegex.firstMatch(releaseDate); return match?.group(1); } int _validPlaylistPosition(DownloadItem item) { final position = item.playlistPosition; if (position == null || position <= 0) return 0; return position; } String _filenameFormatForItem(DownloadItem item, String baseFormat) { if (!item.fromBatch) { return baseFormat; } final trimmed = baseFormat.trim(); if (trimmed.isEmpty) { return baseFormat; } if (_batchUniqueFilenameTokenPattern.hasMatch(trimmed)) { return baseFormat; } return '$trimmed - {track:02} - {title}'; } Map _filenameMetadataForTrack( Track track, { int playlistPosition = 0, }) { return { 'title': track.name, 'artist': track.artistName, 'album': track.albumName, 'track': track.trackNumber ?? 0, 'disc': track.discNumber ?? 0, 'year': _extractYear(track.releaseDate) ?? '', 'date': track.releaseDate ?? '', 'playlist_position': playlistPosition, 'playlistPosition': playlistPosition, }; } }