diff --git a/.cursorignore b/.cursorignore
new file mode 100644
index 00000000..6f9f00ff
--- /dev/null
+++ b/.cursorignore
@@ -0,0 +1 @@
+# Add directories or file patterns to ignore during indexing (e.g. foo/ or *.csv)
diff --git a/go_backend/metadata.go b/go_backend/metadata.go
index 3001730e..9fdd02d2 100644
--- a/go_backend/metadata.go
+++ b/go_backend/metadata.go
@@ -92,14 +92,12 @@ func EmbedMetadata(filePath string, metadata Metadata, coverPath string) error {
f.Meta = append(f.Meta, &cmtBlock)
}
- // Add cover art if provided
if coverPath != "" {
if fileExists(coverPath) {
coverData, err := os.ReadFile(coverPath)
if err != nil {
fmt.Printf("[Metadata] Warning: Failed to read cover file %s: %v\n", coverPath, err)
} else {
- // Remove existing picture blocks first (like PC version)
for i := len(f.Meta) - 1; i >= 0; i-- {
if f.Meta[i].Type == flac.Picture {
f.Meta = append(f.Meta[:i], f.Meta[i+1:]...)
@@ -137,7 +135,6 @@ func EmbedMetadataWithCoverData(filePath string, metadata Metadata, coverData []
return fmt.Errorf("failed to parse FLAC file: %w", err)
}
- // Find or create vorbis comment block
var cmtIdx int = -1
var cmt *flacvorbis.MetaDataBlockVorbisComment
@@ -196,9 +193,7 @@ func EmbedMetadataWithCoverData(filePath string, metadata Metadata, coverData []
f.Meta = append(f.Meta, &cmtBlock)
}
- // Add cover art if provided
if len(coverData) > 0 {
- // Remove existing picture blocks first
for i := len(f.Meta) - 1; i >= 0; i-- {
if f.Meta[i].Type == flac.Picture {
f.Meta = append(f.Meta[:i], f.Meta[i+1:]...)
@@ -220,7 +215,6 @@ func EmbedMetadataWithCoverData(filePath string, metadata Metadata, coverData []
}
}
- // Save file
return f.Save(filePath)
}
@@ -257,7 +251,6 @@ func ReadMetadata(filePath string) (*Metadata, error) {
if trackNum != "" {
fmt.Sscanf(trackNum, "%d", &metadata.TrackNumber)
}
- // Also try lowercase variant (some encoders use lowercase)
if metadata.TrackNumber == 0 {
trackNum = getComment(cmt, "TRACK")
if trackNum != "" {
@@ -269,7 +262,6 @@ func ReadMetadata(filePath string) (*Metadata, error) {
if discNum != "" {
fmt.Sscanf(discNum, "%d", &metadata.DiscNumber)
}
- // Also try DISC variant
if metadata.DiscNumber == 0 {
discNum = getComment(cmt, "DISC")
if discNum != "" {
@@ -277,7 +269,6 @@ func ReadMetadata(filePath string) (*Metadata, error) {
}
}
- // Try DATE variants
if metadata.Date == "" {
metadata.Date = getComment(cmt, "YEAR")
}
@@ -293,7 +284,6 @@ func setComment(cmt *flacvorbis.MetaDataBlockVorbisComment, key, value string) {
if value == "" {
return
}
- // Remove existing (case-insensitive comparison for Vorbis comments)
keyUpper := strings.ToUpper(key)
for i := len(cmt.Comments) - 1; i >= 0; i-- {
comment := cmt.Comments[i]
@@ -305,7 +295,6 @@ func setComment(cmt *flacvorbis.MetaDataBlockVorbisComment, key, value string) {
}
}
}
- // Add new
cmt.Comments = append(cmt.Comments, key+"="+value)
}
@@ -313,7 +302,6 @@ func getComment(cmt *flacvorbis.MetaDataBlockVorbisComment, key string) string {
keyUpper := strings.ToUpper(key) + "="
for _, comment := range cmt.Comments {
if len(comment) > len(key) {
- // Case-insensitive comparison for Vorbis comments
commentUpper := strings.ToUpper(comment[:len(key)+1])
if commentUpper == keyUpper {
return comment[len(key)+1:]
diff --git a/go_backend/tidal.go b/go_backend/tidal.go
index 6373501b..d34e5c2a 100644
--- a/go_backend/tidal.go
+++ b/go_backend/tidal.go
@@ -194,7 +194,6 @@ func (t *TidalDownloader) GetAccessToken() (string, error) {
return "", err
}
- // Cache the token
t.cachedToken = result.AccessToken
if result.ExpiresIn > 0 {
t.tokenExpiresAt = time.Now().Add(time.Duration(result.ExpiresIn) * time.Second)
@@ -662,12 +661,10 @@ func getDownloadURLParallel(apis []string, trackID int64, quality string) (strin
resultChan := make(chan tidalAPIResult, len(apis))
startTime := time.Now()
- // Start all requests in parallel
for _, apiURL := range apis {
go func(api string) {
reqStart := time.Now()
- // Create client with timeout for parallel requests
client := &http.Client{
Timeout: 15 * time.Second,
}
@@ -698,7 +695,6 @@ func getDownloadURLParallel(apis []string, trackID int64, quality string) (strin
return
}
- // Try v2 format first (object with manifest)
var v2Response TidalAPIResponseV2
if err := json.Unmarshal(body, &v2Response); err == nil && v2Response.Data.Manifest != "" {
// IMPORTANT: Reject PREVIEW responses - we need FULL tracks
@@ -716,7 +712,6 @@ func getDownloadURLParallel(apis []string, trackID int64, quality string) (strin
return
}
- // Fallback to v1 format (array with OriginalTrackUrl)
var v1Responses []struct {
OriginalTrackURL string `json:"OriginalTrackUrl"`
}
@@ -738,13 +733,11 @@ func getDownloadURLParallel(apis []string, trackID int64, quality string) (strin
}(apiURL)
}
- // Collect results - return first success
var errors []string
for i := 0; i < len(apis); i++ {
result := <-resultChan
if result.err == nil {
- // First success - use this one
GoLog("[Tidal] [Parallel] ✓ Got response from %s (%d-bit/%dHz) in %v\n",
result.apiURL, result.info.BitDepth, result.info.SampleRate, result.duration)
@@ -777,7 +770,6 @@ func (t *TidalDownloader) GetDownloadURL(trackID int64, quality string) (TidalDo
return TidalDownloadInfo{}, fmt.Errorf("no API URL configured")
}
- // Use parallel approach - request from all APIs simultaneously
_, info, err := getDownloadURLParallel(apis, trackID, quality)
if err != nil {
return TidalDownloadInfo{}, fmt.Errorf("failed to get download URL: %w", err)
@@ -795,16 +787,13 @@ func parseManifest(manifestB64 string) (directURL string, initURL string, mediaU
manifestStr := string(manifestBytes)
- // Debug: log first 500 chars of manifest for debugging
manifestPreview := manifestStr
if len(manifestPreview) > 500 {
manifestPreview = manifestPreview[:500] + "..."
}
GoLog("[Tidal] Manifest content: %s\n", manifestPreview)
- // Check if it's BTS format (JSON) or DASH format (XML)
if strings.HasPrefix(manifestStr, "{") {
- // BTS format - JSON with direct URLs
var btsManifest TidalBTSManifest
if err := json.Unmarshal(manifestBytes, &btsManifest); err != nil {
return "", "", nil, fmt.Errorf("failed to parse BTS manifest: %w", err)
@@ -817,7 +806,6 @@ func parseManifest(manifestB64 string) (directURL string, initURL string, mediaU
return btsManifest.URLs[0], "", nil, nil
}
- // DASH format - XML with segments
var mpd MPD
if err := xml.Unmarshal(manifestBytes, &mpd); err != nil {
return "", "", nil, fmt.Errorf("failed to parse manifest XML: %w", err)
@@ -828,7 +816,6 @@ func parseManifest(manifestB64 string) (directURL string, initURL string, mediaU
mediaTemplate := segTemplate.Media
if initURL == "" || mediaTemplate == "" {
- // Fallback: try regex extraction
initRe := regexp.MustCompile(`initialization="([^"]+)"`)
mediaRe := regexp.MustCompile(`media="([^"]+)"`)
@@ -844,11 +831,9 @@ func parseManifest(manifestB64 string) (directURL string, initURL string, mediaU
return "", "", nil, fmt.Errorf("no initialization URL found in manifest")
}
- // Unescape HTML entities in URLs
initURL = strings.ReplaceAll(initURL, "&", "&")
mediaTemplate = strings.ReplaceAll(mediaTemplate, "&", "&")
- // Calculate segment count from timeline
segmentCount := 0
GoLog("[Tidal] XML parsed segments: %d entries in timeline\n", len(segTemplate.Timeline.Segments))
for i, seg := range segTemplate.Timeline.Segments {
@@ -857,10 +842,8 @@ func parseManifest(manifestB64 string) (directURL string, initURL string, mediaU
}
GoLog("[Tidal] Segment count from XML: %d\n", segmentCount)
- // If no segments found via XML, try regex
if segmentCount == 0 {
fmt.Println("[Tidal] No segments from XML, trying regex...")
- // Match or
segRe := regexp.MustCompile(` 0 && itemID != "" {
SetItemBytesTotal(itemID, expectedSize)
}
@@ -946,24 +925,19 @@ func (t *TidalDownloader) DownloadFile(downloadURL, outputPath, itemID string) e
return err
}
- // Use buffered writer for better performance (256KB buffer)
bufWriter := bufio.NewWriterSize(out, 256*1024)
- // Use item progress writer with buffered output
var written int64
if itemID != "" {
progressWriter := NewItemProgressWriter(bufWriter, itemID)
written, err = io.Copy(progressWriter, resp.Body)
} else {
- // Fallback: direct copy without progress tracking
written, err = io.Copy(bufWriter, resp.Body)
}
- // Flush buffer before checking for errors
flushErr := bufWriter.Flush()
closeErr := out.Close()
- // Check for any errors
if err != nil {
os.Remove(outputPath)
if isDownloadCancelled(itemID) {
@@ -980,7 +954,6 @@ func (t *TidalDownloader) DownloadFile(downloadURL, outputPath, itemID string) e
return fmt.Errorf("failed to close file: %w", closeErr)
}
- // Verify file size if Content-Length was provided
if expectedSize > 0 && written != expectedSize {
os.Remove(outputPath)
return fmt.Errorf("incomplete download: expected %d bytes, got %d bytes", expectedSize, written)
@@ -1003,7 +976,6 @@ func (t *TidalDownloader) downloadFromManifest(ctx context.Context, manifestB64,
Timeout: 120 * time.Second,
}
- // If we have a direct URL (BTS format), download directly with progress tracking
if directURL != "" {
GoLog("[Tidal] BTS format - downloading from direct URL: %s...\n", directURL[:min(80, len(directURL))])
// Note: Progress tracking is initialized by the caller (DownloadFile)
@@ -1035,7 +1007,6 @@ func (t *TidalDownloader) downloadFromManifest(ctx context.Context, manifestB64,
GoLog("[Tidal] BTS response OK, Content-Length: %d\n", resp.ContentLength)
expectedSize := resp.ContentLength
- // Set total bytes for progress tracking
if expectedSize > 0 && itemID != "" {
SetItemBytesTotal(itemID, expectedSize)
}
@@ -1045,7 +1016,6 @@ func (t *TidalDownloader) downloadFromManifest(ctx context.Context, manifestB64,
return fmt.Errorf("failed to create file: %w", err)
}
- // Use item progress writer
var written int64
if itemID != "" {
progressWriter := NewItemProgressWriter(out, itemID)
@@ -1068,7 +1038,6 @@ func (t *TidalDownloader) downloadFromManifest(ctx context.Context, manifestB64,
return fmt.Errorf("failed to close file: %w", closeErr)
}
- // Verify file size if Content-Length was provided
if expectedSize > 0 && written != expectedSize {
os.Remove(outputPath)
return fmt.Errorf("incomplete download: expected %d bytes, got %d bytes", expectedSize, written)
@@ -1077,21 +1046,15 @@ func (t *TidalDownloader) downloadFromManifest(ctx context.Context, manifestB64,
return nil
}
- // DASH format - download segments directly to M4A file (no temp file to avoid Android permission issues)
- // On Android, we can't use ffmpeg, so we save as M4A directly
m4aPath := strings.TrimSuffix(outputPath, ".flac") + ".m4a"
GoLog("[Tidal] DASH format - downloading %d segments directly to: %s\n", len(mediaURLs), m4aPath)
- // Note: Progress tracking is initialized by the caller (DownloadFile or downloadFromTidal)
- // We just update progress here based on segment count
-
out, err := os.Create(m4aPath)
if err != nil {
GoLog("[Tidal] Failed to create M4A file: %v\n", err)
return fmt.Errorf("failed to create M4A file: %w", err)
}
- // Download initialization segment
GoLog("[Tidal] Downloading init segment...\n")
if isDownloadCancelled(itemID) {
out.Close()
@@ -1134,7 +1097,6 @@ func (t *TidalDownloader) downloadFromManifest(ctx context.Context, manifestB64,
return fmt.Errorf("failed to write init segment: %w", err)
}
- // Download media segments with progress
totalSegments := len(mediaURLs)
for i, mediaURL := range mediaURLs {
if isDownloadCancelled(itemID) {
@@ -1147,7 +1109,6 @@ func (t *TidalDownloader) downloadFromManifest(ctx context.Context, manifestB64,
GoLog("[Tidal] Downloading segment %d/%d...\n", i+1, totalSegments)
}
- // Update progress based on segment count
if itemID != "" {
progress := float64(i+1) / float64(totalSegments)
SetItemProgress(itemID, progress, 0, 0)
diff --git a/lib/providers/download_queue_provider.dart b/lib/providers/download_queue_provider.dart
index 3c98cdc6..d7a32bac 100644
--- a/lib/providers/download_queue_provider.dart
+++ b/lib/providers/download_queue_provider.dart
@@ -45,7 +45,6 @@ class DownloadHistoryItem {
final int? duration;
final String? releaseDate;
final String? quality;
- // Audio quality info (from file after download)
final int? bitDepth;
final int? sampleRate;
@@ -141,7 +140,6 @@ class DownloadHistoryNotifier extends Notifier {
@override
DownloadHistoryState build() {
- // Load history from storage on init
_loadFromStorageSync();
return DownloadHistoryState();
}
@@ -165,13 +163,11 @@ class DownloadHistoryNotifier extends Notifier {
.map((e) => DownloadHistoryItem.fromJson(e as Map))
.toList();
- // Deduplicate existing history on load
final deduplicatedItems = _deduplicateHistory(items);
state = state.copyWith(items: deduplicatedItems);
_historyLog.i('Loaded ${deduplicatedItems.length} items from storage (original: ${items.length})');
- // Save if duplicates were removed
if (deduplicatedItems.length < items.length) {
_historyLog.i('Removed ${items.length - deduplicatedItems.length} duplicate entries');
await _saveToStorage();
@@ -194,9 +190,7 @@ class DownloadHistoryNotifier extends Notifier {
final item = items[i];
String? key;
- // Generate unique key based on available identifiers
if (item.spotifyId != null && item.spotifyId!.isNotEmpty) {
- // Extract numeric ID for deezer: prefixed IDs
if (item.spotifyId!.startsWith('deezer:')) {
key = 'deezer:${item.spotifyId!.substring(7)}';
} else {
@@ -208,11 +202,9 @@ class DownloadHistoryNotifier extends Notifier {
if (key != null) {
if (!seen.containsKey(key)) {
- // First occurrence - keep it (most recent since list is sorted by date desc)
seen[key] = result.length;
result.add(item);
} else {
- // Duplicate found - skip (keep the first/most recent one)
_historyLog.d('Skipping duplicate: ${item.trackName} (key: $key)');
}
} else {
@@ -241,9 +233,7 @@ class DownloadHistoryNotifier extends Notifier {
}
void addToHistory(DownloadHistoryItem item) {
- // Check if track already exists in history (by spotifyId, deezerId, or ISRC)
final existingIndex = state.items.indexWhere((existing) {
- // Match by spotifyId (primary identifier - includes deezer:xxx format)
if (item.spotifyId != null &&
item.spotifyId!.isNotEmpty &&
existing.spotifyId == item.spotifyId) {
@@ -253,14 +243,13 @@ class DownloadHistoryNotifier extends Notifier {
// 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); // Remove "deezer:" prefix
+ final itemDeezerId = item.spotifyId!.substring(7);
final existingDeezerId = existing.spotifyId!.substring(7);
if (itemDeezerId == existingDeezerId) {
return true;
}
}
- // Fallback: match by ISRC if spotifyId not available
if (item.isrc != null &&
item.isrc!.isNotEmpty &&
existing.isrc == item.isrc) {
@@ -279,7 +268,6 @@ class DownloadHistoryNotifier extends Notifier {
state = state.copyWith(items: updatedItems);
_historyLog.d('Updated existing history entry: ${item.trackName}');
} else {
- // Add new entry
state = state.copyWith(items: [item, ...state.items]);
_historyLog.d('Added new history entry: ${item.trackName}');
}
@@ -402,7 +390,6 @@ class DownloadQueueNotifier extends Notifier {
_progressTimer = null;
});
- // Initialize output directory and load persisted queue asynchronously
Future.microtask(() async {
await _initOutputDir();
await _loadQueueFromStorage();
@@ -432,7 +419,6 @@ class DownloadQueueNotifier extends Notifier {
return item;
}).toList();
- // Only restore queued/downloading items (not completed/failed/skipped)
final pendingItems = restoredItems
.where((item) => item.status == DownloadStatus.queued)
.toList();
@@ -461,7 +447,6 @@ class DownloadQueueNotifier extends Notifier {
try {
final prefs = await SharedPreferences.getInstance();
- // Only persist queued and downloading items
final pendingItems = state.items
.where(
(item) =>
@@ -471,7 +456,6 @@ class DownloadQueueNotifier extends Notifier {
.toList();
if (pendingItems.isEmpty) {
- // Clear storage if no pending items
await prefs.remove(_queueStorageKey);
_log.d('Cleared queue storage (no pending items)');
} else {
@@ -523,12 +507,9 @@ class DownloadQueueNotifier extends Notifier {
itemProgress['is_downloading'] as bool? ?? false;
final status = itemProgress['status'] as String? ?? 'downloading';
- // Check if status is "finalizing" (embedding metadata)
- // Only trust finalizing status if bytesTotal > 0 (download actually happened)
if (status == 'finalizing' && bytesTotal > 0) {
updateItemStatus(itemId, DownloadStatus.finalizing, progress: 1.0);
- // Track finalizing item for notification
final currentItem = state.items
.where((i) => i.id == itemId)
.firstOrNull;
@@ -540,7 +521,6 @@ class DownloadQueueNotifier extends Notifier {
continue;
}
- // Use progress from backend if available (handles both explicit progress and byte-based)
final progressFromBackend =
(itemProgress['progress'] as num?)?.toDouble() ?? 0.0;
@@ -556,7 +536,6 @@ class DownloadQueueNotifier extends Notifier {
updateProgress(itemId, percentage, speedMBps: speedMBps);
- // Log progress for each item with speed
final mbReceived = bytesReceived / (1024 * 1024);
final mbTotal = bytesTotal / (1024 * 1024);
if (bytesTotal > 0) {
@@ -571,7 +550,6 @@ class DownloadQueueNotifier extends Notifier {
}
}
- // Show finalizing notification if any item is finalizing (takes priority)
if (hasFinalizingItem && finalizingTrackName != null) {
_notificationService.showDownloadFinalizing(
trackName: finalizingTrackName,
@@ -592,7 +570,6 @@ class DownloadQueueNotifier extends Notifier {
.where((i) => i.status == DownloadStatus.downloading)
.toList();
if (downloadingItems.isNotEmpty) {
- // Show single track name if only 1 download, otherwise show count
final trackName = downloadingItems.length == 1
? downloadingItems.first.track.name
: '${downloadingItems.length} downloads';
@@ -600,12 +577,10 @@ class DownloadQueueNotifier extends Notifier {
? downloadingItems.first.track.artistName
: 'Downloading...';
- // Calculate notification progress values
int notifProgress = bytesReceived;
int notifTotal = bytesTotal;
if (bytesTotal <= 0) {
- // Fallback to percentage for DASH/unknown size
final progressPercent =
(firstProgress['progress'] as num?)?.toDouble() ?? 0.0;
notifProgress = (progressPercent * 100).toInt();
@@ -616,10 +591,9 @@ class DownloadQueueNotifier extends Notifier {
trackName: trackName,
artistName: artistName,
progress: notifProgress,
- total: notifTotal > 0 ? notifTotal : 1,
- );
+ total: notifTotal > 0 ? notifTotal : 1,
+ );
- // Update foreground service notification (Android)
if (Platform.isAndroid) {
PlatformBridge.updateDownloadServiceProgress(
trackName: downloadingItems.first.track.name,
@@ -632,7 +606,6 @@ class DownloadQueueNotifier extends Notifier {
}
}
} catch (e) {
- // Ignore polling errors
}
});
}
@@ -665,7 +638,6 @@ class DownloadQueueNotifier extends Notifier {
}
state = state.copyWith(outputDir: musicDir.path);
} else {
- // Fallback to documents directory
final docDir = await getApplicationDocumentsDirectory();
final musicDir = Directory('${docDir.path}/SpotiFLAC');
if (!await musicDir.exists()) {
@@ -675,7 +647,6 @@ class DownloadQueueNotifier extends Notifier {
}
}
} catch (e) {
- // Fallback for any platform
final dir = await getApplicationDocumentsDirectory();
final musicDir = Directory('${dir.path}/SpotiFLAC');
if (!await musicDir.exists()) {
@@ -695,12 +666,10 @@ class DownloadQueueNotifier extends Notifier {
String baseDir = state.outputDir;
final albumArtist = _normalizeOptionalString(track.albumArtist) ?? track.artistName;
- // If separateSingles is enabled, use Albums/Singles structure
if (separateSingles) {
final isSingle = track.isSingle;
if (isSingle) {
- // Singles go to Singles folder (flat structure)
final singlesPath = '$baseDir${Platform.pathSeparator}Singles';
final dir = Directory(singlesPath);
if (!await dir.exists()) {
@@ -709,7 +678,6 @@ class DownloadQueueNotifier extends Notifier {
}
return singlesPath;
} else {
- // Albums folder structure based on setting
final albumName = _sanitizeFolderName(track.albumName);
final artistName = _sanitizeFolderName(albumArtist);
final year = _extractYear(track.releaseDate);
@@ -726,12 +694,10 @@ class DownloadQueueNotifier extends Notifier {
albumPath = '$baseDir${Platform.pathSeparator}Albums${Platform.pathSeparator}$artistName${Platform.pathSeparator}$yearAlbum';
break;
case 'year_album':
- // Albums/[Year] Album structure (no artist folder)
final yearAlbum = year != null ? '[$year] $albumName' : albumName;
albumPath = '$baseDir${Platform.pathSeparator}Albums${Platform.pathSeparator}$yearAlbum';
break;
default:
- // Albums/Artist/Album structure (default: artist_album)
albumPath = '$baseDir${Platform.pathSeparator}Albums${Platform.pathSeparator}$artistName${Platform.pathSeparator}$albumName';
}
@@ -951,7 +917,6 @@ class DownloadQueueNotifier extends Notifier {
if (state.isPaused) {
state = state.copyWith(isPaused: false);
_log.i('Queue resumed');
- // If there are still queued items, continue processing
if (state.queuedCount > 0 && !state.isProcessing) {
Future.microtask(() => _processQueue());
}
@@ -995,9 +960,8 @@ class DownloadQueueNotifier extends Notifier {
return i;
}).toList();
state = state.copyWith(items: items);
- _saveQueueToStorage(); // Persist queue
+ _saveQueueToStorage();
- // Start processing if not already running
if (!state.isProcessing) {
_log.d('Starting queue processing for retry');
Future.microtask(() => _processQueue());
@@ -1093,7 +1057,6 @@ class DownloadQueueNotifier extends Notifier {
var coverUrl = track.coverUrl;
if (coverUrl != null && coverUrl.isNotEmpty) {
try {
- // Upgrade cover URL to max quality if setting is enabled
if (settings.maxQualityCover) {
coverUrl = _upgradeToMaxQualityCover(coverUrl);
_log.d('Cover URL upgraded to max quality: $coverUrl');
@@ -1104,7 +1067,6 @@ class DownloadQueueNotifier extends Notifier {
'${DateTime.now().millisecondsSinceEpoch}_${Random().nextInt(10000)}';
coverPath = '${tempDir.path}/cover_$uniqueId.jpg';
- // Download cover using HTTP
final httpClient = HttpClient();
final request = await httpClient.getUrl(Uri.parse(coverUrl));
final response = await request.close();
@@ -1125,12 +1087,7 @@ class DownloadQueueNotifier extends Notifier {
}
}
- // Use Go backend to embed metadata
try {
- // Use FFmpeg to embed cover art AND text metadata
- // FFmpeg can embed cover art to FLAC and also set tags
-
- // Construct metadata map
final metadata = {
'TITLE': track.name,
'ARTIST': track.artistName,
diff --git a/lib/providers/recent_access_provider.dart b/lib/providers/recent_access_provider.dart
index 0882b39f..9d383cb3 100644
--- a/lib/providers/recent_access_provider.dart
+++ b/lib/providers/recent_access_provider.dart
@@ -112,7 +112,6 @@ class RecentAccessNotifier extends Notifier {
.toList();
state = state.copyWith(items: items, isLoaded: true);
} catch (e) {
- // Invalid JSON, start fresh
state = state.copyWith(isLoaded: true);
}
} else {
@@ -210,10 +209,8 @@ class RecentAccessNotifier extends Notifier {
.where((e) => e.uniqueKey != item.uniqueKey)
.toList();
- // Add new item at the beginning
updatedItems.insert(0, item);
- // Limit to max items
if (updatedItems.length > _maxRecentItems) {
updatedItems.removeRange(_maxRecentItems, updatedItems.length);
}
@@ -221,7 +218,6 @@ class RecentAccessNotifier extends Notifier {
state = state.copyWith(items: updatedItems);
_saveHistory();
- // Debug log
// ignore: avoid_print
print('[RecentAccess] Total items now: ${updatedItems.length}');
}
diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart
index 39bb3900..1983293f 100644
--- a/lib/providers/settings_provider.dart
+++ b/lib/providers/settings_provider.dart
@@ -22,13 +22,10 @@ class SettingsNotifier extends Notifier {
if (json != null) {
state = AppSettings.fromJson(jsonDecode(json));
- // Run migrations if needed
await _runMigrations(prefs);
- // Apply Spotify credentials to Go backend on load
_applySpotifyCredentials();
- // Sync logging state
LogBuffer.loggingEnabled = state.enableLogging;
}
}
@@ -38,16 +35,12 @@ class SettingsNotifier extends Notifier {
final lastMigration = prefs.getInt(_migrationVersionKey) ?? 0;
if (lastMigration < 1) {
- // Migration 1: Set metadataSource to 'deezer' for existing users
- // Only apply if user hasn't enabled custom Spotify credentials
- // (users with custom credentials likely prefer Spotify)
if (!state.useCustomSpotifyCredentials) {
state = state.copyWith(metadataSource: 'deezer');
await _saveSettings();
}
}
- // Save current migration version
if (lastMigration < _currentMigrationVersion) {
await prefs.setInt(_migrationVersionKey, _currentMigrationVersion);
}
@@ -68,8 +61,6 @@ class SettingsNotifier extends Notifier {
state.spotifyClientSecret,
);
}
- // Note: If credentials are empty, Spotify API will return error
- // User should use Deezer as metadata source instead
}
void setDefaultService(String service) {
@@ -113,7 +104,6 @@ class SettingsNotifier extends Notifier {
}
void setConcurrentDownloads(int count) {
- // Clamp between 1 and 3
final clamped = count.clamp(1, 3);
state = state.copyWith(concurrentDownloads: clamped);
_saveSettings();
diff --git a/lib/providers/track_provider.dart b/lib/providers/track_provider.dart
index 83848520..49f65bf6 100644
--- a/lib/providers/track_provider.dart
+++ b/lib/providers/track_provider.dart
@@ -142,14 +142,11 @@ class TrackNotifier extends Notifier {
bool _isRequestValid(int requestId) => requestId == _currentRequestId;
Future fetchFromUrl(String url, {bool useDeezerFallback = true}) async {
- // Increment request ID to cancel any pending requests
final requestId = ++_currentRequestId;
- // Preserve hasSearchText during fetch
state = TrackState(isLoading: true, hasSearchText: state.hasSearchText);
try {
- // First, check if any extension can handle this URL
final extensionHandler = await PlatformBridge.findURLHandler(url);
if (extensionHandler != null) {
_log.i('Found extension URL handler: $extensionHandler for URL: $url');
@@ -188,7 +185,6 @@ class TrackNotifier extends Notifier {
final albumsList = artistData['albums'] as List? ?? [];
final albums = albumsList.map((a) => _parseArtistAlbum(a as Map)).toList();
- // Parse top tracks if available
final topTracksList = artistData['top_tracks'] as List? ?? [];
final topTracks = topTracksList.map((t) => _parseSearchTrack(t as Map, source: extensionId)).toList();
@@ -209,13 +205,11 @@ class TrackNotifier extends Notifier {
}
}
- // No extension handler found, try Spotify URL parsing
final parsed = await PlatformBridge.parseSpotifyUrl(url);
if (!_isRequestValid(requestId)) return; // Request cancelled
final type = parsed['type'] as String;
- // Use the new fallback-enabled method
Map metadata;
try {
@@ -225,7 +219,6 @@ class TrackNotifier extends Notifier {
// ignore: avoid_print
print('[FetchURL] Metadata fetch success');
} catch (e) {
- // If fallback also fails, show error
// ignore: avoid_print
print('[FetchURL] Metadata fetch failed: $e');
rethrow;
@@ -252,7 +245,6 @@ class TrackNotifier extends Notifier {
albumName: albumInfo['name'] as String?,
coverUrl: albumInfo['images'] as String?,
);
- // Pre-warm cache for album tracks in background
_preWarmCacheForTracks(tracks);
} else if (type == 'playlist') {
final playlistInfo = metadata['playlist_info'] as Map;
@@ -281,8 +273,7 @@ class TrackNotifier extends Notifier {
);
}
} catch (e) {
- if (!_isRequestValid(requestId)) return; // Request cancelled
- // Preserve hasSearchText on error so user stays on search screen
+ if (!_isRequestValid(requestId)) return;
state = TrackState(isLoading: false, error: e.toString(), hasSearchText: state.hasSearchText);
}
}
@@ -295,7 +286,6 @@ class TrackNotifier extends Notifier {
state = TrackState(isLoading: true, hasSearchText: state.hasSearchText);
try {
- // Check if extension providers should be used for search
final settings = ref.read(settingsProvider);
final extensionState = ref.read(extensionProvider);
final hasActiveMetadataExtensions = extensionState.extensions.any(
@@ -308,7 +298,6 @@ class TrackNotifier extends Notifier {
searchProvider != null &&
searchProvider.isNotEmpty;
- // Use Deezer or Spotify based on settings
final source = metadataSource ?? 'deezer';
_log.i(
@@ -318,14 +307,12 @@ class TrackNotifier extends Notifier {
Map results;
List