diff --git a/go_backend/metadata.go b/go_backend/metadata.go index 9fdd02d2..e026aa33 100644 --- a/go_backend/metadata.go +++ b/go_backend/metadata.go @@ -33,7 +33,6 @@ func EmbedMetadata(filePath string, metadata Metadata, coverPath string) error { return fmt.Errorf("failed to parse FLAC file: %w", err) } - // Find or create vorbis comment block var cmtIdx int = -1 var cmt *flacvorbis.MetaDataBlockVorbisComment @@ -123,7 +122,6 @@ func EmbedMetadata(filePath string, metadata Metadata, coverPath string) error { } } - // Save file return f.Save(filePath) } @@ -403,7 +401,6 @@ func GetAudioQuality(filePath string) (AudioQuality, error) { } defer file.Close() - // Read first 4 bytes to detect file type marker := make([]byte, 4) if _, err := file.Read(marker); err != nil { return AudioQuality{}, fmt.Errorf("failed to read marker: %w", err) @@ -429,13 +426,10 @@ func GetAudioQuality(filePath string) (AudioQuality, error) { return AudioQuality{}, fmt.Errorf("failed to read STREAMINFO: %w", err) } - // Parse sample rate (20 bits starting at byte 10) sampleRate := (int(streamInfo[10]) << 12) | (int(streamInfo[11]) << 4) | (int(streamInfo[12]) >> 4) - // Parse bits per sample (5 bits) bitsPerSample := ((int(streamInfo[12]) & 0x01) << 4) | (int(streamInfo[13]) >> 4) + 1 - // Parse total samples (36 bits: 4 bits from byte 13, all of bytes 14-17) totalSamples := int64(streamInfo[13]&0x0F)<<32 | int64(streamInfo[14])<<24 | int64(streamInfo[15])<<16 | @@ -449,17 +443,14 @@ func GetAudioQuality(filePath string) (AudioQuality, error) { }, nil } - // Check if it's an M4A/MP4 file (starts with size + "ftyp") - // First 4 bytes are size, next 4 should be "ftyp" - file.Seek(0, 0) // Reset to beginning + file.Seek(0, 0) header8 := make([]byte, 8) if _, err := file.Read(header8); err != nil { return AudioQuality{}, fmt.Errorf("failed to read header: %w", err) } if string(header8[4:8]) == "ftyp" { - // It's an M4A/MP4 file, use M4A quality reader - file.Close() // Close before calling GetM4AQuality which opens the file again + file.Close() return GetM4AQuality(filePath) } @@ -471,9 +462,7 @@ func GetAudioQuality(filePath string) (AudioQuality, error) { // ======================================== // EmbedM4AMetadata embeds metadata into an M4A file using iTunes-style atoms -// This is a simplified implementation that writes metadata to the file func EmbedM4AMetadata(filePath string, metadata Metadata, coverData []byte) error { - // Read the entire file data, err := os.ReadFile(filePath) if err != nil { return fmt.Errorf("failed to read M4A file: %w", err) @@ -485,11 +474,9 @@ func EmbedM4AMetadata(filePath string, metadata Metadata, coverData []byte) erro return fmt.Errorf("moov atom not found in M4A file") } - // Find udta atom inside moov, or create one moovSize := int(uint32(data[moovPos])<<24 | uint32(data[moovPos+1])<<16 | uint32(data[moovPos+2])<<8 | uint32(data[moovPos+3])) udtaPos := findAtom(data, "udta", moovPos+8) - // Build new metadata atoms metaAtom := buildMetaAtom(metadata, coverData) var newData []byte @@ -499,13 +486,11 @@ func EmbedM4AMetadata(filePath string, metadata Metadata, coverData []byte) erro metaPos := findAtom(data, "meta", udtaPos+8) if metaPos >= 0 && metaPos < udtaPos+udtaSize { - // Replace existing meta atom metaSize := int(uint32(data[metaPos])<<24 | uint32(data[metaPos+1])<<16 | uint32(data[metaPos+2])<<8 | uint32(data[metaPos+3])) newData = append(newData, data[:metaPos]...) newData = append(newData, metaAtom...) newData = append(newData, data[metaPos+metaSize:]...) } else { - // Add meta atom to udta newUdtaContent := append(data[udtaPos+8:udtaPos+udtaSize], metaAtom...) newUdtaSize := 8 + len(newUdtaContent) newUdta := make([]byte, 4) @@ -521,7 +506,6 @@ func EmbedM4AMetadata(filePath string, metadata Metadata, coverData []byte) erro newData = append(newData, data[udtaPos+udtaSize:]...) } } else { - // Create new udta with meta udtaContent := metaAtom udtaSize := 8 + len(udtaContent) newUdta := make([]byte, 4) @@ -532,7 +516,6 @@ func EmbedM4AMetadata(filePath string, metadata Metadata, coverData []byte) erro newUdta = append(newUdta, []byte("udta")...) newUdta = append(newUdta, udtaContent...) - // Insert udta at end of moov insertPos := moovPos + moovSize newData = append(newData, data[:insertPos]...) newData = append(newData, newUdta...) @@ -546,7 +529,6 @@ func EmbedM4AMetadata(filePath string, metadata Metadata, coverData []byte) erro newData[moovPos+2] = byte(newMoovSize >> 8) newData[moovPos+3] = byte(newMoovSize) - // Write back to file if err := os.WriteFile(filePath, newData, 0644); err != nil { return fmt.Errorf("failed to write M4A file: %w", err) } @@ -573,7 +555,6 @@ func findAtom(data []byte, name string, offset int) int { // buildMetaAtom builds a complete meta atom with ilst containing metadata func buildMetaAtom(metadata Metadata, coverData []byte) []byte { - // Build ilst content var ilst []byte // ©nam - Title @@ -631,7 +612,6 @@ func buildMetaAtom(metadata Metadata, coverData []byte) []byte { ilstAtom = append(ilstAtom, []byte("ilst")...) ilstAtom = append(ilstAtom, ilst...) - // Build hdlr atom (required for meta) hdlr := []byte{ 0, 0, 0, 33, // size = 33 'h', 'd', 'l', 'r', @@ -788,18 +768,13 @@ func GetM4AQuality(filePath string) (AudioQuality, error) { return AudioQuality{}, fmt.Errorf("moov atom not found") } - // Search for mp4a or alac atom which contains audio info - // This is a simplified search - real implementation would traverse the atom tree for i := moovPos; i < len(data)-20; i++ { if string(data[i:i+4]) == "mp4a" || string(data[i:i+4]) == "alac" { - // Sample rate is at offset 22-23 from atom start (16-bit big-endian) if i+24 < len(data) { sampleRate := int(data[i+22])<<8 | int(data[i+23]) - // For AAC, bit depth is typically 16 bitDepth := 16 if string(data[i:i+4]) == "alac" { - // ALAC can have higher bit depth, check esds or alac specific data - bitDepth = 24 // Assume 24-bit for ALAC + bitDepth = 24 } return AudioQuality{BitDepth: bitDepth, SampleRate: sampleRate}, nil } diff --git a/go_backend/tidal.go b/go_backend/tidal.go index d34e5c2a..abb299e6 100644 --- a/go_backend/tidal.go +++ b/go_backend/tidal.go @@ -130,16 +130,14 @@ func NewTidalDownloader() *TidalDownloader { // GetAvailableAPIs returns list of available Tidal APIs func (t *TidalDownloader) GetAvailableAPIs() []string { encodedAPIs := []string{ - // Priority 1: APIs that return FULL tracks (not PREVIEW) - "dGlkYWwua2lub3BsdXMub25saW5l", // tidal.kinoplus.online - returns FULL - "dGlkYWwtYXBpLmJpbmltdW0ub3Jn", // tidal-api.binimum.org - "dHJpdG9uLnNxdWlkLnd0Zg==", // triton.squid.wtf - // Priority 2: qqdl.site APIs (often return PREVIEW only) - "dm9nZWwucXFkbC5zaXRl", // vogel.qqdl.site - "bWF1cy5xcWRsLnNpdGU=", // maus.qqdl.site - "aHVuZC5xcWRsLnNpdGU=", // hund.qqdl.site - "a2F0emUucXFkbC5zaXRl", // katze.qqdl.site - "d29sZi5xcWRsLnNpdGU=", // wolf.qqdl.site + "dGlkYWwua2lub3BsdXMub25saW5l", + "dGlkYWwtYXBpLmJpbmltdW0ub3Jn", + "dHJpdG9uLnNxdWlkLnd0Zg==", + "dm9nZWwucXFkbC5zaXRl", + "bWF1cy5xcWRsLnNpdGU=", + "aHVuZC5xcWRsLnNpdGU=", + "a2F0emUucXFkbC5zaXRl", + "d29sZi5xcWRsLnNpdGU=", } var apis []string @@ -159,7 +157,6 @@ func (t *TidalDownloader) GetAccessToken() (string, error) { t.tokenMu.Lock() defer t.tokenMu.Unlock() - // Return cached token if still valid (with 60s buffer) if t.cachedToken != "" && time.Now().Add(60*time.Second).Before(t.tokenExpiresAt) { return t.cachedToken, nil } @@ -385,22 +382,17 @@ func (t *TidalDownloader) SearchTrackByMetadataWithISRC(trackName, artistName, s queries = append(queries, artistName+" "+trackName) } - // Strategy 2: Track name only if trackName != "" { queries = append(queries, trackName) } - // Strategy 3: Romaji versions if Japanese detected (NEW - from PC version) if ContainsJapanese(trackName) || ContainsJapanese(artistName) { - // Convert to romaji (hiragana/katakana only, kanji stays) romajiTrack := JapaneseToRomaji(trackName) romajiArtist := JapaneseToRomaji(artistName) - // Clean and remove ALL non-ASCII characters (including kanji) cleanRomajiTrack := CleanToASCII(romajiTrack) cleanRomajiArtist := CleanToASCII(romajiArtist) - // Artist + Track romaji (cleaned to ASCII only) if cleanRomajiArtist != "" && cleanRomajiTrack != "" { romajiQuery := cleanRomajiArtist + " " + cleanRomajiTrack if !containsQuery(queries, romajiQuery) { @@ -409,14 +401,12 @@ func (t *TidalDownloader) SearchTrackByMetadataWithISRC(trackName, artistName, s } } - // Track romaji only (cleaned) if cleanRomajiTrack != "" && cleanRomajiTrack != trackName { if !containsQuery(queries, cleanRomajiTrack) { queries = append(queries, cleanRomajiTrack) } } - // Also try with partial romaji (artist + cleaned track) if artistName != "" && cleanRomajiTrack != "" { partialQuery := artistName + " " + cleanRomajiTrack if !containsQuery(queries, partialQuery) { @@ -425,7 +415,6 @@ func (t *TidalDownloader) SearchTrackByMetadataWithISRC(trackName, artistName, s } } - // Strategy 4: Artist only as last resort if artistName != "" { artistOnly := CleanToASCII(JapaneseToRomaji(artistName)) if artistOnly != "" && !containsQuery(queries, artistOnly) { @@ -435,7 +424,6 @@ func (t *TidalDownloader) SearchTrackByMetadataWithISRC(trackName, artistName, s searchBase, _ := base64.StdEncoding.DecodeString("aHR0cHM6Ly9hcGkudGlkYWwuY29tL3YxL3NlYXJjaC90cmFja3M/cXVlcnk9") - // Collect all search results from all queries var allTracks []TidalTrack searchedQueries := make(map[string]bool) @@ -485,7 +473,6 @@ func (t *TidalDownloader) SearchTrackByMetadataWithISRC(trackName, artistName, s for i := range result.Items { if result.Items[i].ISRC == spotifyISRC { track := &result.Items[i] - // Verify duration if provided if expectedDuration > 0 { durationDiff := track.Duration - expectedDuration if durationDiff < 0 { @@ -495,7 +482,6 @@ func (t *TidalDownloader) SearchTrackByMetadataWithISRC(trackName, artistName, s GoLog("[Tidal] ✓ ISRC match: '%s' (duration verified)\n", track.Title) return track, nil } - // Duration mismatch, continue searching GoLog("[Tidal] ISRC match but duration mismatch (expected %ds, got %ds), continuing...\n", expectedDuration, track.Duration) } else { @@ -514,7 +500,6 @@ func (t *TidalDownloader) SearchTrackByMetadataWithISRC(trackName, artistName, s return nil, fmt.Errorf("no tracks found for any search query") } - // Priority 1: Match by ISRC (exact match) WITH title verification if spotifyISRC != "" { GoLog("[Tidal] Looking for ISRC match: %s\n", spotifyISRC) var isrcMatches []*TidalTrack @@ -526,7 +511,6 @@ func (t *TidalDownloader) SearchTrackByMetadataWithISRC(trackName, artistName, s } if len(isrcMatches) > 0 { - // Verify duration first (most important check) if expectedDuration > 0 { var durationVerifiedMatches []*TidalTrack for _, track := range isrcMatches { @@ -534,37 +518,31 @@ func (t *TidalDownloader) SearchTrackByMetadataWithISRC(trackName, artistName, s if durationDiff < 0 { durationDiff = -durationDiff } - // Allow 3 seconds tolerance for duration (same as PC version) if durationDiff <= 3 { durationVerifiedMatches = append(durationVerifiedMatches, track) } } if len(durationVerifiedMatches) > 0 { - // Return first duration-verified match GoLog("[Tidal] ✓ ISRC match with duration verification: '%s' (expected %ds, found %ds)\n", durationVerifiedMatches[0].Title, expectedDuration, durationVerifiedMatches[0].Duration) return durationVerifiedMatches[0], nil } - // ISRC matches but duration doesn't - this is likely wrong version GoLog("[Tidal] WARNING: ISRC %s found but duration mismatch. Expected=%ds, Found=%ds. Rejecting.\n", spotifyISRC, expectedDuration, isrcMatches[0].Duration) return nil, fmt.Errorf("ISRC found but duration mismatch: expected %ds, found %ds (likely different version/edit)", expectedDuration, isrcMatches[0].Duration) } - // No duration to verify, just return first ISRC match GoLog("[Tidal] ✓ ISRC match (no duration verification): '%s'\n", isrcMatches[0].Title) return isrcMatches[0], nil } - // If ISRC was provided but no match found, return error GoLog("[Tidal] ✗ No ISRC match found for: %s\n", spotifyISRC) return nil, fmt.Errorf("ISRC mismatch: no track found with ISRC %s on Tidal", spotifyISRC) } - // Priority 2: Match by duration (within tolerance) + prefer best quality if expectedDuration > 0 { tolerance := 3 // 3 seconds tolerance var durationMatches []*TidalTrack @@ -581,7 +559,6 @@ func (t *TidalDownloader) SearchTrackByMetadataWithISRC(trackName, artistName, s } if len(durationMatches) > 0 { - // Find best quality among duration matches bestMatch := durationMatches[0] for _, track := range durationMatches { for _, tag := range track.MediaMetadata.Tags { @@ -597,7 +574,6 @@ func (t *TidalDownloader) SearchTrackByMetadataWithISRC(trackName, artistName, s } } - // Priority 3: Just take the best quality from first results bestMatch := &allTracks[0] for i := range allTracks { track := &allTracks[i] diff --git a/lib/providers/download_queue_provider.dart b/lib/providers/download_queue_provider.dart index d7a32bac..ab2293a9 100644 --- a/lib/providers/download_queue_provider.dart +++ b/lib/providers/download_queue_provider.dart @@ -26,7 +26,6 @@ String? _normalizeOptionalString(String? value) { return trimmed; } -// Download History Item model class DownloadHistoryItem { final String id; final String trackName; @@ -37,7 +36,6 @@ class DownloadHistoryItem { final String filePath; final String service; final DateTime downloadedAt; - // Additional metadata final String? isrc; final String? spotifyId; final int? trackNumber; @@ -113,7 +111,6 @@ class DownloadHistoryItem { ); } -// Download History State class DownloadHistoryState { final List items; final Set _downloadedSpotifyIds; // Cache for O(1) lookup @@ -133,7 +130,6 @@ class DownloadHistoryState { } } -// Download History Notifier (Riverpod 3.x) class DownloadHistoryNotifier extends Notifier { static const _storageKey = 'download_history'; bool _isLoaded = false; @@ -208,7 +204,6 @@ class DownloadHistoryNotifier extends Notifier { _historyLog.d('Skipping duplicate: ${item.trackName} (key: $key)'); } } else { - // No identifier - keep it (can't deduplicate) result.add(item); } } @@ -240,7 +235,6 @@ class DownloadHistoryNotifier extends Notifier { return true; } - // Match Deezer tracks: extract numeric ID from "deezer:123456" format if (item.spotifyId != null && item.spotifyId!.startsWith('deezer:') && existing.spotifyId != null && existing.spotifyId!.startsWith('deezer:')) { final itemDeezerId = item.spotifyId!.substring(7); @@ -259,10 +253,8 @@ class DownloadHistoryNotifier extends Notifier { }); if (existingIndex >= 0) { - // Replace existing entry (update with new download info) final updatedItems = [...state.items]; updatedItems[existingIndex] = item; - // Move to top of list (most recent) updatedItems.removeAt(existingIndex); updatedItems.insert(0, item); state = state.copyWith(items: updatedItems); @@ -301,7 +293,6 @@ class DownloadHistoryNotifier extends Notifier { } } -// Download History Provider final downloadHistoryProvider = NotifierProvider( DownloadHistoryNotifier.new, @@ -369,7 +360,6 @@ class DownloadQueueState { items.where((i) => i.status == DownloadStatus.downloading).length; } -// Download Queue Notifier (Riverpod 3.x) class DownloadQueueNotifier extends Notifier { Timer? _progressTimer; int _downloadCount = 0; // Counter for connection cleanup @@ -384,7 +374,6 @@ class DownloadQueueNotifier extends Notifier { @override DownloadQueueState build() { - // Cleanup timer when provider is disposed ref.onDispose(() { _progressTimer?.cancel(); _progressTimer = null; @@ -411,7 +400,6 @@ class DownloadQueueNotifier extends Notifier { .map((e) => DownloadItem.fromJson(e as Map)) .toList(); - // Reset downloading items to queued (they were interrupted) final restoredItems = items.map((item) { if (item.status == DownloadStatus.downloading) { return item.copyWith(status: DownloadStatus.queued, progress: 0); @@ -527,10 +515,8 @@ class DownloadQueueNotifier extends Notifier { if (isDownloading) { double percentage = 0.0; if (bytesTotal > 0) { - // Calculate from bytes if available for precision percentage = bytesReceived / bytesTotal; } else { - // Fallback to backend-reported progress (e.g. for DASH segments) percentage = progressFromBackend; } @@ -558,14 +544,12 @@ class DownloadQueueNotifier extends Notifier { return; // Don't show download progress notification } - // Update notification with active downloads if (items.isNotEmpty) { final firstEntry = items.entries.first; final firstProgress = firstEntry.value as Map; final bytesReceived = firstProgress['bytes_received'] as int? ?? 0; final bytesTotal = firstProgress['bytes_total'] as int? ?? 0; - // Find downloading items (not finalizing) final downloadingItems = state.items .where((i) => i.status == DownloadStatus.downloading) .toList(); @@ -627,7 +611,6 @@ class DownloadQueueNotifier extends Notifier { } state = state.copyWith(outputDir: musicDir.path); } else { - // Android: Use external storage Music folder final dir = await getExternalStorageDirectory(); if (dir != null) { final musicDir = Directory( @@ -685,11 +668,9 @@ class DownloadQueueNotifier extends Notifier { switch (albumFolderStructure) { case 'album_only': - // Albums/Album structure (no artist folder) albumPath = '$baseDir${Platform.pathSeparator}Albums${Platform.pathSeparator}$albumName'; break; case 'artist_year_album': - // Albums/Artist/[Year] Album structure final yearAlbum = year != null ? '[$year] $albumName' : albumName; albumPath = '$baseDir${Platform.pathSeparator}Albums${Platform.pathSeparator}$artistName${Platform.pathSeparator}$yearAlbum'; break; @@ -710,7 +691,6 @@ class DownloadQueueNotifier extends Notifier { } } - // Original folder organization logic (when separateSingles is disabled) if (folderOrganization == 'none') { return baseDir; } @@ -756,7 +736,6 @@ class DownloadQueueNotifier extends Notifier { /// Extract year from release date (format: "2005-06-13" or "2005") String? _extractYear(String? releaseDate) { if (releaseDate == null || releaseDate.isEmpty) return null; - // Handle both "2005-06-13" and "2005" formats final match = RegExp(r'^(\d{4})').firstMatch(releaseDate); return match?.group(1); } @@ -774,7 +753,6 @@ class DownloadQueueNotifier extends Notifier { } String addToQueue(Track track, String service, {String? qualityOverride}) { - // Sync settings before adding to queue final settings = ref.read(settingsProvider); updateSettings(settings); @@ -789,10 +767,9 @@ class DownloadQueueNotifier extends Notifier { ); state = state.copyWith(items: [...state.items, item]); - _saveQueueToStorage(); // Persist queue + _saveQueueToStorage(); if (!state.isProcessing) { - // Run in microtask to not block UI Future.microtask(() => _processQueue()); } @@ -804,7 +781,6 @@ class DownloadQueueNotifier extends Notifier { String service, { String? qualityOverride, }) { - // Sync settings before adding to queue final settings = ref.read(settingsProvider); updateSettings(settings); @@ -824,7 +800,6 @@ class DownloadQueueNotifier extends Notifier { _saveQueueToStorage(); // Persist queue if (!state.isProcessing) { - // Run in microtask to not block UI Future.microtask(() => _processQueue()); } } @@ -854,7 +829,6 @@ class DownloadQueueNotifier extends Notifier { state = state.copyWith(items: items); - // Persist queue when status changes to completed/failed/skipped (item removed from pending) if (status == DownloadStatus.completed || status == DownloadStatus.failed || status == DownloadStatus.skipped) { @@ -940,7 +914,6 @@ class DownloadQueueNotifier extends Notifier { return; } - // Only retry if status is failed or skipped if (item.status != DownloadStatus.failed && item.status != DownloadStatus.skipped) { _log.w('retryItem: Item status is ${item.status}, not retrying'); @@ -983,7 +956,6 @@ class DownloadQueueNotifier extends Notifier { final settings = ref.read(settingsProvider); final extensionState = ref.read(extensionProvider); - // Check if post-processing is enabled and there are extensions with hooks if (!settings.useExtensionProviders) return; final hasPostProcessing = extensionState.extensions.any( @@ -993,7 +965,6 @@ class DownloadQueueNotifier extends Notifier { _log.d('Running post-processing hooks on: $filePath'); - // Build metadata map for post-processing final metadata = { 'title': track.name, 'artist': track.artistName, @@ -1023,7 +994,6 @@ class DownloadQueueNotifier extends Notifier { } } catch (e) { _log.w('Post-processing error: $e'); - // Don't fail the download if post-processing fails } } @@ -1032,15 +1002,13 @@ class DownloadQueueNotifier extends Notifier { String _upgradeToMaxQualityCover(String coverUrl) { const spotifySize300 = 'ab67616d00001e02'; // 300x300 (small) const spotifySize640 = 'ab67616d0000b273'; // 640x640 (medium) - const spotifySizeMax = 'ab67616d000082c1'; // Max resolution (~2000x2000) + const spotifySizeMax = 'ab67616d000082c1'; - // First upgrade small (300) to medium (640) var result = coverUrl; if (result.contains(spotifySize300)) { result = result.replaceFirst(spotifySize300, spotifySize640); } - // Then upgrade medium (640) to max if (result.contains(spotifySize640)) { result = result.replaceFirst(spotifySize640, spotifySizeMax); } @@ -1052,7 +1020,6 @@ class DownloadQueueNotifier extends Notifier { Future _embedMetadataAndCover(String flacPath, Track track) async { final settings = ref.read(settingsProvider); - // Download cover first String? coverPath; var coverUrl = track.coverUrl; if (coverUrl != null && coverUrl.isNotEmpty) { @@ -1119,9 +1086,6 @@ class DownloadQueueNotifier extends Notifier { _log.d('Metadata map content: $metadata'); - // Fetch Lyrics (Critical for M4A->FLAC conversion parity) - // Since we are in the Flutter context, we can call the bridge to get lyrics - // This ensures even converted files have lyrics embedded if available try { final lrcContent = await PlatformBridge.getLyricsLRC( track.id, // spotifyID @@ -1141,8 +1105,6 @@ class DownloadQueueNotifier extends Notifier { _log.d('Generating tags for FLAC: $metadata'); - // Perform embedding (cover + text metadata) - // Note: FFmpegService.embedMetadata handles safe temp file creation final result = await FFmpegService.embedMetadata( flacPath: flacPath, coverPath: coverPath != null && await File(coverPath).exists() @@ -1157,14 +1119,10 @@ class DownloadQueueNotifier extends Notifier { _log.w('FFmpeg metadata/cover embed failed'); } - // Clean up cover file if it exists if (coverPath != null) { try { final coverFile = File(coverPath); if (await coverFile.exists()) { - // In Android 10+ scoped storage, we can't easily delete if we didn't create it - // in this session or if it's not in our app dir. - // But coverPath is typically in temp dir now. await coverFile.delete(); } } catch (_) {} @@ -1180,14 +1138,12 @@ class DownloadQueueNotifier extends Notifier { state = state.copyWith(isProcessing: true); _log.i('Starting queue processing...'); - // Track total items at start for notification _totalQueuedAtStart = state.items .where((i) => i.status == DownloadStatus.queued) .length; _completedInSession = 0; _failedInSession = 0; - // Start foreground service to keep downloads running in background (Android only) if (Platform.isAndroid && _totalQueuedAtStart > 0) { final firstItem = state.items.firstWhere( (item) => item.status == DownloadStatus.queued, @@ -1205,13 +1161,11 @@ class DownloadQueueNotifier extends Notifier { } } - // Ensure output directory is initialized before processing if (state.outputDir.isEmpty) { _log.d('Output dir empty, initializing...'); await _initOutputDir(); } - // If still empty, use fallback if (state.outputDir.isEmpty) { _log.d('Using fallback directory...'); final dir = await getApplicationDocumentsDirectory(); @@ -1225,7 +1179,6 @@ class DownloadQueueNotifier extends Notifier { _log.d('Output directory: ${state.outputDir}'); _log.d('Concurrent downloads: ${state.concurrentDownloads}'); - // Use parallel processing if concurrentDownloads > 1 if (state.concurrentDownloads > 1) { await _processQueueParallel(); } else { @@ -1234,7 +1187,6 @@ class DownloadQueueNotifier extends Notifier { _stopProgressPolling(); - // Stop foreground service (Android only) if (Platform.isAndroid) { try { await PlatformBridge.stopDownloadService(); @@ -1244,7 +1196,6 @@ class DownloadQueueNotifier extends Notifier { } } - // Final cleanup after queue finishes if (_downloadCount > 0) { _log.d('Final connection cleanup...'); try { @@ -1255,7 +1206,6 @@ class DownloadQueueNotifier extends Notifier { _downloadCount = 0; } - // Show queue completion notification _log.i( 'Queue stats - completed: $_completedInSession, failed: $_failedInSession, totalAtStart: $_totalQueuedAtStart', ); @@ -1269,7 +1219,6 @@ class DownloadQueueNotifier extends Notifier { _log.i('Queue processing finished'); state = state.copyWith(isProcessing: false, currentDownload: null); - // Check if there are new queued items (e.g., from retry) and restart if needed final hasQueuedItems = state.items.any( (item) => item.status == DownloadStatus.queued, ); @@ -1283,18 +1232,15 @@ class DownloadQueueNotifier extends Notifier { /// Sequential download processing (uses multi-progress system with single item) Future _processQueueSequential() async { - // Start multi-progress polling (works for both sequential and parallel) _startMultiProgressPolling(); while (true) { - // Check if paused if (state.isPaused) { _log.d('Queue is paused, waiting...'); await Future.delayed(const Duration(milliseconds: 500)); continue; } - // Re-read state to get latest items (important for retry) final currentItems = state.items; final nextItem = currentItems.firstWhere( (item) => item.status == DownloadStatus.queued, @@ -1324,11 +1270,9 @@ class DownloadQueueNotifier extends Notifier { ); await _downloadSingleItem(nextItem); - // Clear item progress after download completes PlatformBridge.clearItemProgress(nextItem.id).catchError((_) {}); } - // Stop polling when queue is done _stopProgressPolling(); } @@ -1337,11 +1281,9 @@ class DownloadQueueNotifier extends Notifier { final maxConcurrent = state.concurrentDownloads; final activeDownloads = >{}; // Map item ID to future - // Start multi-progress polling (shared with sequential mode) _startMultiProgressPolling(); while (true) { - // Check if paused - don't start new downloads but let active ones finish if (state.isPaused) { _log.d('Queue is paused, waiting for active downloads...'); if (activeDownloads.isNotEmpty) { @@ -1352,7 +1294,6 @@ class DownloadQueueNotifier extends Notifier { continue; } - // Get queued items final queuedItems = state.items .where((item) => item.status == DownloadStatus.queued) .toList(); @@ -1362,19 +1303,15 @@ class DownloadQueueNotifier extends Notifier { break; } - // Start new downloads up to max concurrent limit while (activeDownloads.length < maxConcurrent && queuedItems.isNotEmpty && !state.isPaused) { final item = queuedItems.removeAt(0); - // Mark as downloading immediately to prevent double-processing updateItemStatus(item.id, DownloadStatus.downloading); - // Create the download future final future = _downloadSingleItem(item).whenComplete(() { activeDownloads.remove(item.id); - // Clear item progress after download completes PlatformBridge.clearItemProgress(item.id).catchError((_) {}); }); @@ -1384,18 +1321,15 @@ class DownloadQueueNotifier extends Notifier { ); } - // Wait for at least one download to complete before checking for more if (activeDownloads.isNotEmpty) { await Future.any(activeDownloads.values); } } - // Wait for all remaining downloads to complete if (activeDownloads.isNotEmpty) { await Future.wait(activeDownloads.values); } - // Stop polling when queue is done _stopProgressPolling(); } @@ -1419,15 +1353,9 @@ class DownloadQueueNotifier extends Notifier { updateItemStatus(item.id, DownloadStatus.downloading); try { - // Get folder organization setting and build output directory final settings = ref.read(settingsProvider); - // Metadata Enrichment: - // If track number is missing/0 (common from Search results), fetch full metadata - // This ensures the downloaded file has correct tags (Track, Disc, Year) Track trackToDownload = item.track; - // Enrich metadata if ISRC or track number is missing (common from Search results) - // ISRC is critical for accurate track matching on streaming services final needsEnrichment = trackToDownload.id.startsWith('deezer:') && (trackToDownload.isrc == null || @@ -1452,7 +1380,6 @@ class DownloadQueueNotifier extends Notifier { _log.d('Got response keys: ${fullData.keys.toList()}'); if (fullData.containsKey('track')) { - // Parse Go backend response (snake_case) to Track final trackData = fullData['track']; _log.d('Track data type: ${trackData.runtimeType}'); if (trackData is Map) { @@ -1500,7 +1427,6 @@ class DownloadQueueNotifier extends Notifier { } } - // Log cover URL for debugging CSV import issues _log.d('Track coverUrl after enrichment: ${trackToDownload.coverUrl}'); final normalizedAlbumArtist = @@ -1518,7 +1444,6 @@ class DownloadQueueNotifier extends Notifier { Map result; - // Check if extension providers should be used final extensionState = ref.read(extensionProvider); final hasActiveExtensions = extensionState.extensions.any((e) => e.enabled); final useExtensions = settings.useExtensionProviders && hasActiveExtensions; @@ -1597,7 +1522,6 @@ class DownloadQueueNotifier extends Notifier { _log.d('Result: $result'); - // Check if item was cancelled while downloading final currentItem = state.items.firstWhere( (i) => i.id == item.id, orElse: () => item, @@ -1623,14 +1547,12 @@ class DownloadQueueNotifier extends Notifier { if (result['success'] == true) { var filePath = result['file_path'] as String?; - // Strip EXISTS: prefix from duplicate detection if (filePath != null && filePath.startsWith('EXISTS:')) { filePath = filePath.substring(7); // Remove "EXISTS:" prefix } _log.i('Download success, file: $filePath'); - // Get actual quality from response (if available) final actualBitDepth = result['actual_bit_depth'] as int?; final actualSampleRate = result['actual_sample_rate'] as int?; String actualQuality = quality; // Default to requested quality @@ -1646,7 +1568,6 @@ class DownloadQueueNotifier extends Notifier { _log.i('Actual quality: $actualQuality'); } - // M4A files from Tidal DASH streams - try to convert to FLAC if (filePath != null && filePath.endsWith('.m4a')) { _log.d( 'M4A file detected (Hi-Res DASH stream), attempting conversion to FLAC...', @@ -1676,11 +1597,8 @@ class DownloadQueueNotifier extends Notifier { filePath = flacPath; _log.d('Converted to FLAC: $flacPath'); - // After conversion, embed metadata and cover to the new FLAC file _log.d('Embedding metadata and cover to converted FLAC...'); try { - // Update track with actual metadata from backend result (if available) - // This creates the most accurate metadata possible (from the service itself) Track finalTrack = trackToDownload; if (result.containsKey('track_number') || result.containsKey('release_date')) { @@ -1742,18 +1660,15 @@ class DownloadQueueNotifier extends Notifier { } } catch (e) { _log.w('FFmpeg conversion process failed: $e, keeping M4A file'); - // Keep the M4A file if conversion fails } } - // Check again if cancelled before updating status and adding to history final itemAfterDownload = state.items.firstWhere( (i) => i.id == item.id, orElse: () => item, ); if (itemAfterDownload.status == DownloadStatus.skipped) { _log.i('Download was cancelled during finalization, cleaning up'); - // Delete the downloaded file if (filePath != null) { try { final file = File(filePath); @@ -1775,15 +1690,12 @@ class DownloadQueueNotifier extends Notifier { filePath: filePath, ); - // Run post-processing hooks if enabled if (filePath != null) { await _runPostProcessingHooks(filePath, trackToDownload); } - // Increment completed counter _completedInSession++; - - // Show completion notification for this track + await _notificationService.showDownloadComplete( trackName: item.track.name, artistName: item.track.artistName, @@ -1792,7 +1704,6 @@ class DownloadQueueNotifier extends Notifier { ); if (filePath != null) { - // Extract metadata from backend result (most accurate source) final backendTitle = result['title'] as String?; final backendArtist = result['artist'] as String?; final backendAlbum = result['album'] as String?; @@ -1851,7 +1762,6 @@ class DownloadQueueNotifier extends Notifier { ), ); - // Auto-remove completed item from queue (it's now in history) removeItem(item.id); } } else { @@ -1901,7 +1811,6 @@ class DownloadQueueNotifier extends Notifier { _failedInSession++; } - // Increment download counter and cleanup connections periodically _downloadCount++; if (_downloadCount % _cleanupInterval == 0) { _log.d( @@ -1928,7 +1837,6 @@ class DownloadQueueNotifier extends Notifier { String errorMsg = e.toString(); DownloadErrorType errorType = DownloadErrorType.unknown; - // Check for specific Deezer fallback error if (errorMsg.contains('could not find Deezer equivalent') || errorMsg.contains('track not found on Deezer')) { errorMsg = 'Track not found on Deezer (Metadata Unavailable)'; diff --git a/lib/providers/recent_access_provider.dart b/lib/providers/recent_access_provider.dart index 9d383cb3..aad77455 100644 --- a/lib/providers/recent_access_provider.dart +++ b/lib/providers/recent_access_provider.dart @@ -200,11 +200,9 @@ class RecentAccessNotifier extends Notifier { } void _recordAccess(RecentAccessItem item) { - // Debug log // ignore: avoid_print print('[RecentAccess] Recording: ${item.type.name} - ${item.name} (${item.id})'); - // Remove any existing entry with same unique key final updatedItems = state.items .where((e) => e.uniqueKey != item.uniqueKey) .toList(); diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart index 1983293f..a5dd74c1 100644 --- a/lib/providers/settings_provider.dart +++ b/lib/providers/settings_provider.dart @@ -53,7 +53,6 @@ class SettingsNotifier extends Notifier { /// Apply current Spotify credentials to Go backend Future _applySpotifyCredentials() async { - // Only apply if both fields are set if (state.spotifyClientId.isNotEmpty && state.spotifyClientSecret.isNotEmpty) { await PlatformBridge.setSpotifyCredentials( @@ -197,7 +196,6 @@ class SettingsNotifier extends Notifier { void setEnableLogging(bool enabled) { state = state.copyWith(enableLogging: enabled); _saveSettings(); - // Sync logging state to LogBuffer LogBuffer.loggingEnabled = enabled; } diff --git a/lib/providers/track_provider.dart b/lib/providers/track_provider.dart index 49f65bf6..1f03f7a4 100644 --- a/lib/providers/track_provider.dart +++ b/lib/providers/track_provider.dart @@ -257,7 +257,6 @@ class TrackNotifier extends Notifier { playlistName: owner?['name'] as String?, coverUrl: owner?['images'] as String?, ); - // Pre-warm cache for playlist tracks in background _preWarmCacheForTracks(tracks); } else if (type == 'artist') { final artistInfo = metadata['artist_info'] as Map; @@ -279,7 +278,6 @@ class TrackNotifier extends Notifier { } Future search(String query, {String? metadataSource}) async { - // Increment request ID to cancel any pending requests final requestId = ++_currentRequestId; // Preserve hasSearchText during search @@ -345,10 +343,8 @@ class TrackNotifier extends Notifier { _log.d('Raw results: ${trackList.length} tracks, ${artistList.length} artists'); - // Parse tracks with error handling per item final tracks = []; - // Add extension tracks first (they have priority) tracks.addAll(extensionTracks); final existingIsrcs = extensionTracks @@ -404,7 +400,6 @@ class TrackNotifier extends Notifier { /// Perform custom search using a specific extension Future customSearch(String extensionId, String query, {Map? options}) async { - // Increment request ID to cancel any pending requests final requestId = ++_currentRequestId; // Preserve hasSearchText during search @@ -484,7 +479,6 @@ class TrackNotifier extends Notifier { tracks[index] = updatedTrack; state = state.copyWith(tracks: tracks); } catch (e) { - // Silently fail availability check } } @@ -536,7 +530,6 @@ class TrackNotifier extends Notifier { } Track _parseSearchTrack(Map data, {String? source}) { - // Handle duration_ms which might be int or double int durationMs = 0; final durationValue = data['duration_ms']; if (durationValue is int) { @@ -591,11 +584,9 @@ class TrackNotifier extends Notifier { /// Pre-warm track ID cache for faster downloads /// Runs in background, doesn't block UI void _preWarmCacheForTracks(List tracks) { - // Only pre-warm if we have tracks with ISRC final tracksWithIsrc = tracks.where((t) => t.isrc != null && t.isrc!.isNotEmpty).toList(); if (tracksWithIsrc.isEmpty) return; - // Build request list for Go backend final cacheRequests = tracksWithIsrc.map((t) => { 'isrc': t.isrc!, 'track_name': t.name, diff --git a/lib/screens/queue_tab.dart b/lib/screens/queue_tab.dart index 7714515e..34560419 100644 --- a/lib/screens/queue_tab.dart +++ b/lib/screens/queue_tab.dart @@ -296,7 +296,6 @@ class _QueueTabState extends ConsumerState { switch (filterMode) { case 'albums': - // Album = more than 1 track from same album in history return items.where((item) { final key = '${item.albumName}|${item.albumArtist ?? item.artistName}'; @@ -379,7 +378,6 @@ class _QueueTabState extends ConsumerState { albumKeys.add(key); } - // Count albums with more than 1 track int count = 0; for (final key in albumKeys) { final trackCount = items @@ -412,7 +410,6 @@ class _QueueTabState extends ConsumerState { @override Widget build(BuildContext context) { - // Initialize page controller on first build _initializePageController(); final queueItems = ref.watch(downloadQueueProvider.select((s) => s.items)); @@ -490,7 +487,6 @@ class _QueueTabState extends ConsumerState { ), ), - // Pause/Resume controls if ((isProcessing || queuedCount > 0) && (queueItems.length > 1 || isPaused)) SliverToBoxAdapter( @@ -536,10 +532,9 @@ class _QueueTabState extends ConsumerState { ), ), ), - ), ), + ), - // Queue header if (queueItems.isNotEmpty) SliverToBoxAdapter( child: Padding( @@ -550,10 +545,9 @@ class _QueueTabState extends ConsumerState { fontWeight: FontWeight.bold, ), ), - ), ), + ), - // Queue list if (queueItems.isNotEmpty) SliverList( delegate: SliverChildBuilderDelegate((context, index) { @@ -618,7 +612,6 @@ class _QueueTabState extends ConsumerState { if (notification is OverscrollNotification) { final overscroll = notification.overscroll; - // At first page and overscrolling to the left -> push parent toward Home if (page == 0 && overscroll < 0) { final currentOffset = parentController.offset; final targetOffset = (currentOffset + overscroll).clamp( @@ -629,7 +622,6 @@ class _QueueTabState extends ConsumerState { return true; } - // At last page and overscrolling to the right -> push parent toward next tab if (page == 2 && overscroll > 0) { final currentOffset = parentController.offset; final targetOffset = (currentOffset + overscroll).clamp( @@ -641,32 +633,26 @@ class _QueueTabState extends ConsumerState { } } - // Snap parent to nearest page when scroll ends if (notification is ScrollEndNotification) { if (page == 0 || page == 2) { final currentPage = parentController.page ?? widget.parentPageIndex.toDouble(); final historyPage = widget.parentPageIndex.toDouble(); final offset = currentPage - historyPage; - // Only snap if we've moved the parent if (offset.abs() > 0.01) { - // Use 0.3 threshold (30%) if (offset < -0.3) { - // Swiped enough toward Home - animate to Home parentController.animateToPage( widget.parentPageIndex - 1, duration: const Duration(milliseconds: 250), curve: Curves.easeOutCubic, ); } else if (offset > 0.3) { - // Swiped enough toward next tab - animate to next parentController.animateToPage( widget.nextPageIndex ?? (widget.parentPageIndex + 1), duration: const Duration(milliseconds: 250), curve: Curves.easeOutCubic, ); } else { - // Not enough - instant jump back (no animation) parentController.jumpToPage(widget.parentPageIndex); } } @@ -680,7 +666,6 @@ class _QueueTabState extends ConsumerState { physics: const ClampingScrollPhysics(), onPageChanged: _onFilterPageChanged, children: [ - // All tab _buildFilterContent( context: context, colorScheme: colorScheme, @@ -690,7 +675,6 @@ class _QueueTabState extends ConsumerState { queueItems: queueItems, groupedAlbums: groupedAlbums, ), - // Albums tab _buildFilterContent( context: context, colorScheme: colorScheme, @@ -700,7 +684,6 @@ class _QueueTabState extends ConsumerState { queueItems: queueItems, groupedAlbums: groupedAlbums, ), - // Singles tab _buildFilterContent( context: context, colorScheme: colorScheme, @@ -715,7 +698,6 @@ class _QueueTabState extends ConsumerState { ), ), - // Bottom Selection Action Bar AnimatedPositioned( duration: const Duration(milliseconds: 250), curve: Curves.easeOutCubic, @@ -748,7 +730,6 @@ class _QueueTabState extends ConsumerState { return CustomScrollView( slivers: [ - // History section header if (historyItems.isNotEmpty && queueItems.isEmpty && filterMode != 'albums') @@ -779,7 +760,6 @@ class _QueueTabState extends ConsumerState { ), ), - // Albums section header (when Albums filter is selected) if (groupedAlbums.isNotEmpty && queueItems.isEmpty && filterMode == 'albums') @@ -795,7 +775,6 @@ class _QueueTabState extends ConsumerState { ), ), - // History section header when queue has items if (historyItems.isNotEmpty && queueItems.isNotEmpty) SliverToBoxAdapter( child: Padding( @@ -831,7 +810,6 @@ class _QueueTabState extends ConsumerState { ), ), - // History - Grid or List (for All and Singles filter) if (historyItems.isNotEmpty && filterMode != 'albums') historyViewMode == 'grid' ? SliverPadding( @@ -871,10 +849,9 @@ class _QueueTabState extends ConsumerState { colorScheme, ), ); - }, childCount: historyItems.length), - ), + }, childCount: historyItems.length ), + ), - // Empty state if (queueItems.isEmpty && historyItems.isEmpty && (filterMode != 'albums' || groupedAlbums.isEmpty)) @@ -887,7 +864,6 @@ class _QueueTabState extends ConsumerState { ), ) else - // Add bottom padding when selection mode is active to avoid overlap with bottom bar SliverToBoxAdapter( child: SizedBox(height: _isSelectionMode ? 100 : 16), ), @@ -956,7 +932,6 @@ class _QueueTabState extends ConsumerState { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Album cover with track count badge Expanded( child: Stack( children: [ @@ -982,7 +957,6 @@ class _QueueTabState extends ConsumerState { ), ), ), - // Track count badge Positioned( right: 8, bottom: 8, @@ -1020,16 +994,14 @@ class _QueueTabState extends ConsumerState { ), ), const SizedBox(height: 8), - // Album name Text( album.albumName, maxLines: 1, overflow: TextOverflow.ellipsis, style: Theme.of( context, - ).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600), + ).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600 ), ), - // Artist name Text( album.artistName, maxLines: 1, @@ -1084,10 +1056,8 @@ class _QueueTabState extends ConsumerState { ), ), - // Selection info row Row( children: [ - // Close button IconButton.filledTonal( onPressed: _exitSelectionMode, icon: const Icon(Icons.close), @@ -1141,7 +1111,6 @@ class _QueueTabState extends ConsumerState { const SizedBox(height: 16), - // Delete button SizedBox( width: double.infinity, child: FilledButton.icon( @@ -1449,7 +1418,6 @@ class _QueueTabState extends ConsumerState { ), ), ), - // Quality badge if (item.quality != null && item.quality!.contains('bit')) Positioned( left: 4, @@ -1478,7 +1446,6 @@ class _QueueTabState extends ConsumerState { ), ), ), - // Play button if (fileExists && !_isSelectionMode) Positioned( right: 4, @@ -1499,7 +1466,6 @@ class _QueueTabState extends ConsumerState { ), ), ), - // Error indicator if (!fileExists && !_isSelectionMode) Positioned( right: 4, @@ -1517,7 +1483,6 @@ class _QueueTabState extends ConsumerState { ), ), ), - // Selection overlay if (_isSelectionMode) Positioned.fill( child: Container( @@ -1550,7 +1515,6 @@ class _QueueTabState extends ConsumerState { ), ], ), - // Selection checkbox if (_isSelectionMode) Positioned( right: 4, @@ -1618,7 +1582,6 @@ class _QueueTabState extends ConsumerState { padding: const EdgeInsets.all(12), child: Row( children: [ - // Selection checkbox if (_isSelectionMode) ...[ Container( width: 24, @@ -1645,7 +1608,6 @@ class _QueueTabState extends ConsumerState { ), const SizedBox(width: 12), ], - // Cover art item.coverUrl != null ? ClipRRect( borderRadius: BorderRadius.circular(8), @@ -1672,7 +1634,6 @@ class _QueueTabState extends ConsumerState { ), const SizedBox(width: 12), - // Track info Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -1740,7 +1701,6 @@ class _QueueTabState extends ConsumerState { ), const SizedBox(width: 8), - // Action buttons (hide in selection mode) if (!_isSelectionMode) Row( mainAxisSize: MainAxisSize.min, diff --git a/lib/screens/settings/about_page.dart b/lib/screens/settings/about_page.dart index 966a25ed..83e2f401 100644 --- a/lib/screens/settings/about_page.dart +++ b/lib/screens/settings/about_page.dart @@ -51,7 +51,6 @@ class AboutPage extends StatelessWidget { ), ), - // App header card with logo and description SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.fromLTRB(16, 16, 16, 8), @@ -208,7 +207,6 @@ class AboutPage extends StatelessWidget { ), ), - // Bottom padding const SliverToBoxAdapter(child: SizedBox(height: 16)), ], ), @@ -240,8 +238,6 @@ class _AppHeaderCard extends StatelessWidget { padding: const EdgeInsets.all(24), child: Column( children: [ - // App logo - // App logo Container( width: 88, height: 88, diff --git a/lib/screens/settings/appearance_settings_page.dart b/lib/screens/settings/appearance_settings_page.dart index c92e0d83..08476175 100644 --- a/lib/screens/settings/appearance_settings_page.dart +++ b/lib/screens/settings/appearance_settings_page.dart @@ -38,7 +38,6 @@ class AppearanceSettingsPage extends ConsumerWidget { ), ), - // Preview Section SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.symmetric( @@ -211,7 +210,6 @@ class _ThemePreviewCard extends StatelessWidget { ), child: Row( children: [ - // Fake Album Art Container( width: 108, height: 108, @@ -627,7 +625,6 @@ class _ViewModeChip extends StatelessWidget { final colorScheme = Theme.of(context).colorScheme; final isDark = Theme.of(context).brightness == Brightness.dark; - // Unselected chips need contrast with card background final unselectedColor = isDark ? Color.alphaBlend( Colors.white.withValues(alpha: 0.05), diff --git a/lib/screens/settings/log_screen.dart b/lib/screens/settings/log_screen.dart index 7598b338..efe95a08 100644 --- a/lib/screens/settings/log_screen.dart +++ b/lib/screens/settings/log_screen.dart @@ -211,7 +211,6 @@ class _LogScreenState extends State { SliverToBoxAdapter( child: SettingsGroup( children: [ - // Level filter Padding( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), child: Row( @@ -309,7 +308,6 @@ class _LogScreenState extends State { ), ), - // Log entries section SliverToBoxAdapter( child: SettingsSectionHeader( title: _selectedLevel != 'ALL' || _searchQuery.isNotEmpty @@ -628,7 +626,6 @@ class _LogSummaryCard extends StatelessWidget { final errorLower = (log.error ?? '').toLowerCase(); final combined = '$msgLower $errorLower'; - // Check for ISP blocking (detected by Go backend) if (combined.contains('isp blocking') || combined.contains('isp may be') || combined.contains('blocked by isp') || @@ -642,7 +639,6 @@ class _LogSummaryCard extends StatelessWidget { } } - // Check for rate limiting if (combined.contains('rate limit') || combined.contains('429') || combined.contains('too many requests')) { diff --git a/lib/screens/setup_screen.dart b/lib/screens/setup_screen.dart index c95e9f59..30c89e2c 100644 --- a/lib/screens/setup_screen.dart +++ b/lib/screens/setup_screen.dart @@ -76,7 +76,6 @@ class _SetupScreenState extends ConsumerState { debugPrint('[Permission] Android 11-12 check: MANAGE_EXTERNAL_STORAGE=$manageStatus'); storageGranted = manageStatus.isGranted; } else { - // Android 10 and below: Use legacy storage permission final storageStatus = await Permission.storage.status; debugPrint('[Permission] Android 10- check: STORAGE=$storageStatus'); storageGranted = storageStatus.isGranted; @@ -183,7 +182,6 @@ class _SetupScreenState extends ConsumerState { allGranted = manageStatus.isGranted; } else { - // Android 10 and below: Use legacy storage permission final status = await Permission.storage.request(); allGranted = status.isGranted; @@ -920,7 +918,6 @@ class _SetupScreenState extends ConsumerState { ), const SizedBox(height: 16), - // Info banner Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( diff --git a/lib/screens/store_tab.dart b/lib/screens/store_tab.dart index d603d9f0..9053f546 100644 --- a/lib/screens/store_tab.dart +++ b/lib/screens/store_tab.dart @@ -508,7 +508,7 @@ class _ExtensionItem extends StatelessWidget { child: Text( extension.displayName, style: Theme.of(context).textTheme.bodyLarge - ?.copyWith(fontWeight: FontWeight.w500 ), + ?.copyWith(fontWeight: FontWeight.w500), ), ), Container( diff --git a/lib/screens/track_metadata_screen.dart b/lib/screens/track_metadata_screen.dart index 8f47e4bc..39adcfec 100644 --- a/lib/screens/track_metadata_screen.dart +++ b/lib/screens/track_metadata_screen.dart @@ -290,7 +290,6 @@ class _TrackMetadataScreenState extends ConsumerState { ], ), - // File status if (!fileExists) ...[ const SizedBox(height: 12), Container( @@ -806,7 +805,6 @@ class _TrackMetadataScreenState extends ConsumerState { } String _cleanLrcForDisplay(String lrc) { - // Remove LRC timestamps [mm:ss.xx] for cleaner display final lines = lrc.split('\n'); final cleanLines = []; final timestampPattern = RegExp(r'^\[\d{2}:\d{2}\.\d{2,3}\]'); diff --git a/lib/services/csv_import_service.dart b/lib/services/csv_import_service.dart index 0a0f8aa7..e8090b4e 100644 --- a/lib/services/csv_import_service.dart +++ b/lib/services/csv_import_service.dart @@ -50,7 +50,6 @@ class CsvImportService { if (track.coverUrl == null || track.duration == 0) { Map? trackData; - // Try ISRC first if available if (track.isrc != null && track.isrc!.isNotEmpty) { try { trackData = await PlatformBridge.searchDeezerByISRC(track.isrc!); @@ -112,7 +111,6 @@ class CsvImportService { _log.d('Enriched: ${track.name} - cover: ${coverUrl != null}, duration: ${durationMs ~/ 1000}s'); - // Small delay to avoid rate limiting if (i < tracks.length - 1) { await Future.delayed(const Duration(milliseconds: 100)); } @@ -147,7 +145,6 @@ class CsvImportService { _log.d('CSV Headers: ${colMap.keys.toList()}'); - // Parse rows for (int i = startIdx + 1; i < lines.length; i++) { final line = lines[i].trim(); if (line.isEmpty) continue; @@ -161,10 +158,9 @@ class CsvImportService { String? trackName = getVal(['track name', 'track', 'name', 'title']); String? artistName = getVal(['artist name', 'artist']); String? albumName = getVal(['album name', 'album']); - String? isrc = getVal(['isrc']); // Often formatted with leading/trailing quotes - String? spotifyId = getVal(['spotify - id', 'spotify id', 'id', 'uri']); // Uri might need parsing + String? isrc = getVal(['isrc']); + String? spotifyId = getVal(['spotify - id', 'spotify id', 'id', 'uri']); - // If 'spotify uri' contains the id: 'spotify:track:ID' if (spotifyId != null && spotifyId.startsWith('spotify:track:')) { spotifyId = spotifyId.replaceAll('spotify:track:', ''); } @@ -207,23 +203,17 @@ class CsvImportService { return val; } - // Robust CSV Line Parser static List _parseLine(String line) { final List result = []; bool inQuote = false; StringBuffer buffer = StringBuffer(); for (int i=0; i Thumb "Up" - // My _cleanValue handles it, so I should just preserve raw content here mostly, - // BUT I need to know if " toggles inQuote. - // Escaped "" does NOT toggle inQuote mode effectively (it counts as literal char inside quote). - buffer.write('"'); // Write 1st quote + String char = line[i]; + if (char == '"') { + if (i + 1 < line.length && line[i+1] == '"') { + buffer.write('"'); + buffer.write('"'); i++; // Skip next quote char loop buffer.write('"'); // Write 2nd quote } else { diff --git a/lib/services/ffmpeg_service.dart b/lib/services/ffmpeg_service.dart index c0cbbf35..d1fdbc55 100644 --- a/lib/services/ffmpeg_service.dart +++ b/lib/services/ffmpeg_service.dart @@ -57,7 +57,6 @@ class FFmpegService { inputPath.split(Platform.pathSeparator).last.replaceAll('.flac', ''); final outputDir = '$dir${Platform.pathSeparator}MP3'; - // Create output directory await Directory(outputDir).create(recursive: true); final outputPath = '$outputDir${Platform.pathSeparator}$baseName.mp3'; @@ -175,18 +174,14 @@ class FFmpegService { if (result.success) { try { - // Copy temp output back to original location (replace) final tempFile = File(tempOutput); final originalFile = File(flacPath); if (await tempFile.exists()) { - // Delete original file if (await originalFile.exists()) { await originalFile.delete(); } - // Copy temp file to original location await tempFile.copy(flacPath); - // Delete temp file await tempFile.delete(); return flacPath; diff --git a/lib/services/share_intent_service.dart b/lib/services/share_intent_service.dart index 6b900bc9..257e057c 100644 --- a/lib/services/share_intent_service.dart +++ b/lib/services/share_intent_service.dart @@ -38,7 +38,6 @@ class ShareIntentService { final initialMedia = await ReceiveSharingIntent.instance.getInitialMedia(); if (initialMedia.isNotEmpty) { _handleSharedMedia(initialMedia, isInitial: true); - // Tell the library that we are done processing the intent ReceiveSharingIntent.instance.reset(); } }