From e20becdca705e8557b80c5851bc38fccab6c1c93 Mon Sep 17 00:00:00 2001 From: zarzet Date: Wed, 4 Feb 2026 10:20:04 +0700 Subject: [PATCH] refactor: remove more redundant comments --- go_backend/audio_metadata.go | 40 +---- go_backend/exports.go | 4 - go_backend/extension_providers.go | 171 +++------------------ go_backend/extension_runtime_ffmpeg.go | 12 -- go_backend/extension_runtime_file.go | 1 - go_backend/extension_runtime_polyfills.go | 24 +-- go_backend/library_scan.go | 13 -- lib/providers/download_queue_provider.dart | 5 - lib/providers/local_library_provider.dart | 16 -- lib/screens/album_screen.dart | 30 ++-- lib/screens/artist_screen.dart | 2 +- lib/screens/home_tab.dart | 9 -- lib/screens/playlist_screen.dart | 2 +- lib/screens/queue_tab.dart | 25 --- lib/services/cover_cache_manager.dart | 4 - lib/utils/logger.dart | 1 - 16 files changed, 44 insertions(+), 315 deletions(-) diff --git a/go_backend/audio_metadata.go b/go_backend/audio_metadata.go index 81deda9a..74bc1762 100644 --- a/go_backend/audio_metadata.go +++ b/go_backend/audio_metadata.go @@ -180,7 +180,6 @@ func parseID3v22Frames(data []byte, metadata *AudioMetadata, tagUnsync bool) { } } -// parseID3v23Frames parses ID3v2.3 and ID3v2.4 frames (4-char frame IDs) func parseID3v23Frames(data []byte, metadata *AudioMetadata, version byte, tagUnsync bool) { pos := 0 for pos+10 < len(data) { @@ -191,10 +190,8 @@ func parseID3v23Frames(data []byte, metadata *AudioMetadata, version byte, tagUn var frameSize int if version == 4 { - // ID3v2.4 uses syncsafe integers frameSize = int(data[pos+4])<<21 | int(data[pos+5])<<14 | int(data[pos+6])<<7 | int(data[pos+7]) } else { - // ID3v2.3 uses regular integers frameSize = int(data[pos+4])<<24 | int(data[pos+5])<<16 | int(data[pos+6])<<8 | int(data[pos+7]) } @@ -208,9 +205,7 @@ func parseID3v23Frames(data []byte, metadata *AudioMetadata, version byte, tagUn _ = statusFlags formatFlags := data[pos+9] - // Handle frame-specific flags if version == 3 { - // ID3v2.3 format flags: compression/encryption/grouping not supported const ( id3v23FlagCompression = 0x80 id3v23FlagEncryption = 0x40 @@ -231,7 +226,6 @@ func parseID3v23Frames(data []byte, metadata *AudioMetadata, version byte, tagUn frameData = removeUnsync(frameData) } } else if version == 4 { - // ID3v2.4 format flags: grouping, compression, encryption, unsync, data length indicator const ( id3v24FlagGrouping = 0x40 id3v24FlagCompression = 0x08 @@ -527,27 +521,24 @@ func GetMP3Quality(filePath string) (*MP3Quality, error) { sampleRateIdx := (frameHeader[2] >> 2) & 0x03 sampleRates := [][]int{ - {11025, 12000, 8000}, // MPEG 2.5 - {0, 0, 0}, // Reserved - {22050, 24000, 16000}, // MPEG 2 - {44100, 48000, 32000}, // MPEG 1 + {11025, 12000, 8000}, + {0, 0, 0}, + {22050, 24000, 16000}, + {44100, 48000, 32000}, } if version < 4 && sampleRateIdx < 3 { quality.SampleRate = sampleRates[version][sampleRateIdx] } - // Get bitrate (for MPEG 1 Layer 3) - if version == 3 && layer == 1 { // MPEG 1, Layer 3 + if version == 3 && layer == 1 { bitrates := []int{0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 0} if bitrateIdx < 16 { quality.Bitrate = bitrates[bitrateIdx] * 1000 } } - // MP3 is always 16-bit PCM when decoded quality.BitDepth = 16 - // Estimate duration from file size and bitrate if quality.Bitrate > 0 { audioSize := fileSize - audioStart - 128 if audioSize > 0 { @@ -564,11 +555,6 @@ func GetMP3Quality(filePath string) (*MP3Quality, error) { return quality, nil } -// ============================================================================= -// Ogg/Opus Vorbis Comment Reading -// ============================================================================= - -// ReadOggVorbisComments reads Vorbis comments from Ogg/Opus files func ReadOggVorbisComments(filePath string) (*AudioMetadata, error) { file, err := os.Open(filePath) if err != nil { @@ -598,7 +584,6 @@ func ReadOggVorbisComments(filePath string) (*AudioMetadata, error) { break } } - // Fallback: if unknown, still try OpusTags if streamType == oggStreamUnknown { if len(pkt) > 8 && string(pkt[0:8]) == "OpusTags" { parseVorbisComments(pkt[8:], metadata) @@ -620,7 +605,6 @@ type oggPage struct { data []byte } -// readOggPageWithHeader reads a single Ogg page including header info func readOggPageWithHeader(file *os.File) (*oggPage, error) { header := make([]byte, 27) if _, err := io.ReadFull(file, header); err != nil { @@ -656,7 +640,6 @@ func readOggPageWithHeader(file *os.File) (*oggPage, error) { }, nil } -// readOggPage reads a single Ogg page (data only) func readOggPage(file *os.File) ([]byte, error) { page, err := readOggPageWithHeader(file) if err != nil { @@ -665,7 +648,6 @@ func readOggPage(file *os.File) ([]byte, error) { return page.data, nil } -// collectOggPackets reads Ogg pages and returns reassembled packets func collectOggPackets(file *os.File, maxPackets, maxPages int) ([][]byte, error) { const maxPacketSize = 10 * 1024 * 1024 var packets [][]byte @@ -681,7 +663,6 @@ func collectOggPackets(file *os.File, maxPackets, maxPages int) ([][]byte, error return nil, err } - // If this page is not a continuation but we have partial packet, drop it if page.headerType&0x01 == 0 && len(cur) > 0 { cur = nil skipPacket = false @@ -1197,7 +1178,7 @@ func parseFLACPictureBlock(data []byte) ([]byte, string) { var dataLen uint32 binary.Read(reader, binary.BigEndian, &dataLen) - if dataLen > 10000000 { // 10MB + if dataLen > 10000000 { return nil, "" } @@ -1207,12 +1188,10 @@ func parseFLACPictureBlock(data []byte) ([]byte, string) { return imageData, mimeType } -// base64StdDecodeLen returns decoded length func base64StdDecodeLen(n int) int { return n * 6 / 8 } -// base64StdDecode decodes base64 data (simplified) func base64StdDecode(dst, src []byte) (int, error) { const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" @@ -1270,7 +1249,6 @@ func base64StdDecode(dst, src []byte) (int, error) { return di, nil } -// extractAnyCoverArt extracts cover art from any supported audio file func extractAnyCoverArt(filePath string) ([]byte, string, error) { ext := strings.ToLower(filepath.Ext(filePath)) @@ -1280,7 +1258,6 @@ func extractAnyCoverArt(filePath string) ([]byte, string, error) { if err != nil { return nil, "", err } - // Detect MIME type from magic bytes mimeType := "image/jpeg" if len(data) > 8 && string(data[1:4]) == "PNG" { mimeType = "image/png" @@ -1294,8 +1271,6 @@ func extractAnyCoverArt(filePath string) ([]byte, string, error) { return extractOggCoverArt(filePath) case ".m4a": - // M4A cover extraction would need more complex MP4 atom parsing - // For now, return error return nil, "", fmt.Errorf("M4A cover extraction not yet supported") default: @@ -1303,10 +1278,7 @@ func extractAnyCoverArt(filePath string) ([]byte, string, error) { } } -// SaveCoverToCache extracts and saves cover art to cache directory -// Returns the path to the saved cover image, or empty string if no cover found func SaveCoverToCache(filePath, cacheDir string) (string, error) { - // Generate cache filename from file path + size + mtime to reduce stale cache cacheKey := filePath if stat, err := os.Stat(filePath); err == nil { cacheKey = fmt.Sprintf("%s|%d|%d", filePath, stat.Size(), stat.ModTime().UnixNano()) diff --git a/go_backend/exports.go b/go_backend/exports.go index 0229d2d5..79820dbe 100644 --- a/go_backend/exports.go +++ b/go_backend/exports.go @@ -2092,22 +2092,18 @@ func SetLibraryCoverCacheDirJSON(cacheDir string) { SetLibraryCoverCacheDir(cacheDir) } -// ScanLibraryFolderJSON scans a folder for audio files and returns metadata func ScanLibraryFolderJSON(folderPath string) (string, error) { return ScanLibraryFolder(folderPath) } -// GetLibraryScanProgressJSON returns current scan progress func GetLibraryScanProgressJSON() string { return GetLibraryScanProgress() } -// CancelLibraryScanJSON cancels ongoing library scan func CancelLibraryScanJSON() { CancelLibraryScan() } -// ReadAudioMetadataJSON reads metadata from a single audio file func ReadAudioMetadataJSON(filePath string) (string, error) { return ReadAudioMetadata(filePath) } diff --git a/go_backend/extension_providers.go b/go_backend/extension_providers.go index 97d0e49c..0aedb2c9 100644 --- a/go_backend/extension_providers.go +++ b/go_backend/extension_providers.go @@ -25,7 +25,7 @@ type ExtTrackMetadata struct { AlbumArtist string `json:"album_artist,omitempty"` DurationMS int `json:"duration_ms"` CoverURL string `json:"cover_url,omitempty"` - Images string `json:"images,omitempty"` // Alternative field for cover URL (used by some extensions) + Images string `json:"images,omitempty"` ReleaseDate string `json:"release_date,omitempty"` TrackNumber int `json:"track_number,omitempty"` DiscNumber int `json:"disc_number,omitempty"` @@ -33,19 +33,18 @@ type ExtTrackMetadata struct { ProviderID string `json:"provider_id"` ItemType string `json:"item_type,omitempty"` AlbumType string `json:"album_type,omitempty"` - // Enrichment fields from Odesli/song.link + TidalID string `json:"tidal_id,omitempty"` QobuzID string `json:"qobuz_id,omitempty"` DeezerID string `json:"deezer_id,omitempty"` SpotifyID string `json:"spotify_id,omitempty"` - ExternalLinks map[string]string `json:"external_links,omitempty"` // service -> URL mapping - // Extended metadata from enrichment (can come from Deezer, Spotify, etc.) - Label string `json:"label,omitempty"` // Record label - Copyright string `json:"copyright,omitempty"` // Copyright information - Genre string `json:"genre,omitempty"` // Music genre(s) + ExternalLinks map[string]string `json:"external_links,omitempty"` + + Label string `json:"label,omitempty"` + Copyright string `json:"copyright,omitempty"` + Genre string `json:"genre,omitempty"` } -// ResolvedCoverURL returns the cover URL, checking both CoverURL and Images fields func (t *ExtTrackMetadata) ResolvedCoverURL() string { if t.CoverURL != "" { return t.CoverURL @@ -53,7 +52,6 @@ func (t *ExtTrackMetadata) ResolvedCoverURL() string { return t.Images } -// ExtAlbumMetadata represents album metadata from an extension type ExtAlbumMetadata struct { ID string `json:"id"` Name string `json:"name"` @@ -67,34 +65,28 @@ type ExtAlbumMetadata struct { ProviderID string `json:"provider_id"` } -// ExtArtistMetadata represents artist metadata from an extension type ExtArtistMetadata struct { ID string `json:"id"` Name string `json:"name"` ImageURL string `json:"image_url,omitempty"` - HeaderImage string `json:"header_image,omitempty"` // Header image for artist page background - Listeners int `json:"listeners,omitempty"` // Monthly listeners + HeaderImage string `json:"header_image,omitempty"` + Listeners int `json:"listeners,omitempty"` Albums []ExtAlbumMetadata `json:"albums,omitempty"` - TopTracks []ExtTrackMetadata `json:"top_tracks,omitempty"` // Popular tracks + TopTracks []ExtTrackMetadata `json:"top_tracks,omitempty"` ProviderID string `json:"provider_id"` } -// ExtSearchResult represents search results from an extension type ExtSearchResult struct { Tracks []ExtTrackMetadata `json:"tracks"` Total int `json:"total"` } -// ==================== Download Types ==================== - -// ExtAvailabilityResult represents availability check result type ExtAvailabilityResult struct { Available bool `json:"available"` Reason string `json:"reason,omitempty"` TrackID string `json:"track_id,omitempty"` } -// ExtDownloadURLResult represents download URL info type ExtDownloadURLResult struct { URL string `json:"url"` Format string `json:"format"` @@ -102,7 +94,6 @@ type ExtDownloadURLResult struct { SampleRate int `json:"sample_rate,omitempty"` } -// ExtDownloadResult represents download result from an extension type ExtDownloadResult struct { Success bool `json:"success"` FilePath string `json:"file_path,omitempty"` @@ -110,7 +101,7 @@ type ExtDownloadResult struct { SampleRate int `json:"sample_rate,omitempty"` ErrorMessage string `json:"error_message,omitempty"` ErrorType string `json:"error_type,omitempty"` - // Metadata returned by extension (optional - if provided, can skip enrichment) + Title string `json:"title,omitempty"` Artist string `json:"artist,omitempty"` Album string `json:"album,omitempty"` @@ -122,15 +113,11 @@ type ExtDownloadResult struct { ISRC string `json:"isrc,omitempty"` } -// ==================== Provider Wrapper ==================== - -// ExtensionProviderWrapper wraps an extension to call its provider methods type ExtensionProviderWrapper struct { extension *LoadedExtension vm *goja.Runtime } -// NewExtensionProviderWrapper creates a new provider wrapper func NewExtensionProviderWrapper(ext *LoadedExtension) *ExtensionProviderWrapper { return &ExtensionProviderWrapper{ extension: ext, @@ -138,9 +125,6 @@ func NewExtensionProviderWrapper(ext *LoadedExtension) *ExtensionProviderWrapper } } -// ==================== Metadata Provider Methods ==================== - -// SearchTracks searches for tracks using the extension func (p *ExtensionProviderWrapper) SearchTracks(query string, limit int) (*ExtSearchResult, error) { if !p.extension.Manifest.IsMetadataProvider() { return nil, fmt.Errorf("extension '%s' is not a metadata provider", p.extension.ID) @@ -150,11 +134,9 @@ func (p *ExtensionProviderWrapper) SearchTracks(query string, limit int) (*ExtSe return nil, fmt.Errorf("extension '%s' is disabled", p.extension.ID) } - // Lock VM to prevent concurrent access p.extension.VMMu.Lock() defer p.extension.VMMu.Unlock() - // Call extension's searchTracks function script := fmt.Sprintf(` (function() { if (typeof extension !== 'undefined' && typeof extension.searchTracks === 'function') { @@ -184,14 +166,11 @@ func (p *ExtensionProviderWrapper) SearchTracks(query string, limit int) (*ExtSe var searchResult ExtSearchResult - // Try to parse as ExtSearchResult object first if err := json.Unmarshal(jsonBytes, &searchResult); err != nil { - // If that fails, try parsing as array of tracks directly var tracks []ExtTrackMetadata if arrErr := json.Unmarshal(jsonBytes, &tracks); arrErr != nil { return nil, fmt.Errorf("failed to parse search result: %w (also tried array: %v)", err, arrErr) } - // Wrap array in ExtSearchResult searchResult = ExtSearchResult{ Tracks: tracks, Total: len(tracks), @@ -205,7 +184,6 @@ func (p *ExtensionProviderWrapper) SearchTracks(query string, limit int) (*ExtSe return &searchResult, nil } -// GetTrack gets track details by ID func (p *ExtensionProviderWrapper) GetTrack(trackID string) (*ExtTrackMetadata, error) { if !p.extension.Manifest.IsMetadataProvider() { return nil, fmt.Errorf("extension '%s' is not a metadata provider", p.extension.ID) @@ -215,7 +193,6 @@ func (p *ExtensionProviderWrapper) GetTrack(trackID string) (*ExtTrackMetadata, return nil, fmt.Errorf("extension '%s' is disabled", p.extension.ID) } - // Lock VM to prevent concurrent access p.extension.VMMu.Lock() defer p.extension.VMMu.Unlock() @@ -255,7 +232,6 @@ func (p *ExtensionProviderWrapper) GetTrack(trackID string) (*ExtTrackMetadata, return &track, nil } -// GetAlbum gets album details by ID func (p *ExtensionProviderWrapper) GetAlbum(albumID string) (*ExtAlbumMetadata, error) { if !p.extension.Manifest.IsMetadataProvider() { return nil, fmt.Errorf("extension '%s' is not a metadata provider", p.extension.ID) @@ -265,7 +241,6 @@ func (p *ExtensionProviderWrapper) GetAlbum(albumID string) (*ExtAlbumMetadata, return nil, fmt.Errorf("extension '%s' is disabled", p.extension.ID) } - // Lock VM to prevent concurrent access p.extension.VMMu.Lock() defer p.extension.VMMu.Unlock() @@ -308,7 +283,6 @@ func (p *ExtensionProviderWrapper) GetAlbum(albumID string) (*ExtAlbumMetadata, return &album, nil } -// GetArtist gets artist details by ID func (p *ExtensionProviderWrapper) GetArtist(artistID string) (*ExtArtistMetadata, error) { if !p.extension.Manifest.IsMetadataProvider() { return nil, fmt.Errorf("extension '%s' is not a metadata provider", p.extension.ID) @@ -318,7 +292,6 @@ func (p *ExtensionProviderWrapper) GetArtist(artistID string) (*ExtArtistMetadat return nil, fmt.Errorf("extension '%s' is disabled", p.extension.ID) } - // Lock VM to prevent concurrent access p.extension.VMMu.Lock() defer p.extension.VMMu.Unlock() @@ -358,23 +331,18 @@ func (p *ExtensionProviderWrapper) GetArtist(artistID string) (*ExtArtistMetadat return &artist, nil } -// EnrichTrack enriches track metadata before download (e.g., fetch real ISRC) -// This is called lazily when download starts, not when playlist/album is loaded -// Extension should implement enrichTrack(track) function that returns enriched track func (p *ExtensionProviderWrapper) EnrichTrack(track *ExtTrackMetadata) (*ExtTrackMetadata, error) { if !p.extension.Manifest.IsMetadataProvider() { - return track, nil // Not a metadata provider, return as-is + return track, nil } if !p.extension.Enabled { - return track, nil // Extension disabled, return as-is + return track, nil } - // Lock VM to prevent concurrent access p.extension.VMMu.Lock() defer p.extension.VMMu.Unlock() - // Convert track to JSON for passing to JS trackJSON, err := json.Marshal(track) if err != nil { GoLog("[Extension] EnrichTrack: failed to marshal track: %v\n", err) @@ -401,7 +369,6 @@ func (p *ExtensionProviderWrapper) EnrichTrack(track *ExtTrackMetadata) (*ExtTra return track, nil } - // If extension doesn't implement enrichTrack or returns null, return original if result == nil || goja.IsUndefined(result) || goja.IsNull(result) { return track, nil } @@ -419,18 +386,11 @@ func (p *ExtensionProviderWrapper) EnrichTrack(track *ExtTrackMetadata) (*ExtTra return track, nil } - // Preserve provider ID enrichedTrack.ProviderID = track.ProviderID - GoLog("[Extension] EnrichTrack: enriched track from %s (ISRC: %s -> %s)\n", - p.extension.ID, track.ISRC, enrichedTrack.ISRC) - return &enrichedTrack, nil } -// ==================== Download Provider Methods ==================== - -// CheckAvailability checks if a track is available for download func (p *ExtensionProviderWrapper) CheckAvailability(isrc, trackName, artistName string) (*ExtAvailabilityResult, error) { if !p.extension.Manifest.IsDownloadProvider() { return nil, fmt.Errorf("extension '%s' is not a download provider", p.extension.ID) @@ -440,7 +400,6 @@ func (p *ExtensionProviderWrapper) CheckAvailability(isrc, trackName, artistName return nil, fmt.Errorf("extension '%s' is disabled", p.extension.ID) } - // Lock VM to prevent concurrent access p.extension.VMMu.Lock() defer p.extension.VMMu.Unlock() @@ -479,7 +438,6 @@ func (p *ExtensionProviderWrapper) CheckAvailability(isrc, trackName, artistName return &availability, nil } -// GetDownloadURL gets the download URL for a track func (p *ExtensionProviderWrapper) GetDownloadURL(trackID, quality string) (*ExtDownloadURLResult, error) { if !p.extension.Manifest.IsDownloadProvider() { return nil, fmt.Errorf("extension '%s' is not a download provider", p.extension.ID) @@ -489,7 +447,6 @@ func (p *ExtensionProviderWrapper) GetDownloadURL(trackID, quality string) (*Ext return nil, fmt.Errorf("extension '%s' is disabled", p.extension.ID) } - // Lock VM to prevent concurrent access p.extension.VMMu.Lock() defer p.extension.VMMu.Unlock() @@ -528,10 +485,8 @@ func (p *ExtensionProviderWrapper) GetDownloadURL(trackID, quality string) (*Ext return &urlResult, nil } -// ExtDownloadTimeout is longer for extension download operations (5 minutes) const ExtDownloadTimeout = 5 * time.Minute -// Download downloads a track with progress reporting func (p *ExtensionProviderWrapper) Download(trackID, quality, outputPath string, onProgress func(percent int)) (*ExtDownloadResult, error) { if !p.extension.Manifest.IsDownloadProvider() { return nil, fmt.Errorf("extension '%s' is not a download provider", p.extension.ID) @@ -541,15 +496,12 @@ func (p *ExtensionProviderWrapper) Download(trackID, quality, outputPath string, return nil, fmt.Errorf("extension '%s' is disabled", p.extension.ID) } - // Lock VM to prevent concurrent access p.extension.VMMu.Lock() defer p.extension.VMMu.Unlock() - // Set up progress callback in VM p.vm.Set("__onProgress", func(call goja.FunctionCall) goja.Value { if len(call.Arguments) > 0 { percent := int(call.Arguments[0].ToInteger()) - // Clamp to 0-100 if percent < 0 { percent = 0 } @@ -572,7 +524,6 @@ func (p *ExtensionProviderWrapper) Download(trackID, quality, outputPath string, })() `, trackID, quality, outputPath) - // Use longer timeout for downloads (5 minutes) result, err := RunWithTimeoutAndRecover(p.vm, script, ExtDownloadTimeout) if err != nil { errMsg := err.Error() @@ -618,9 +569,6 @@ func (p *ExtensionProviderWrapper) Download(trackID, quality, outputPath string, return &downloadResult, nil } -// ==================== Extension Manager Provider Methods ==================== - -// GetMetadataProviders returns all enabled metadata provider extensions func (m *ExtensionManager) GetMetadataProviders() []*ExtensionProviderWrapper { m.mu.RLock() defer m.mu.RUnlock() @@ -634,7 +582,6 @@ func (m *ExtensionManager) GetMetadataProviders() []*ExtensionProviderWrapper { return providers } -// GetDownloadProviders returns all enabled download provider extensions func (m *ExtensionManager) GetDownloadProviders() []*ExtensionProviderWrapper { m.mu.RLock() defer m.mu.RUnlock() @@ -648,7 +595,6 @@ func (m *ExtensionManager) GetDownloadProviders() []*ExtensionProviderWrapper { return providers } -// SearchTracksWithExtensions searches all metadata providers func (m *ExtensionManager) SearchTracksWithExtensions(query string, limit int) ([]ExtTrackMetadata, error) { providers := m.GetMetadataProviders() if len(providers) == 0 { @@ -670,18 +616,12 @@ func (m *ExtensionManager) SearchTracksWithExtensions(query string, limit int) ( return allTracks, nil } -// ==================== Provider Priority ==================== - -// providerPriority stores the order of download providers var providerPriority []string var providerPriorityMu sync.RWMutex -// metadataProviderPriority stores the order of metadata providers var metadataProviderPriority []string var metadataProviderPriorityMu sync.RWMutex -// SetProviderPriority sets the order of download providers -// providerIDs should include both built-in ("tidal", "qobuz", "amazon") and extension IDs func SetProviderPriority(providerIDs []string) { providerPriorityMu.Lock() defer providerPriorityMu.Unlock() @@ -689,13 +629,11 @@ func SetProviderPriority(providerIDs []string) { GoLog("[Extension] Download provider priority set: %v\n", providerIDs) } -// GetProviderPriority returns the current provider priority order func GetProviderPriority() []string { providerPriorityMu.RLock() defer providerPriorityMu.RUnlock() if len(providerPriority) == 0 { - // Default order: built-in providers first return []string{"tidal", "qobuz", "amazon"} } @@ -704,8 +642,6 @@ func GetProviderPriority() []string { return result } -// SetMetadataProviderPriority sets the order of metadata providers -// providerIDs should include both built-in ("spotify", "deezer") and extension IDs func SetMetadataProviderPriority(providerIDs []string) { metadataProviderPriorityMu.Lock() defer metadataProviderPriorityMu.Unlock() @@ -713,13 +649,11 @@ func SetMetadataProviderPriority(providerIDs []string) { GoLog("[Extension] Metadata provider priority set: %v\n", providerIDs) } -// GetMetadataProviderPriority returns the current metadata provider priority order func GetMetadataProviderPriority() []string { metadataProviderPriorityMu.RLock() defer metadataProviderPriorityMu.RUnlock() if len(metadataProviderPriority) == 0 { - // Default order: built-in providers first return []string{"deezer", "spotify"} } @@ -728,7 +662,6 @@ func GetMetadataProviderPriority() []string { return result } -// isBuiltInProvider checks if a provider ID is a built-in provider func isBuiltInProvider(providerID string) bool { switch providerID { case "tidal", "qobuz", "amazon", "deezer": @@ -738,20 +671,12 @@ func isBuiltInProvider(providerID string) bool { } } -// ==================== Download with Fallback ==================== - -// DownloadWithExtensionFallback tries to download from providers in priority order -// Includes both built-in providers and extension providers -// If req.Source is set (extension ID), that extension is tried first func DownloadWithExtensionFallback(req DownloadRequest) (*DownloadResponse, error) { priority := GetProviderPriority() extManager := GetExtensionManager() - // If req.Service is a built-in provider, prioritize it first - // This handles user's explicit selection from the service picker if req.Service != "" && isBuiltInProvider(req.Service) { GoLog("[DownloadWithExtensionFallback] User selected service: %s, prioritizing it first\n", req.Service) - // Reorder priority to put req.Service first newPriority := []string{req.Service} for _, p := range priority { if p != req.Service { @@ -763,10 +688,8 @@ func DownloadWithExtensionFallback(req DownloadRequest) (*DownloadResponse, erro } var lastErr error - var skipBuiltIn bool // If source extension has skipBuiltInFallback, don't try built-in providers + var skipBuiltIn bool - // LAZY ENRICHMENT: If track came from an extension, try to enrich metadata (e.g., get real ISRC) - // This is done lazily at download time, not when playlist/album is loaded if req.Source != "" && !isBuiltInProvider(req.Source) { ext, err := extManager.GetExtension(req.Source) if err == nil && ext.Enabled && ext.Error == "" && ext.Manifest.IsMetadataProvider() { @@ -810,7 +733,6 @@ func DownloadWithExtensionFallback(req DownloadRequest) (*DownloadResponse, erro if enrichedTrack.Artists != "" { req.ArtistName = enrichedTrack.Artists } - // Copy extended metadata from enrichment (label, copyright, genre, release_date) if enrichedTrack.Label != "" && req.Label == "" { GoLog("[DownloadWithExtensionFallback] Label from enrichment: %s\n", enrichedTrack.Label) req.Label = enrichedTrack.Label @@ -831,7 +753,6 @@ func DownloadWithExtensionFallback(req DownloadRequest) (*DownloadResponse, erro } } - // If source extension is specified, try it first before the priority list if req.Source != "" && !isBuiltInProvider(req.Source) { GoLog("[DownloadWithExtensionFallback] Track source is extension '%s', trying it first\n", req.Source) @@ -841,7 +762,6 @@ func DownloadWithExtensionFallback(req DownloadRequest) (*DownloadResponse, erro provider := NewExtensionProviderWrapper(ext) - // For tracks from extension search, use the track ID directly (e.g., "youtube:VIDEO_ID") trackID := req.SpotifyID GoLog("[DownloadWithExtensionFallback] Downloading from source extension with trackID: %s (skipBuiltInFallback: %v)\n", trackID, skipBuiltIn) @@ -867,7 +787,6 @@ func DownloadWithExtensionFallback(req DownloadRequest) (*DownloadResponse, erro Copyright: req.Copyright, } - // Embed genre and label if provided (from Deezer metadata) if req.Genre != "" || req.Label != "" { if err := EmbedGenreLabel(result.FilePath, req.Genre, req.Label); err != nil { GoLog("[DownloadWithExtensionFallback] Warning: failed to embed genre/label: %v\n", err) @@ -925,12 +844,11 @@ func DownloadWithExtensionFallback(req DownloadRequest) (*DownloadResponse, erro } GoLog("[DownloadWithExtensionFallback] Source extension %s failed: %v\n", req.Source, lastErr) - // If skipBuiltInFallback is true, don't continue to other providers if skipBuiltIn { GoLog("[DownloadWithExtensionFallback] skipBuiltInFallback is true, not trying other providers\n") return &DownloadResponse{ Success: false, - Error: fmt.Sprintf("Download failed: %v", lastErr), + Error: "Download failed: " + lastErr.Error(), ErrorType: "extension_error", Service: req.Source, }, nil @@ -940,7 +858,6 @@ func DownloadWithExtensionFallback(req DownloadRequest) (*DownloadResponse, erro } } - // Continue with priority list for _, providerID := range priority { if providerID == req.Source { continue @@ -954,7 +871,6 @@ func DownloadWithExtensionFallback(req DownloadRequest) (*DownloadResponse, erro GoLog("[DownloadWithExtensionFallback] Trying provider: %s\n", providerID) if isBuiltInProvider(providerID) { - // For built-in providers, enrich with Deezer metadata if not already present if (req.Genre == "" || req.Label == "") && req.ISRC != "" { GoLog("[DownloadWithExtensionFallback] Enriching extended metadata from Deezer for ISRC: %s\n", req.ISRC) ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) @@ -975,11 +891,9 @@ func DownloadWithExtensionFallback(req DownloadRequest) (*DownloadResponse, erro } } - // Use built-in provider result, err := tryBuiltInProvider(providerID, req) if err == nil && result.Success { result.Service = providerID - // Copy enriched metadata to response for Flutter (needed for M4A->FLAC conversion) if req.Label != "" { result.Label = req.Label } @@ -1007,7 +921,6 @@ func DownloadWithExtensionFallback(req DownloadRequest) (*DownloadResponse, erro GoLog("[DownloadWithExtensionFallback] %s failed: %v\n", providerID, err) } } else { - // Try extension provider ext, err := extManager.GetExtension(providerID) if err != nil || !ext.Enabled || ext.Error != "" { GoLog("[DownloadWithExtensionFallback] Extension %s not available\n", providerID) @@ -1050,7 +963,6 @@ func DownloadWithExtensionFallback(req DownloadRequest) (*DownloadResponse, erro Copyright: req.Copyright, } - // Embed genre and label if provided (from Deezer metadata) if req.Genre != "" || req.Label != "" { if err := EmbedGenreLabel(result.FilePath, req.Genre, req.Label); err != nil { GoLog("[DownloadWithExtensionFallback] Warning: failed to embed genre/label: %v\n", err) @@ -1061,7 +973,6 @@ func DownloadWithExtensionFallback(req DownloadRequest) (*DownloadResponse, erro if ext.Manifest.SkipMetadataEnrichment { resp.SkipMetadataEnrichment = true - // Copy metadata from extension result if provided if result.Title != "" { resp.Title = result.Title } @@ -1114,7 +1025,7 @@ func DownloadWithExtensionFallback(req DownloadRequest) (*DownloadResponse, erro if lastErr != nil { return &DownloadResponse{ Success: false, - Error: fmt.Sprintf("All providers failed. Last error: %v", lastErr), + Error: "All providers failed. Last error: " + lastErr.Error(), ErrorType: "not_found", }, nil } @@ -1126,7 +1037,6 @@ func DownloadWithExtensionFallback(req DownloadRequest) (*DownloadResponse, erro }, nil } -// tryBuiltInProvider attempts download from a built-in provider func tryBuiltInProvider(providerID string, req DownloadRequest) (*DownloadResponse, error) { req.Service = providerID @@ -1212,7 +1122,6 @@ func tryBuiltInProvider(providerID string, req DownloadRequest) (*DownloadRespon }, nil } -// buildOutputPath builds the output file path from request func buildOutputPath(req DownloadRequest) string { metadata := map[string]interface{}{ "title": req.TrackName, @@ -1232,9 +1141,6 @@ func buildOutputPath(req DownloadRequest) string { return fmt.Sprintf("%s/%s.flac", req.OutputDir, filename) } -// ==================== Custom Search ==================== - -// CustomSearch performs a custom search using an extension's search function func (p *ExtensionProviderWrapper) CustomSearch(query string, options map[string]interface{}) ([]ExtTrackMetadata, error) { if !p.extension.Manifest.HasCustomSearch() { return nil, fmt.Errorf("extension '%s' does not support custom search", p.extension.ID) @@ -1244,11 +1150,9 @@ func (p *ExtensionProviderWrapper) CustomSearch(query string, options map[string return nil, fmt.Errorf("extension '%s' is disabled", p.extension.ID) } - // Lock VM to prevent concurrent access p.extension.VMMu.Lock() defer p.extension.VMMu.Unlock() - // Convert options to JSON optionsJSON, _ := json.Marshal(options) script := fmt.Sprintf(` @@ -1294,20 +1198,16 @@ func (p *ExtensionProviderWrapper) CustomSearch(query string, options map[string return tracks, nil } -// ==================== Custom URL Handler ==================== - -// ExtURLHandleResult represents the result of URL handling type ExtURLHandleResult struct { - Type string `json:"type"` // "track", "album", "playlist", "artist" - Track *ExtTrackMetadata `json:"track,omitempty"` // For single track - Tracks []ExtTrackMetadata `json:"tracks,omitempty"` // For album/playlist - Album *ExtAlbumMetadata `json:"album,omitempty"` // Album info - Artist *ExtArtistMetadata `json:"artist,omitempty"` // Artist info - Name string `json:"name,omitempty"` // Playlist/album name - CoverURL string `json:"cover_url,omitempty"` // Cover image + Type string `json:"type"` + Track *ExtTrackMetadata `json:"track,omitempty"` + Tracks []ExtTrackMetadata `json:"tracks,omitempty"` + Album *ExtAlbumMetadata `json:"album,omitempty"` + Artist *ExtArtistMetadata `json:"artist,omitempty"` + Name string `json:"name,omitempty"` + CoverURL string `json:"cover_url,omitempty"` } -// HandleURL processes a URL using the extension's URL handler func (p *ExtensionProviderWrapper) HandleURL(url string) (*ExtURLHandleResult, error) { if !p.extension.Manifest.HasURLHandler() { return nil, fmt.Errorf("extension '%s' does not support URL handling", p.extension.ID) @@ -1317,7 +1217,6 @@ func (p *ExtensionProviderWrapper) HandleURL(url string) (*ExtURLHandleResult, e return nil, fmt.Errorf("extension '%s' is disabled", p.extension.ID) } - // Lock VM to prevent concurrent access p.extension.VMMu.Lock() defer p.extension.VMMu.Unlock() @@ -1381,9 +1280,6 @@ func (p *ExtensionProviderWrapper) HandleURL(url string) (*ExtURLHandleResult, e return &handleResult, nil } -// ==================== Custom Track Matching ==================== - -// MatchTrackResult represents the result of custom track matching type MatchTrackResult struct { Matched bool `json:"matched"` TrackID string `json:"track_id,omitempty"` @@ -1391,7 +1287,6 @@ type MatchTrackResult struct { Reason string `json:"reason,omitempty"` } -// MatchTrack uses extension's custom matching algorithm func (p *ExtensionProviderWrapper) MatchTrack(sourceTrack map[string]interface{}, candidates []map[string]interface{}) (*MatchTrackResult, error) { if !p.extension.Manifest.HasCustomMatching() { return nil, fmt.Errorf("extension '%s' does not support custom matching", p.extension.ID) @@ -1401,7 +1296,6 @@ func (p *ExtensionProviderWrapper) MatchTrack(sourceTrack map[string]interface{} return nil, fmt.Errorf("extension '%s' is disabled", p.extension.ID) } - // Lock VM to prevent concurrent access p.extension.VMMu.Lock() defer p.extension.VMMu.Unlock() @@ -1443,22 +1337,16 @@ func (p *ExtensionProviderWrapper) MatchTrack(sourceTrack map[string]interface{} return &matchResult, nil } -// ==================== Post-Processing ==================== - -// PostProcessResult represents the result of post-processing type PostProcessResult struct { Success bool `json:"success"` NewFilePath string `json:"new_file_path,omitempty"` Error string `json:"error,omitempty"` - // Additional metadata that may have changed - BitDepth int `json:"bit_depth,omitempty"` - SampleRate int `json:"sample_rate,omitempty"` + BitDepth int `json:"bit_depth,omitempty"` + SampleRate int `json:"sample_rate,omitempty"` } -// PostProcessTimeout is longer for post-processing (2 minutes) const PostProcessTimeout = 2 * time.Minute -// PostProcess runs post-processing hooks on a downloaded file func (p *ExtensionProviderWrapper) PostProcess(filePath string, metadata map[string]interface{}, hookID string) (*PostProcessResult, error) { if !p.extension.Manifest.HasPostProcessing() { return nil, fmt.Errorf("extension '%s' does not support post-processing", p.extension.ID) @@ -1468,7 +1356,6 @@ func (p *ExtensionProviderWrapper) PostProcess(filePath string, metadata map[str return nil, fmt.Errorf("extension '%s' is disabled", p.extension.ID) } - // Lock VM to prevent concurrent access p.extension.VMMu.Lock() defer p.extension.VMMu.Unlock() @@ -1522,9 +1409,6 @@ func (p *ExtensionProviderWrapper) PostProcess(filePath string, metadata map[str return &postResult, nil } -// ==================== Extension Manager Advanced Methods ==================== - -// GetSearchProviders returns all extensions that provide custom search func (m *ExtensionManager) GetSearchProviders() []*ExtensionProviderWrapper { m.mu.RLock() defer m.mu.RUnlock() @@ -1538,7 +1422,6 @@ func (m *ExtensionManager) GetSearchProviders() []*ExtensionProviderWrapper { return providers } -// GetURLHandlers returns all extensions that handle custom URLs func (m *ExtensionManager) GetURLHandlers() []*ExtensionProviderWrapper { m.mu.RLock() defer m.mu.RUnlock() @@ -1552,7 +1435,6 @@ func (m *ExtensionManager) GetURLHandlers() []*ExtensionProviderWrapper { return providers } -// FindURLHandler finds an extension that can handle the given URL func (m *ExtensionManager) FindURLHandler(url string) *ExtensionProviderWrapper { m.mu.RLock() defer m.mu.RUnlock() @@ -1565,14 +1447,11 @@ func (m *ExtensionManager) FindURLHandler(url string) *ExtensionProviderWrapper return nil } -// ExtURLHandleResultWithExtID wraps ExtURLHandleResult with extension ID for gomobile compatibility type ExtURLHandleResultWithExtID struct { Result *ExtURLHandleResult ExtensionID string } -// HandleURLWithExtension tries to handle a URL with any matching extension -// Returns result with extension ID, or error if no handler found func (m *ExtensionManager) HandleURLWithExtension(url string) (*ExtURLHandleResultWithExtID, error) { handler := m.FindURLHandler(url) if handler == nil { diff --git a/go_backend/extension_runtime_ffmpeg.go b/go_backend/extension_runtime_ffmpeg.go index f5a5b578..19e1b67c 100644 --- a/go_backend/extension_runtime_ffmpeg.go +++ b/go_backend/extension_runtime_ffmpeg.go @@ -64,7 +64,6 @@ func (r *ExtensionRuntime) ffmpegExecute(call goja.FunctionCall) goja.Value { command := call.Arguments[0].String() - // Generate unique command ID ffmpegCommandsMu.Lock() ffmpegCommandID++ cmdID := fmt.Sprintf("%s_%d", r.extensionID, ffmpegCommandID) @@ -77,7 +76,6 @@ func (r *ExtensionRuntime) ffmpegExecute(call goja.FunctionCall) goja.Value { GoLog("[Extension:%s] FFmpeg command queued: %s\n", r.extensionID, cmdID) - // Wait for completion (with timeout) timeout := 5 * time.Minute start := time.Now() for { @@ -97,7 +95,6 @@ func (r *ExtensionRuntime) ffmpegExecute(call goja.FunctionCall) goja.Value { } ffmpegCommandsMu.RUnlock() - // Cleanup ClearFFmpegCommand(cmdID) return r.vm.ToValue(result) } @@ -124,7 +121,6 @@ func (r *ExtensionRuntime) ffmpegGetInfo(call goja.FunctionCall) goja.Value { filePath := call.Arguments[0].String() - // Use Go's built-in audio quality function quality, err := GetAudioQuality(filePath) if err != nil { return r.vm.ToValue(map[string]interface{}{ @@ -153,7 +149,6 @@ func (r *ExtensionRuntime) ffmpegConvert(call goja.FunctionCall) goja.Value { inputPath := call.Arguments[0].String() outputPath := call.Arguments[1].String() - // Get options if provided options := map[string]interface{}{} if len(call.Arguments) > 2 && !goja.IsUndefined(call.Arguments[2]) && !goja.IsNull(call.Arguments[2]) { if opts, ok := call.Arguments[2].Export().(map[string]interface{}); ok { @@ -161,36 +156,29 @@ func (r *ExtensionRuntime) ffmpegConvert(call goja.FunctionCall) goja.Value { } } - // Build FFmpeg command var cmdParts []string cmdParts = append(cmdParts, "-i", fmt.Sprintf("%q", inputPath)) - // Audio codec if codec, ok := options["codec"].(string); ok { cmdParts = append(cmdParts, "-c:a", codec) } - // Bitrate if bitrate, ok := options["bitrate"].(string); ok { cmdParts = append(cmdParts, "-b:a", bitrate) } - // Sample rate if sampleRate, ok := options["sample_rate"].(float64); ok { cmdParts = append(cmdParts, "-ar", fmt.Sprintf("%d", int(sampleRate))) } - // Channels if channels, ok := options["channels"].(float64); ok { cmdParts = append(cmdParts, "-ac", fmt.Sprintf("%d", int(channels))) } - // Overwrite output cmdParts = append(cmdParts, "-y", fmt.Sprintf("%q", outputPath)) command := strings.Join(cmdParts, " ") - // Execute via ffmpegExecute execCall := goja.FunctionCall{ Arguments: []goja.Value{r.vm.ToValue(command)}, } diff --git a/go_backend/extension_runtime_file.go b/go_backend/extension_runtime_file.go index 442aa8a6..46b4f644 100644 --- a/go_backend/extension_runtime_file.go +++ b/go_backend/extension_runtime_file.go @@ -349,7 +349,6 @@ func (r *ExtensionRuntime) fileWrite(call goja.FunctionCall) goja.Value { }) } - // Create directory if needed dir := filepath.Dir(fullPath) if err := os.MkdirAll(dir, 0755); err != nil { return r.vm.ToValue(map[string]interface{}{ diff --git a/go_backend/extension_runtime_polyfills.go b/go_backend/extension_runtime_polyfills.go index 5293841e..e75d679a 100644 --- a/go_backend/extension_runtime_polyfills.go +++ b/go_backend/extension_runtime_polyfills.go @@ -40,7 +40,6 @@ func (r *ExtensionRuntime) fetchPolyfill(call goja.FunctionCall) goja.Value { if len(call.Arguments) > 1 && !goja.IsUndefined(call.Arguments[1]) && !goja.IsNull(call.Arguments[1]) { optionsObj := call.Arguments[1].Export() if opts, ok := optionsObj.(map[string]interface{}); ok { - // Method if m, ok := opts["method"].(string); ok { method = strings.ToUpper(m) } @@ -61,7 +60,6 @@ func (r *ExtensionRuntime) fetchPolyfill(call goja.FunctionCall) goja.Value { } } - // Headers if h, ok := opts["headers"]; ok && h != nil { switch hv := h.(type) { case map[string]interface{}: @@ -73,7 +71,6 @@ func (r *ExtensionRuntime) fetchPolyfill(call goja.FunctionCall) goja.Value { } } - // Create HTTP request var reqBody io.Reader if bodyStr != "" { reqBody = strings.NewReader(bodyStr) @@ -84,7 +81,6 @@ func (r *ExtensionRuntime) fetchPolyfill(call goja.FunctionCall) goja.Value { return r.createFetchError(err.Error()) } - // Set headers - user headers first for k, v := range headers { req.Header.Set(k, v) } @@ -96,20 +92,17 @@ func (r *ExtensionRuntime) fetchPolyfill(call goja.FunctionCall) goja.Value { req.Header.Set("Content-Type", "application/json") } - // Execute request resp, err := r.httpClient.Do(req) if err != nil { return r.createFetchError(err.Error()) } defer resp.Body.Close() - // Read body body, err := io.ReadAll(resp.Body) if err != nil { return r.createFetchError(err.Error()) } - // Extract response headers respHeaders := make(map[string]interface{}) for k, v := range resp.Header { if len(v) == 1 { @@ -127,15 +120,12 @@ func (r *ExtensionRuntime) fetchPolyfill(call goja.FunctionCall) goja.Value { responseObj.Set("headers", respHeaders) responseObj.Set("url", urlStr) - // Store body for methods bodyString := string(body) - // text() method - returns body as string responseObj.Set("text", func(call goja.FunctionCall) goja.Value { return r.vm.ToValue(bodyString) }) - // json() method - parses body as JSON responseObj.Set("json", func(call goja.FunctionCall) goja.Value { var result interface{} if err := json.Unmarshal(body, &result); err != nil { @@ -145,7 +135,6 @@ func (r *ExtensionRuntime) fetchPolyfill(call goja.FunctionCall) goja.Value { return r.vm.ToValue(result) }) - // arrayBuffer() method - returns body as array (simplified) responseObj.Set("arrayBuffer", func(call goja.FunctionCall) goja.Value { // Return as array of bytes byteArray := make([]interface{}, len(body)) @@ -208,7 +197,6 @@ func (r *ExtensionRuntime) registerTextEncoderDecoder(vm *goja.Runtime) { encoder := call.This encoder.Set("encoding", "utf-8") - // encode() method - string to Uint8Array encoder.Set("encode", func(call goja.FunctionCall) goja.Value { if len(call.Arguments) < 1 { return vm.ToValue([]byte{}) @@ -224,7 +212,6 @@ func (r *ExtensionRuntime) registerTextEncoderDecoder(vm *goja.Runtime) { return vm.ToValue(result) }) - // encodeInto() method encoder.Set("encodeInto", func(call goja.FunctionCall) goja.Value { // Simplified implementation if len(call.Arguments) < 2 { @@ -253,7 +240,6 @@ func (r *ExtensionRuntime) registerTextEncoderDecoder(vm *goja.Runtime) { decoder.Set("fatal", false) decoder.Set("ignoreBOM", false) - // decode() method - Uint8Array to string decoder.Set("decode", func(call goja.FunctionCall) goja.Value { if len(call.Arguments) < 1 { return vm.ToValue("") @@ -292,7 +278,6 @@ func (r *ExtensionRuntime) registerTextEncoderDecoder(vm *goja.Runtime) { }) } -// registerURLClass registers the URL class for URL parsing func (r *ExtensionRuntime) registerURLClass(vm *goja.Runtime) { vm.Set("URL", func(call goja.ConstructorCall) *goja.Object { urlObj := call.This @@ -322,7 +307,6 @@ func (r *ExtensionRuntime) registerURLClass(vm *goja.Runtime) { return nil } - // Set URL properties urlObj.Set("href", parsed.String()) urlObj.Set("protocol", parsed.Scheme+":") urlObj.Set("host", parsed.Host) @@ -342,10 +326,9 @@ func (r *ExtensionRuntime) registerURLClass(vm *goja.Runtime) { password, _ := parsed.User.Password() urlObj.Set("password", password) - // searchParams object - searchParams := vm.NewObject() queryValues := parsed.Query() + searchParams := vm.NewObject() searchParams.Set("get", func(call goja.FunctionCall) goja.Value { if len(call.Arguments) < 1 { return goja.Null() @@ -379,12 +362,10 @@ func (r *ExtensionRuntime) registerURLClass(vm *goja.Runtime) { urlObj.Set("searchParams", searchParams) - // toString method urlObj.Set("toString", func(call goja.FunctionCall) goja.Value { return vm.ToValue(parsed.String()) }) - // toJSON method urlObj.Set("toJSON", func(call goja.FunctionCall) goja.Value { return vm.ToValue(parsed.String()) }) @@ -392,17 +373,14 @@ func (r *ExtensionRuntime) registerURLClass(vm *goja.Runtime) { return nil }) - // URLSearchParams constructor vm.Set("URLSearchParams", func(call goja.ConstructorCall) *goja.Object { paramsObj := call.This values := url.Values{} - // Parse initial value if provided if len(call.Arguments) > 0 && !goja.IsUndefined(call.Arguments[0]) { init := call.Arguments[0].Export() switch v := init.(type) { case string: - // Parse query string parsed, _ := url.ParseQuery(strings.TrimPrefix(v, "?")) values = parsed case map[string]interface{}: diff --git a/go_backend/library_scan.go b/go_backend/library_scan.go index 20c33f20..27828d5e 100644 --- a/go_backend/library_scan.go +++ b/go_backend/library_scan.go @@ -174,7 +174,6 @@ func ScanLibraryFolder(folderPath string) (string, error) { return string(jsonBytes), nil } -// scanAudioFile reads metadata from a single audio file func scanAudioFile(filePath, scanTime string) (*LibraryScanResult, error) { ext := strings.ToLower(filepath.Ext(filePath)) @@ -209,7 +208,6 @@ func scanAudioFile(filePath, scanTime string) (*LibraryScanResult, error) { } } -// scanFLACFile reads metadata from FLAC file func scanFLACFile(filePath string, result *LibraryScanResult) (*LibraryScanResult, error) { metadata, err := ReadMetadata(filePath) if err != nil { @@ -248,7 +246,6 @@ func scanFLACFile(filePath string, result *LibraryScanResult) (*LibraryScanResul return result, nil } -// scanM4AFile reads metadata from M4A/AAC file func scanM4AFile(filePath string, result *LibraryScanResult) (*LibraryScanResult, error) { quality, err := GetM4AQuality(filePath) if err == nil { @@ -259,7 +256,6 @@ func scanM4AFile(filePath string, result *LibraryScanResult) (*LibraryScanResult return scanFromFilename(filePath, result) } -// scanMP3File reads metadata from MP3 file (ID3 tags) func scanMP3File(filePath string, result *LibraryScanResult) (*LibraryScanResult, error) { metadata, err := ReadID3Tags(filePath) if err != nil { @@ -301,7 +297,6 @@ func scanMP3File(filePath string, result *LibraryScanResult) (*LibraryScanResult return result, nil } -// scanOggFile reads metadata from Ogg Vorbis/Opus file (Vorbis comments) func scanOggFile(filePath string, result *LibraryScanResult) (*LibraryScanResult, error) { metadata, err := ReadOggVorbisComments(filePath) if err != nil { @@ -339,7 +334,6 @@ func scanOggFile(filePath string, result *LibraryScanResult) (*LibraryScanResult return result, nil } -// scanFromFilename extracts title/artist from filename pattern func scanFromFilename(filePath string, result *LibraryScanResult) (*LibraryScanResult, error) { filename := strings.TrimSuffix(filepath.Base(filePath), filepath.Ext(filePath)) @@ -371,7 +365,6 @@ func scanFromFilename(filePath string, result *LibraryScanResult) (*LibraryScanR return result, nil } -// isNumeric checks if string contains only digits func isNumeric(s string) bool { for _, c := range s { if c < '0' || c > '9' { @@ -381,12 +374,10 @@ func isNumeric(s string) bool { return len(s) > 0 } -// generateLibraryID creates a unique ID for a library item func generateLibraryID(filePath string) string { return fmt.Sprintf("lib_%x", hashString(filePath)) } -// hashString creates a simple hash of a string func hashString(s string) uint32 { var hash uint32 = 5381 for _, c := range s { @@ -395,7 +386,6 @@ func hashString(s string) uint32 { return hash } -// GetLibraryScanProgress returns current scan progress func GetLibraryScanProgress() string { libraryScanProgressMu.RLock() defer libraryScanProgressMu.RUnlock() @@ -404,7 +394,6 @@ func GetLibraryScanProgress() string { return string(jsonBytes) } -// CancelLibraryScan cancels ongoing library scan func CancelLibraryScan() { libraryScanCancelMu.Lock() defer libraryScanCancelMu.Unlock() @@ -415,8 +404,6 @@ func CancelLibraryScan() { } } -// ReadAudioMetadata reads metadata from any supported audio file -// Returns JSON with track info func ReadAudioMetadata(filePath string) (string, error) { scanTime := time.Now().UTC().Format(time.RFC3339) result, err := scanAudioFile(filePath, scanTime) diff --git a/lib/providers/download_queue_provider.dart b/lib/providers/download_queue_provider.dart index 0661a626..5c8faa1e 100644 --- a/lib/providers/download_queue_provider.dart +++ b/lib/providers/download_queue_provider.dart @@ -1019,10 +1019,6 @@ void removeItem(String id) { _saveQueueToStorage(); } - /// Export failed downloads to a TXT file - /// Returns the file path if successful, null otherwise - /// Uses daily files: same day = append to existing file, new day = new file - /// Saves to 'failed_downloads' subfolder to keep organized Future exportFailedDownloads() async { final failedItems = state.items .where((item) => item.status == DownloadStatus.failed) @@ -1091,7 +1087,6 @@ void removeItem(String id) { } } - /// Clear all failed downloads from queue void clearFailedDownloads() { final items = state.items .where((item) => item.status != DownloadStatus.failed) diff --git a/lib/providers/local_library_provider.dart b/lib/providers/local_library_provider.dart index 1566b451..8bee7fcd 100644 --- a/lib/providers/local_library_provider.dart +++ b/lib/providers/local_library_provider.dart @@ -41,25 +41,20 @@ class LocalLibraryState { .map((item) => MapEntry(item.isrc!, item)), ); - /// Check if ISRC exists in library bool hasIsrc(String isrc) => _isrcSet.contains(isrc); - /// Check if track exists by name and artist bool hasTrack(String trackName, String artistName) { final key = '${trackName.toLowerCase()}|${artistName.toLowerCase()}'; return _trackKeySet.contains(key); } - /// Find library item by ISRC LocalLibraryItem? getByIsrc(String isrc) => _byIsrc[isrc]; - /// Find library item by track name and artist LocalLibraryItem? findByTrackAndArtist(String trackName, String artistName) { final key = '${trackName.toLowerCase()}|${artistName.toLowerCase()}'; return items.where((item) => item.matchKey == key).firstOrNull; } - /// Check if a track exists in library (by ISRC or name matching) bool existsInLibrary({String? isrc, String? trackName, String? artistName}) { if (isrc != null && isrc.isNotEmpty && hasIsrc(isrc)) { return true; @@ -136,13 +131,11 @@ class LocalLibraryNotifier extends Notifier { } } - /// Reload library from database Future reloadFromStorage() async { _isLoaded = false; await _loadFromDatabase(); } - /// Start scanning a folder for audio files Future startScan(String folderPath) async { if (state.isScanning) { _log.w('Scan already in progress'); @@ -230,7 +223,6 @@ class LocalLibraryNotifier extends Notifier { _progressTimer = null; } - /// Cancel ongoing scan Future cancelScan() async { if (!state.isScanning) return; @@ -240,7 +232,6 @@ class LocalLibraryNotifier extends Notifier { _stopProgressPolling(); } - /// Clean up missing files from library Future cleanupMissingFiles() async { final removed = await _db.cleanupMissingFiles(); if (removed > 0) { @@ -249,7 +240,6 @@ class LocalLibraryNotifier extends Notifier { return removed; } - /// Clear all library data Future clearLibrary() async { await _db.clearAll(); @@ -264,7 +254,6 @@ class LocalLibraryNotifier extends Notifier { _log.i('Library cleared'); } - /// Remove a single item from library by ID Future removeItem(String id) async { await _db.delete(id); state = state.copyWith( @@ -272,7 +261,6 @@ class LocalLibraryNotifier extends Notifier { ); } - /// Check if a track exists in library bool existsInLibrary({String? isrc, String? trackName, String? artistName}) { return state.existsInLibrary( isrc: isrc, @@ -281,12 +269,10 @@ class LocalLibraryNotifier extends Notifier { ); } - /// Get library item by ISRC LocalLibraryItem? getByIsrc(String isrc) { return state.getByIsrc(isrc); } - /// Find library item for a track LocalLibraryItem? findExisting({String? isrc, String? trackName, String? artistName}) { if (isrc != null && isrc.isNotEmpty) { final byIsrc = state.getByIsrc(isrc); @@ -298,7 +284,6 @@ class LocalLibraryNotifier extends Notifier { return null; } - /// Search library Future> search(String query) async { if (query.isEmpty) return []; @@ -306,7 +291,6 @@ class LocalLibraryNotifier extends Notifier { return results.map((e) => LocalLibraryItem.fromJson(e)).toList(); } - /// Get library count Future getCount() async { return await _db.getCount(); } diff --git a/lib/screens/album_screen.dart b/lib/screens/album_screen.dart index 0f363c2f..8dd18e77 100644 --- a/lib/screens/album_screen.dart +++ b/lib/screens/album_screen.dart @@ -45,10 +45,10 @@ class AlbumScreen extends ConsumerStatefulWidget { final String albumId; final String albumName; final String? coverUrl; - final List? tracks; // Optional - will fetch if null - final String? extensionId; // If from extension - final String? artistId; // Artist ID for navigation - final String? artistName; // Artist name for navigation + final List? tracks; + final String? extensionId; + final String? artistId; + final String? artistName; const AlbumScreen({ super.key, @@ -93,13 +93,12 @@ class _AlbumScreenState extends ConsumerState { ); }); - // Use provided tracks if not empty, otherwise try cache if (widget.tracks != null && widget.tracks!.isNotEmpty) { _tracks = widget.tracks; } else { _tracks = _AlbumCache.get(widget.albumId); } - _artistId = widget.artistId; // Use provided artist ID if available + _artistId = widget.artistId; if (_tracks == null || _tracks!.isEmpty) { _fetchTracks(); @@ -122,7 +121,7 @@ class _AlbumScreenState extends ConsumerState { } } -Future _extractDominantColor() async { + Future _extractDominantColor() async { if (widget.coverUrl == null) return; final color = await PaletteService.instance.extractDominantColor(widget.coverUrl); if (mounted && color != null) { @@ -131,21 +130,18 @@ Future _extractDominantColor() async { } String _formatReleaseDate(String date) { - // Handle formats: "2024-01-15", "2024-01", "2024" if (date.length >= 10) { - // Full date: 2024-01-15 final parts = date.substring(0, 10).split('-'); if (parts.length == 3) { - return '${parts[2]}/${parts[1]}/${parts[0]}'; // DD/MM/YYYY + return '${parts[2]}/${parts[1]}/${parts[0]}'; } } else if (date.length >= 7) { - // Month: 2024-01 final parts = date.split('-'); if (parts.length >= 2) { - return '${parts[1]}/${parts[0]}'; // MM/YYYY + return '${parts[1]}/${parts[0]}'; } } - return date; // Year only or unknown format + return date; } Future _fetchTracks() async { @@ -164,7 +160,6 @@ Future _fetchTracks() async { final trackList = metadata['track_list'] as List; final tracks = trackList.map((t) => _parseTrack(t as Map)).toList(); - // Extract artist ID from album_info if available final albumInfo = metadata['album_info'] as Map?; final artistId = albumInfo?['artist_id'] as String?; @@ -236,7 +231,7 @@ Future _fetchTracks() async { Widget _buildAppBar(BuildContext context, ColorScheme colorScheme) { final screenWidth = MediaQuery.of(context).size.width; - final coverSize = screenWidth * 0.5; // 50% of screen width + final coverSize = screenWidth * 0.5; final bgColor = _dominantColor ?? colorScheme.surface; return SliverAppBar( @@ -269,7 +264,6 @@ Future _fetchTracks() async { background: Stack( fit: StackFit.expand, children: [ - // Background with dominant color AnimatedContainer( duration: const Duration(milliseconds: 500), decoration: BoxDecoration( @@ -501,11 +495,9 @@ ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(context.l10n.s } void _navigateToArtist(BuildContext context, String artistName) { - // Use stored artist ID if available, otherwise use a placeholder final artistId = _artistId ?? (widget.albumId.startsWith('deezer:') ? 'deezer:unknown' : 'unknown'); - // Don't navigate if artist ID is unknown if (artistId == 'unknown' || artistId == 'deezer:unknown' || artistId.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Artist information not available')), @@ -513,7 +505,6 @@ ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(context.l10n.s return; } - // If from extension, use ExtensionArtistScreen if (widget.extensionId != null) { Navigator.push( context, @@ -621,7 +612,6 @@ class _AlbumTrackItem extends ConsumerWidget { return state.isDownloaded(track.id); })); - // Check local library for duplicate detection final settings = ref.watch(settingsProvider); final showLocalLibraryIndicator = settings.localLibraryEnabled && settings.localLibraryShowDuplicates; final isInLocalLibrary = showLocalLibraryIndicator diff --git a/lib/screens/artist_screen.dart b/lib/screens/artist_screen.dart index 2c1e3088..9f5b8123 100644 --- a/lib/screens/artist_screen.dart +++ b/lib/screens/artist_screen.dart @@ -74,7 +74,7 @@ class ArtistScreen extends ConsumerStatefulWidget { final int? monthlyListeners; final List? albums; final List? topTracks; - final String? extensionId; // If set, skip fetching from Spotify/Deezer + final String? extensionId; const ArtistScreen({ super.key, diff --git a/lib/screens/home_tab.dart b/lib/screens/home_tab.dart index 8a1af0c8..f3196adf 100644 --- a/lib/screens/home_tab.dart +++ b/lib/screens/home_tab.dart @@ -50,19 +50,10 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient late final ProviderSubscription _trackStateSub; late final ProviderSubscription _extensionInitSub; - /// Debounce timer for live search (extension-only feature) Timer? _liveSearchDebounce; - - /// Flag to prevent concurrent live search calls (prevents race conditions in extensions) bool _isLiveSearchInProgress = false; - - /// Pending query to execute after current search completes String? _pendingLiveSearchQuery; - - /// Minimum characters required to trigger live search static const int _minLiveSearchChars = 3; - - /// Debounce duration for live search static const Duration _liveSearchDelay = Duration(milliseconds: 800); List? _recentAccessHistoryCache; diff --git a/lib/screens/playlist_screen.dart b/lib/screens/playlist_screen.dart index 06bfc922..8cc4c74a 100644 --- a/lib/screens/playlist_screen.dart +++ b/lib/screens/playlist_screen.dart @@ -17,7 +17,7 @@ class PlaylistScreen extends ConsumerStatefulWidget { final String playlistName; final String? coverUrl; final List tracks; - final String? playlistId; // Deezer playlist ID for fetching tracks + final String? playlistId; const PlaylistScreen({ super.key, diff --git a/lib/screens/queue_tab.dart b/lib/screens/queue_tab.dart index b95f31a4..58d2292c 100644 --- a/lib/screens/queue_tab.dart +++ b/lib/screens/queue_tab.dart @@ -567,7 +567,6 @@ class _QueueTabState extends ConsumerState { }); } - /// Exit selection mode void _exitSelectionMode() { setState(() { _isSelectionMode = false; @@ -588,25 +587,20 @@ class _QueueTabState extends ConsumerState { }); } - /// Select all visible items void _selectAll(List items) { setState(() { _selectedIds.addAll(items.map((e) => e.id)); }); } - /// Get short badge text for quality display String _getQualityBadgeText(String quality) { - // For lossless: "24-bit/96kHz" -> "24-bit" if (quality.contains('bit')) { return quality.split('/').first; } - // For lossy: "OPUS 128kbps" -> "128k", "MP3 320kbps" -> "320k" final bitrateMatch = RegExp(r'(\d+)kbps').firstMatch(quality); if (bitrateMatch != null) { return '${bitrateMatch.group(1)}k'; } - // Fallback: return format name return quality.split(' ').first; } @@ -725,7 +719,6 @@ class _QueueTabState extends ConsumerState { }); } - /// Count of active advanced filters int get _activeFilterCount { int count = 0; if (_filterSource != null) count++; @@ -735,7 +728,6 @@ class _QueueTabState extends ConsumerState { return count; } - /// Reset all advanced filters void _resetFilters() { setState(() { _filterSource = null; @@ -746,12 +738,10 @@ class _QueueTabState extends ConsumerState { }); } - /// Apply advanced filters to unified items List _applyAdvancedFilters(List items) { if (_activeFilterCount == 0) return items; return items.where((item) { - // Source filter if (_filterSource != null) { if (_filterSource == 'downloaded' && item.source != LibraryItemSource.downloaded) { return false; @@ -761,7 +751,6 @@ class _QueueTabState extends ConsumerState { } } - // Quality filter if (_filterQuality != null && item.quality != null) { final quality = item.quality!.toLowerCase(); switch (_filterQuality) { @@ -770,21 +759,17 @@ class _QueueTabState extends ConsumerState { case 'cd': if (!quality.startsWith('16')) return false; case 'lossy': - // Lossy formats typically don't have bit depth or are labeled differently if (quality.startsWith('24') || quality.startsWith('16')) return false; } } else if (_filterQuality != null && item.quality == null) { - // If quality filter is set but item has no quality info, include only for 'lossy' if (_filterQuality != 'lossy') return false; } - // Format filter if (_filterFormat != null) { final ext = item.filePath.split('.').last.toLowerCase(); if (ext != _filterFormat) return false; } - // Date filter if (_filterDateRange != null) { final now = DateTime.now(); final itemDate = item.addedAt; @@ -808,7 +793,6 @@ class _QueueTabState extends ConsumerState { }).toList(growable: false); } - /// Get available formats from current items Set _getAvailableFormats(List items) { final formats = {}; for (final item in items) { @@ -820,12 +804,10 @@ class _QueueTabState extends ConsumerState { return formats; } - /// Show filter bottom sheet void _showFilterSheet(BuildContext context, List allItems) { final colorScheme = Theme.of(context).colorScheme; final availableFormats = _getAvailableFormats(allItems); - // Temporary filter state for the sheet String? tempSource = _filterSource; String? tempQuality = _filterQuality; String? tempFormat = _filterFormat; @@ -847,7 +829,6 @@ class _QueueTabState extends ConsumerState { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Handle bar Center( child: Container( width: 32, @@ -860,7 +841,6 @@ class _QueueTabState extends ConsumerState { ), ), - // Title row Row( children: [ Text( @@ -885,7 +865,6 @@ class _QueueTabState extends ConsumerState { ), const SizedBox(height: 16), - // Source filter Text( context.l10n.libraryFilterSource, style: Theme.of(context).textTheme.titleSmall?.copyWith( @@ -915,7 +894,6 @@ class _QueueTabState extends ConsumerState { ), const SizedBox(height: 16), - // Quality filter Text( context.l10n.libraryFilterQuality, style: Theme.of(context).textTheme.titleSmall?.copyWith( @@ -950,7 +928,6 @@ class _QueueTabState extends ConsumerState { ), const SizedBox(height: 16), - // Format filter Text( context.l10n.libraryFilterFormat, style: Theme.of(context).textTheme.titleSmall?.copyWith( @@ -976,7 +953,6 @@ class _QueueTabState extends ConsumerState { ), const SizedBox(height: 16), - // Date filter Text( context.l10n.libraryFilterDate, style: Theme.of(context).textTheme.titleSmall?.copyWith( @@ -1016,7 +992,6 @@ class _QueueTabState extends ConsumerState { ), const SizedBox(height: 24), - // Apply button SizedBox( width: double.infinity, child: FilledButton( diff --git a/lib/services/cover_cache_manager.dart b/lib/services/cover_cache_manager.dart index 288dad2f..007d8980 100644 --- a/lib/services/cover_cache_manager.dart +++ b/lib/services/cover_cache_manager.dart @@ -27,7 +27,6 @@ class CoverCacheManager { return _instance!; } - /// Check if cache manager is initialized static bool get isInitialized => _initialized && _instance != null; static Future initialize() async { @@ -62,8 +61,6 @@ class CoverCacheManager { } } - /// Clear all cached cover images. - /// Returns the number of files deleted. static Future clearCache() async { if (!_initialized || _instance == null) return; await _instance!.emptyCache(); @@ -98,7 +95,6 @@ class CoverCacheManager { } } -/// Statistics about the cover image cache class CacheStats { final int fileCount; final int totalSizeBytes; diff --git a/lib/utils/logger.dart b/lib/utils/logger.dart index b4afd4af..aa68806f 100644 --- a/lib/utils/logger.dart +++ b/lib/utils/logger.dart @@ -154,7 +154,6 @@ class LogBuffer extends ChangeNotifier { return buffer.toString(); } - /// Export logs with device information for debugging Future exportWithDeviceInfo() async { final buffer = StringBuffer();