diff --git a/go_backend/audio_metadata.go b/go_backend/audio_metadata.go index 74bc1762..33056507 100644 --- a/go_backend/audio_metadata.go +++ b/go_backend/audio_metadata.go @@ -44,7 +44,6 @@ type OggQuality struct { // ID3 Tag Reading (MP3) // ============================================================================= -// ReadID3Tags reads ID3v2 and ID3v1 tags from an MP3 file func ReadID3Tags(filePath string) (*AudioMetadata, error) { file, err := os.Open(filePath) if err != nil { @@ -87,7 +86,6 @@ func ReadID3Tags(filePath string) (*AudioMetadata, error) { return metadata, nil } -// readID3v2 reads ID3v2 tags from the beginning of file func readID3v2(file *os.File) (*AudioMetadata, error) { file.Seek(0, io.SeekStart) @@ -137,7 +135,6 @@ func readID3v2(file *os.File) (*AudioMetadata, error) { return metadata, nil } -// parseID3v22Frames parses ID3v2.2 frames (3-char frame IDs) func parseID3v22Frames(data []byte, metadata *AudioMetadata, tagUnsync bool) { pos := 0 for pos+6 < len(data) { @@ -286,7 +283,6 @@ func parseID3v23Frames(data []byte, metadata *AudioMetadata, version byte, tagUn } } -// readID3v1 reads ID3v1 tag from end of file func readID3v1(file *os.File) (*AudioMetadata, error) { if _, err := file.Seek(-128, io.SeekEnd); err != nil { return nil, err @@ -321,7 +317,6 @@ func readID3v1(file *os.File) (*AudioMetadata, error) { return metadata, nil } -// extractTextFrame extracts text from ID3 text frame func extractTextFrame(data []byte) string { if len(data) == 0 { return "" @@ -344,7 +339,6 @@ func extractTextFrame(data []byte) string { } } -// decodeUTF16 decodes UTF-16 with BOM func decodeUTF16(data []byte) string { if len(data) < 2 { return "" @@ -362,12 +356,10 @@ func decodeUTF16(data []byte) string { return decodeUTF16Data(data, littleEndian) } -// decodeUTF16BE decodes UTF-16 Big Endian func decodeUTF16BE(data []byte) string { return decodeUTF16Data(data, false) } -// decodeUTF16Data decodes UTF-16 data func decodeUTF16Data(data []byte, littleEndian bool) string { if len(data) < 2 { return "" @@ -389,13 +381,11 @@ func decodeUTF16Data(data []byte, littleEndian bool) string { return string(runes) } -// cleanGenre removes ID3 genre number format like "(17)" or "(17)Rock" func cleanGenre(genre string) string { if len(genre) == 0 { return "" } - // Handle "(17)" or "(17)Rock" format if genre[0] == '(' { end := strings.Index(genre, ")") if end > 0 { @@ -411,7 +401,6 @@ func cleanGenre(genre string) string { return genre } -// parseTrackNumber extracts track number from "1/10" or "1" format func parseTrackNumber(s string) int { s = strings.TrimSpace(s) if idx := strings.Index(s, "/"); idx > 0 { @@ -421,7 +410,6 @@ func parseTrackNumber(s string) int { return num } -// removeUnsync removes ID3 unsynchronization (0xFF 0x00 -> 0xFF) func removeUnsync(data []byte) []byte { if len(data) == 0 { return data @@ -437,7 +425,6 @@ func removeUnsync(data []byte) []byte { return out } -// extendedHeaderSize returns the total number of bytes to skip for the extended header func extendedHeaderSize(data []byte, version byte) int { if len(data) < 4 { return 0 @@ -463,7 +450,6 @@ func extendedHeaderSize(data []byte, version byte) int { return 0 } -// syncsafeToInt decodes a 4-byte syncsafe integer func syncsafeToInt(b []byte) int { if len(b) < 4 { return 0 @@ -471,7 +457,6 @@ func syncsafeToInt(b []byte) int { return int(b[0])<<21 | int(b[1])<<14 | int(b[2])<<7 | int(b[3]) } -// firstTextValue returns the first value in a null-separated text list func firstTextValue(s string) string { if idx := strings.IndexByte(s, 0); idx >= 0 { return s[:idx] @@ -479,7 +464,6 @@ func firstTextValue(s string) string { return s } -// GetMP3Quality reads MP3 audio quality info func GetMP3Quality(filePath string) (*MP3Quality, error) { file, err := os.Open(filePath) if err != nil { @@ -731,7 +715,6 @@ func detectOggStreamType(packets [][]byte) oggStreamType { return oggStreamUnknown } -// parseVorbisComments parses Vorbis comment block func parseVorbisComments(data []byte, metadata *AudioMetadata) { if len(data) < 4 { return @@ -807,7 +790,6 @@ func parseVorbisComments(data []byte, metadata *AudioMetadata) { } } -// GetOggQuality reads Ogg/Opus audio quality info func GetOggQuality(filePath string) (*OggQuality, error) { file, err := os.Open(filePath) if err != nil { @@ -906,7 +888,6 @@ var id3v1Genres = []string{ // Cover Art Extraction // ============================================================================= -// extractMP3CoverArt extracts cover art from MP3 file (APIC frame) func extractMP3CoverArt(filePath string) ([]byte, string, error) { file, err := os.Open(filePath) if err != nil { @@ -976,7 +957,6 @@ func extractMP3CoverArt(filePath string) ([]byte, string, error) { return nil, "", fmt.Errorf("no cover art found") } -// parseAPICFrame parses APIC frame data func parseAPICFrame(data []byte, version byte) ([]byte, string) { if len(data) < 4 { return nil, "" @@ -988,7 +968,6 @@ func parseAPICFrame(data []byte, version byte) ([]byte, string) { var mimeType string if version == 2 { - // ID3v2.2: 3-byte image format (JPG, PNG) if pos+3 > len(data) { return nil, "" } @@ -1003,7 +982,6 @@ func parseAPICFrame(data []byte, version byte) ([]byte, string) { mimeType = "image/jpeg" } } else { - // ID3v2.3/2.4: null-terminated MIME string end := pos for end < len(data) && data[end] != 0 { end++ @@ -1018,15 +996,12 @@ func parseAPICFrame(data []byte, version byte) ([]byte, string) { pos++ - // Skip description (null-terminated, may be UTF-16) if encoding == 0 || encoding == 3 { - // ISO-8859-1 or UTF-8 for pos < len(data) && data[pos] != 0 { pos++ } pos++ } else { - // UTF-16: look for double null for pos+1 < len(data) { if data[pos] == 0 && data[pos+1] == 0 { pos += 2 @@ -1043,7 +1018,6 @@ func parseAPICFrame(data []byte, version byte) ([]byte, string) { return data[pos:], mimeType } -// extractOggCoverArt extracts cover art from Ogg/Opus file (METADATA_BLOCK_PICTURE) func extractOggCoverArt(filePath string) ([]byte, string, error) { file, err := os.Open(filePath) if err != nil { @@ -1087,7 +1061,6 @@ func extractOggCoverArt(filePath string) ([]byte, string, error) { return nil, "", fmt.Errorf("no cover art found") } -// extractPictureFromVorbisComments looks for METADATA_BLOCK_PICTURE in Vorbis comments func extractPictureFromVorbisComments(data []byte) ([]byte, string) { if len(data) < 8 { return nil, "" @@ -1109,13 +1082,12 @@ func extractPictureFromVorbisComments(data []byte) ([]byte, string) { return nil, "" } - // Look for METADATA_BLOCK_PICTURE for i := uint32(0); i < commentCount && i < 100; i++ { var commentLen uint32 if err := binary.Read(reader, binary.LittleEndian, &commentLen); err != nil { break } - if commentLen > 10000000 { // 10MB sanity check + if commentLen > 10000000 { break } @@ -1126,7 +1098,6 @@ func extractPictureFromVorbisComments(data []byte) ([]byte, string) { key := "METADATA_BLOCK_PICTURE=" if len(comment) > len(key) && strings.ToUpper(string(comment[:len(key)])) == key { - // Base64-encoded FLAC picture block b64Data := comment[len(key):] decoded := make([]byte, base64StdDecodeLen(len(b64Data))) n, err := base64StdDecode(decoded, b64Data) @@ -1145,7 +1116,6 @@ func extractPictureFromVorbisComments(data []byte) ([]byte, string) { return nil, "" } -// parseFLACPictureBlock parses FLAC PICTURE metadata block format func parseFLACPictureBlock(data []byte) ([]byte, string) { if len(data) < 32 { return nil, "" diff --git a/go_backend/exports.go b/go_backend/exports.go index 79820dbe..5cc4b9f8 100644 --- a/go_backend/exports.go +++ b/go_backend/exports.go @@ -735,7 +735,6 @@ func SearchDeezerAll(query string, trackLimit, artistLimit int, filter string) ( return string(jsonBytes), nil } -// GetDeezerMetadata fetches metadata from Deezer URL or ID func GetDeezerMetadata(resourceType, resourceID string) (string, error) { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() @@ -834,8 +833,6 @@ func SearchDeezerByISRC(isrc string) (string, error) { return string(jsonBytes), nil } -// ConvertSpotifyToDeezer converts a Spotify track/album ID to Deezer and fetches metadata -// Useful when Spotify API is rate limited func ConvertSpotifyToDeezer(resourceType, spotifyID string) (string, error) { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() @@ -885,7 +882,6 @@ func ConvertSpotifyToDeezer(resourceType, spotifyID string) (string, error) { return "", fmt.Errorf("Spotify to Deezer conversion only supported for tracks and albums. Please search by name for %s", resourceType) } -// GetSpotifyMetadataWithDeezerFallback tries Spotify first, falls back to Deezer on rate limit func GetSpotifyMetadataWithDeezerFallback(spotifyURL string) (string, error) { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() @@ -942,7 +938,6 @@ func CheckAvailabilityFromDeezerID(deezerTrackID string) (string, error) { return string(jsonBytes), nil } -// CheckAvailabilityByPlatformID checks track availability using any platform as source func CheckAvailabilityByPlatformID(platform, entityType, entityID string) (string, error) { client := NewSongLinkClient() availability, err := client.CheckAvailabilityByPlatform(platform, entityType, entityID) diff --git a/go_backend/extension_manager.go b/go_backend/extension_manager.go index bbc5fac3..a2239fbf 100644 --- a/go_backend/extension_manager.go +++ b/go_backend/extension_manager.go @@ -47,7 +47,7 @@ type LoadedExtension struct { ID string `json:"id"` Manifest *ExtensionManifest `json:"manifest"` VM *goja.Runtime `json:"-"` - VMMu sync.Mutex `json:"-"` // Mutex to prevent concurrent VM access + VMMu sync.Mutex `json:"-"` Enabled bool `json:"enabled"` Error string `json:"error,omitempty"` DataDir string `json:"data_dir"` @@ -58,8 +58,8 @@ type LoadedExtension struct { type ExtensionManager struct { mu sync.RWMutex extensions map[string]*LoadedExtension - extensionsDir string // Base directory for extensions - dataDir string // Base directory for extension data + extensionsDir string + dataDir string } var ( @@ -98,7 +98,6 @@ func (m *ExtensionManager) LoadExtensionFromFile(filePath string) (*LoadedExtens return nil, fmt.Errorf("Invalid file format. Please select a .spotiflac-ext file") } - // Open the zip file zipReader, err := zip.OpenReader(filePath) if err != nil { return nil, fmt.Errorf("Cannot open extension file. The file may be corrupted or not a valid extension package") @@ -221,7 +220,6 @@ func (m *ExtensionManager) LoadExtensionFromFile(filePath string) (*LoadedExtens SourceDir: extDir, } - // Initialize Goja VM if err := m.initializeVM(ext); err != nil { ext.Error = err.Error() ext.Enabled = false @@ -268,13 +266,11 @@ func (m *ExtensionManager) initializeVM(ext *LoadedExtension) error { return goja.Undefined() }) - // Run the extension code _, err = vm.RunString(string(jsCode)) if err != nil { return fmt.Errorf("failed to execute extension code: %w", err) } - // Verify extension was registered if registeredExtension == nil || goja.IsUndefined(registeredExtension) { return fmt.Errorf("extension did not call registerExtension()") } @@ -291,9 +287,7 @@ func (m *ExtensionManager) UnloadExtension(extensionID string) error { return fmt.Errorf("Extension not found") } - // Call cleanup if VM is initialized if ext.VM != nil { - // Try to call cleanup function cleanup, err := ext.VM.RunString("typeof extension !== 'undefined' && typeof extension.cleanup === 'function' ? extension.cleanup() : null") if err != nil { GoLog("[Extension] Error calling cleanup for %s: %v\n", extensionID, err) @@ -302,7 +296,6 @@ func (m *ExtensionManager) UnloadExtension(extensionID string) error { } } - // Remove from registry delete(m.extensions, extensionID) GoLog("[Extension] Unloaded extension: %s\n", extensionID) @@ -343,7 +336,6 @@ func (m *ExtensionManager) SetExtensionEnabled(extensionID string, enabled bool) ext.Enabled = enabled GoLog("[Extension] %s %s\n", extensionID, map[bool]string{true: "enabled", false: "disabled"}[enabled]) - // Persist enabled state to settings store store := GetExtensionSettingsStore() if err := store.Set(extensionID, "_enabled", enabled); err != nil { GoLog("[Extension] Failed to persist enabled state for %s: %v\n", extensionID, err) @@ -438,7 +430,6 @@ func (m *ExtensionManager) loadExtensionFromDirectory(dirPath string) (*LoadedEx } } - // Initialize Goja VM if err := m.initializeVM(ext); err != nil { ext.Error = err.Error() ext.Enabled = false @@ -457,12 +448,10 @@ func (m *ExtensionManager) RemoveExtension(extensionID string) error { return err } - // Unload first if err := m.UnloadExtension(extensionID); err != nil { return err } - // Remove source directory if ext.SourceDir != "" { if err := os.RemoveAll(ext.SourceDir); err != nil { GoLog("[Extension] Warning: failed to remove source dir: %v\n", err) @@ -484,7 +473,6 @@ func (m *ExtensionManager) UpgradeExtension(filePath string) (*LoadedExtension, return nil, fmt.Errorf("Invalid file format. Please select a .spotiflac-ext file") } - // Open the zip file zipReader, err := zip.OpenReader(filePath) if err != nil { return nil, fmt.Errorf("Cannot open extension file. The file may be corrupted or not a valid extension package") @@ -548,11 +536,9 @@ func (m *ExtensionManager) UpgradeExtension(filePath string) (*LoadedExtension, extDir := existing.SourceDir wasEnabled := existing.Enabled - // Cleanup and unload existing extension m.CleanupExtension(existing.ID) m.UnloadExtension(existing.ID) - // Remove old source files but keep data directory if extDir != "" { if err := os.RemoveAll(extDir); err != nil { GoLog("[Extension] Warning: failed to remove old source dir: %v\n", err) @@ -634,11 +620,11 @@ type ExtensionUpgradeInfo struct { func (m *ExtensionManager) checkExtensionUpgradeInternal(filePath string) (*ExtensionUpgradeInfo, error) { // Validate file extension if !strings.HasSuffix(strings.ToLower(filePath), ".spotiflac-ext") { - return nil, fmt.Errorf("Invalid file format") + return nil, fmt.Errorf("Invalid file format. Please select a .spotiflac-ext file") } - // Open the zip file zipReader, err := zip.OpenReader(filePath) + if err != nil { return nil, fmt.Errorf("Cannot open extension file") } diff --git a/go_backend/extension_manifest.go b/go_backend/extension_manifest.go index 37af1b83..9fc8f3da 100644 --- a/go_backend/extension_manifest.go +++ b/go_backend/extension_manifest.go @@ -175,7 +175,6 @@ func (m *ExtensionManifest) Validate() error { } } - // Validate settings if present for i, setting := range m.Settings { if strings.TrimSpace(setting.Key) == "" { return &ManifestValidationError{ @@ -236,7 +235,7 @@ func (m *ExtensionManifest) IsDomainAllowed(domain string) bool { } // Support wildcard subdomains (e.g., *.example.com) if strings.HasPrefix(allowed, "*.") { - suffix := allowed[1:] // Remove the * + suffix := allowed[1:] if strings.HasSuffix(domain, suffix) { return true } @@ -269,7 +268,6 @@ func (m *ExtensionManifest) MatchesURL(urlStr string) bool { urlStr = strings.ToLower(strings.TrimSpace(urlStr)) for _, pattern := range m.URLHandler.Patterns { pattern = strings.ToLower(strings.TrimSpace(pattern)) - // Check if URL contains the pattern (host match) if strings.Contains(urlStr, pattern) { return true } diff --git a/go_backend/extension_runtime.go b/go_backend/extension_runtime.go index a6b8e943..5ee1455e 100644 --- a/go_backend/extension_runtime.go +++ b/go_backend/extension_runtime.go @@ -106,7 +106,6 @@ func NewExtensionRuntime(ext *LoadedExtension) *ExtensionRuntime { Timeout: 30 * time.Second, Jar: jar, CheckRedirect: func(req *http.Request, via []*http.Request) error { - // Validate redirect target domain against allowed domains if req.URL.Scheme != "https" { GoLog("[Extension:%s] Redirect blocked: non-https scheme '%s'\n", ext.ID, req.URL.Scheme) return fmt.Errorf("redirect blocked: only https is allowed") @@ -125,7 +124,6 @@ func NewExtensionRuntime(ext *LoadedExtension) *ExtensionRuntime { GoLog("[Extension:%s] Redirect blocked: private IP '%s'\n", ext.ID, domain) return &RedirectBlockedError{Domain: domain, IsPrivate: true} } - // Default redirect limit (10) if len(via) >= 10 { return http.ErrUseLastResponse } @@ -227,7 +225,6 @@ func (r *ExtensionRuntime) SetSettings(settings map[string]interface{}) { func (r *ExtensionRuntime) RegisterAPIs(vm *goja.Runtime) { r.vm = vm - // HTTP client (sandboxed to allowed domains) httpObj := vm.NewObject() httpObj.Set("get", r.httpGet) httpObj.Set("post", r.httpPost) @@ -264,7 +261,6 @@ func (r *ExtensionRuntime) RegisterAPIs(vm *goja.Runtime) { authObj.Set("exchangeCodeWithPKCE", r.authExchangeCodeWithPKCE) vm.Set("auth", authObj) - // File operations (sandboxed) fileObj := vm.NewObject() fileObj.Set("download", r.fileDownload) fileObj.Set("exists", r.fileExists) @@ -282,7 +278,6 @@ func (r *ExtensionRuntime) RegisterAPIs(vm *goja.Runtime) { ffmpegObj.Set("convert", r.ffmpegConvert) vm.Set("ffmpeg", ffmpegObj) - // Track matching API matchingObj := vm.NewObject() matchingObj.Set("compareStrings", r.matchingCompareStrings) matchingObj.Set("compareDuration", r.matchingCompareDuration) diff --git a/go_backend/extension_runtime_auth.go b/go_backend/extension_runtime_auth.go index 37178339..eb17eb37 100644 --- a/go_backend/extension_runtime_auth.go +++ b/go_backend/extension_runtime_auth.go @@ -121,7 +121,6 @@ func (r *ExtensionRuntime) authClear(call goja.FunctionCall) goja.Value { return r.vm.ToValue(true) } -// authIsAuthenticated checks if extension has valid auth func (r *ExtensionRuntime) authIsAuthenticated(call goja.FunctionCall) goja.Value { extensionAuthStateMu.RLock() defer extensionAuthStateMu.RUnlock() @@ -194,7 +193,6 @@ func generatePKCEChallenge(verifier string) string { } func (r *ExtensionRuntime) authGeneratePKCE(call goja.FunctionCall) goja.Value { - // Default length is 64 characters length := 64 if len(call.Arguments) > 0 && !goja.IsUndefined(call.Arguments[0]) { if l, ok := call.Arguments[0].Export().(float64); ok && l >= 43 && l <= 128 { @@ -247,9 +245,7 @@ func (r *ExtensionRuntime) authGetPKCE(call goja.FunctionCall) goja.Value { }) } -// authStartOAuthWithPKCE is a high-level helper that generates PKCE and opens OAuth URL // config: { authUrl, clientId, redirectUri, scope, extraParams } -// Returns: { success, authUrl, pkce: { verifier, challenge } } func (r *ExtensionRuntime) authStartOAuthWithPKCE(call goja.FunctionCall) goja.Value { if len(call.Arguments) < 1 { return r.vm.ToValue(map[string]interface{}{ @@ -267,7 +263,6 @@ func (r *ExtensionRuntime) authStartOAuthWithPKCE(call goja.FunctionCall) goja.V }) } - // Required fields authURL, _ := config["authUrl"].(string) clientID, _ := config["clientId"].(string) redirectURI, _ := config["redirectUri"].(string) @@ -279,11 +274,9 @@ func (r *ExtensionRuntime) authStartOAuthWithPKCE(call goja.FunctionCall) goja.V }) } - // Optional fields scope, _ := config["scope"].(string) extraParams, _ := config["extraParams"].(map[string]interface{}) - // Generate PKCE verifier, err := generatePKCEVerifier(64) if err != nil { return r.vm.ToValue(map[string]interface{}{ @@ -293,7 +286,6 @@ func (r *ExtensionRuntime) authStartOAuthWithPKCE(call goja.FunctionCall) goja.V } challenge := generatePKCEChallenge(verifier) - // Store PKCE in auth state extensionAuthStateMu.Lock() state, exists := extensionAuthState[r.extensionID] if !exists { @@ -302,10 +294,9 @@ func (r *ExtensionRuntime) authStartOAuthWithPKCE(call goja.FunctionCall) goja.V } state.PKCEVerifier = verifier state.PKCEChallenge = challenge - state.AuthCode = "" // Clear any previous auth code + state.AuthCode = "" extensionAuthStateMu.Unlock() - // Build OAuth URL with PKCE parameters parsedURL, err := url.Parse(authURL) if err != nil { return r.vm.ToValue(map[string]interface{}{ @@ -325,7 +316,6 @@ func (r *ExtensionRuntime) authStartOAuthWithPKCE(call goja.FunctionCall) goja.V query.Set("scope", scope) } - // Add extra params for k, v := range extraParams { query.Set(k, fmt.Sprintf("%v", v)) } @@ -333,7 +323,6 @@ func (r *ExtensionRuntime) authStartOAuthWithPKCE(call goja.FunctionCall) goja.V parsedURL.RawQuery = query.Encode() fullAuthURL := parsedURL.String() - // Store pending auth request for Flutter pendingAuthRequestsMu.Lock() pendingAuthRequests[r.extensionID] = &PendingAuthRequest{ ExtensionID: r.extensionID, diff --git a/go_backend/extension_runtime_file.go b/go_backend/extension_runtime_file.go index 46b4f644..ccd51308 100644 --- a/go_backend/extension_runtime_file.go +++ b/go_backend/extension_runtime_file.go @@ -75,7 +75,6 @@ func isPathWithinBase(baseDir, targetPath string) bool { } func (r *ExtensionRuntime) validatePath(path string) (string, error) { - // Check if extension has file permission if !r.manifest.Permissions.File { return "", fmt.Errorf("file access denied: extension does not have 'file' permission") } diff --git a/go_backend/extension_runtime_http.go b/go_backend/extension_runtime_http.go index faa5c675..a5f44617 100644 --- a/go_backend/extension_runtime_http.go +++ b/go_backend/extension_runtime_http.go @@ -38,7 +38,6 @@ func (r *ExtensionRuntime) validateDomain(urlStr string) error { return fmt.Errorf("invalid URL: hostname is required") } - // Block private/local network access (SSRF protection) if isPrivateIP(domain) { return fmt.Errorf("network access denied: private/local network '%s' not allowed", domain) } diff --git a/go_backend/extension_runtime_matching.go b/go_backend/extension_runtime_matching.go index 9e56fa80..4ad4b0e3 100644 --- a/go_backend/extension_runtime_matching.go +++ b/go_backend/extension_runtime_matching.go @@ -9,7 +9,6 @@ import ( // ==================== Track Matching API ==================== -// matchingCompareStrings compares two strings with fuzzy matching func (r *ExtensionRuntime) matchingCompareStrings(call goja.FunctionCall) goja.Value { if len(call.Arguments) < 2 { return r.vm.ToValue(0.0) @@ -22,12 +21,10 @@ func (r *ExtensionRuntime) matchingCompareStrings(call goja.FunctionCall) goja.V return r.vm.ToValue(1.0) } - // Calculate Levenshtein distance-based similarity similarity := calculateStringSimilarity(str1, str2) return r.vm.ToValue(similarity) } -// matchingCompareDuration compares two durations with tolerance func (r *ExtensionRuntime) matchingCompareDuration(call goja.FunctionCall) goja.Value { if len(call.Arguments) < 2 { return r.vm.ToValue(false) @@ -36,8 +33,7 @@ func (r *ExtensionRuntime) matchingCompareDuration(call goja.FunctionCall) goja. dur1 := int(call.Arguments[0].ToInteger()) dur2 := int(call.Arguments[1].ToInteger()) - // Default tolerance: 3 seconds - tolerance := 3000 // milliseconds + tolerance := 3000 if len(call.Arguments) > 2 && !goja.IsUndefined(call.Arguments[2]) { tolerance = int(call.Arguments[2].ToInteger()) } @@ -50,7 +46,6 @@ func (r *ExtensionRuntime) matchingCompareDuration(call goja.FunctionCall) goja. return r.vm.ToValue(diff <= tolerance) } -// matchingNormalizeString normalizes a string for comparison func (r *ExtensionRuntime) matchingNormalizeString(call goja.FunctionCall) goja.Value { if len(call.Arguments) < 1 { return r.vm.ToValue("") @@ -61,7 +56,6 @@ func (r *ExtensionRuntime) matchingNormalizeString(call goja.FunctionCall) goja. return r.vm.ToValue(normalized) } -// calculateStringSimilarity calculates similarity between two strings (0-1) func calculateStringSimilarity(s1, s2 string) float64 { if len(s1) == 0 && len(s2) == 0 { return 1.0 @@ -70,7 +64,6 @@ func calculateStringSimilarity(s1, s2 string) float64 { return 0.0 } - // Use Levenshtein distance distance := levenshteinDistance(s1, s2) maxLen := len(s1) if len(s2) > maxLen { @@ -80,7 +73,6 @@ func calculateStringSimilarity(s1, s2 string) float64 { return 1.0 - float64(distance)/float64(maxLen) } -// levenshteinDistance calculates the Levenshtein distance between two strings func levenshteinDistance(s1, s2 string) int { if len(s1) == 0 { return len(s2) @@ -89,7 +81,6 @@ func levenshteinDistance(s1, s2 string) int { return len(s1) } - // Create matrix matrix := make([][]int, len(s1)+1) for i := range matrix { matrix[i] = make([]int, len(s2)+1) @@ -99,7 +90,6 @@ func levenshteinDistance(s1, s2 string) int { matrix[0][j] = j } - // Fill matrix for i := 1; i <= len(s1); i++ { for j := 1; j <= len(s2); j++ { cost := 1 @@ -107,9 +97,9 @@ func levenshteinDistance(s1, s2 string) int { cost = 0 } matrix[i][j] = min( - matrix[i-1][j]+1, // deletion - matrix[i][j-1]+1, // insertion - matrix[i-1][j-1]+cost, // substitution + matrix[i-1][j]+1, + matrix[i][j-1]+1, + matrix[i-1][j-1]+cost, ) } } @@ -117,12 +107,9 @@ func levenshteinDistance(s1, s2 string) int { return matrix[len(s1)][len(s2)] } -// normalizeStringForMatching normalizes a string for comparison func normalizeStringForMatching(s string) string { - // Convert to lowercase s = strings.ToLower(s) - // Remove common suffixes/prefixes suffixes := []string{ " (remastered)", " (remaster)", " - remastered", " - remaster", " (deluxe)", " (deluxe edition)", " - deluxe", " - deluxe edition", @@ -136,7 +123,6 @@ func normalizeStringForMatching(s string) string { } } - // Remove special characters var result strings.Builder for _, r := range s { if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == ' ' { @@ -144,7 +130,6 @@ func normalizeStringForMatching(s string) string { } } - // Collapse multiple spaces s = strings.Join(strings.Fields(result.String()), " ") return strings.TrimSpace(s) diff --git a/go_backend/extension_runtime_polyfills.go b/go_backend/extension_runtime_polyfills.go index e75d679a..62892c70 100644 --- a/go_backend/extension_runtime_polyfills.go +++ b/go_backend/extension_runtime_polyfills.go @@ -25,14 +25,11 @@ func (r *ExtensionRuntime) fetchPolyfill(call goja.FunctionCall) goja.Value { } urlStr := call.Arguments[0].String() - - // Validate domain if err := r.validateDomain(urlStr); err != nil { GoLog("[Extension:%s] fetch blocked: %v\n", r.extensionID, err) return r.createFetchError(err.Error()) } - // Parse options method := "GET" var bodyStr string headers := make(map[string]string) @@ -84,7 +81,6 @@ func (r *ExtensionRuntime) fetchPolyfill(call goja.FunctionCall) goja.Value { for k, v := range headers { req.Header.Set(k, v) } - // Set defaults if not provided if req.Header.Get("User-Agent") == "" { req.Header.Set("User-Agent", "SpotiFLAC-Extension/1.0") } @@ -112,7 +108,6 @@ func (r *ExtensionRuntime) fetchPolyfill(call goja.FunctionCall) goja.Value { } } - // Create Response object (browser-compatible) responseObj := r.vm.NewObject() responseObj.Set("ok", resp.StatusCode >= 200 && resp.StatusCode < 300) responseObj.Set("status", resp.StatusCode) @@ -136,7 +131,6 @@ func (r *ExtensionRuntime) fetchPolyfill(call goja.FunctionCall) goja.Value { }) responseObj.Set("arrayBuffer", func(call goja.FunctionCall) goja.Value { - // Return as array of bytes byteArray := make([]interface{}, len(body)) for i, b := range body { byteArray[i] = int(b) @@ -171,7 +165,6 @@ func (r *ExtensionRuntime) atobPolyfill(call goja.FunctionCall) goja.Value { input := call.Arguments[0].String() decoded, err := base64.StdEncoding.DecodeString(input) if err != nil { - // Try URL-safe base64 decoded, err = base64.URLEncoding.DecodeString(input) if err != nil { GoLog("[Extension:%s] atob decode error: %v\n", r.extensionID, err) @@ -192,7 +185,6 @@ func (r *ExtensionRuntime) btoaPolyfill(call goja.FunctionCall) goja.Value { // registerTextEncoderDecoder registers TextEncoder and TextDecoder classes func (r *ExtensionRuntime) registerTextEncoderDecoder(vm *goja.Runtime) { - // TextEncoder constructor vm.Set("TextEncoder", func(call goja.ConstructorCall) *goja.Object { encoder := call.This encoder.Set("encoding", "utf-8") @@ -204,7 +196,6 @@ func (r *ExtensionRuntime) registerTextEncoderDecoder(vm *goja.Runtime) { input := call.Arguments[0].String() bytes := []byte(input) - // Return as array (Uint8Array-like) result := make([]interface{}, len(bytes)) for i, b := range bytes { result[i] = int(b) @@ -227,11 +218,9 @@ func (r *ExtensionRuntime) registerTextEncoderDecoder(vm *goja.Runtime) { return nil }) - // TextDecoder constructor vm.Set("TextDecoder", func(call goja.ConstructorCall) *goja.Object { decoder := call.This - // Get encoding from arguments (default: utf-8) encoding := "utf-8" if len(call.Arguments) > 0 && !goja.IsUndefined(call.Arguments[0]) { encoding = call.Arguments[0].String() @@ -245,7 +234,6 @@ func (r *ExtensionRuntime) registerTextEncoderDecoder(vm *goja.Runtime) { return vm.ToValue("") } - // Handle different input types input := call.Arguments[0].Export() var bytes []byte @@ -265,7 +253,6 @@ func (r *ExtensionRuntime) registerTextEncoderDecoder(vm *goja.Runtime) { } } case string: - // Already a string, just return it return vm.ToValue(v) default: return vm.ToValue("") @@ -289,7 +276,6 @@ func (r *ExtensionRuntime) registerURLClass(vm *goja.Runtime) { urlStr := call.Arguments[0].String() - // Handle relative URLs with base if len(call.Arguments) > 1 && !goja.IsUndefined(call.Arguments[1]) { baseStr := call.Arguments[1].String() baseURL, err := url.Parse(baseStr) @@ -446,10 +432,6 @@ func (r *ExtensionRuntime) registerURLClass(vm *goja.Runtime) { // registerJSONGlobal ensures JSON global is properly set up func (r *ExtensionRuntime) registerJSONGlobal(vm *goja.Runtime) { // JSON is already built-in to Goja, but we can enhance it - // This ensures JSON.parse and JSON.stringify work as expected - - // The built-in JSON object should already work, but let's verify - // and add any missing functionality if needed jsonScript := ` if (typeof JSON === 'undefined') { var JSON = { diff --git a/go_backend/extension_settings.go b/go_backend/extension_settings.go index aec2cd43..9ad0c0c1 100644 --- a/go_backend/extension_settings.go +++ b/go_backend/extension_settings.go @@ -15,7 +15,6 @@ type ExtensionSettingsStore struct { settings map[string]map[string]interface{} // extensionID -> settings } -// Global settings store var ( globalSettingsStore *ExtensionSettingsStore globalSettingsStoreOnce sync.Once @@ -129,7 +128,6 @@ func (s *ExtensionSettingsStore) GetAll(extensionID string) map[string]interface return make(map[string]interface{}) } - // Return a copy result := make(map[string]interface{}) for k, v := range extSettings { result[k] = v @@ -156,7 +154,6 @@ func (s *ExtensionSettingsStore) SetAll(extensionID string, settings map[string] s.settings[extensionID] = settings - // Persist to disk return s.saveSettings(extensionID, settings) } @@ -171,7 +168,6 @@ func (s *ExtensionSettingsStore) Remove(extensionID, key string) error { delete(extSettings, key) - // Persist to disk return s.saveSettings(extensionID, extSettings) } diff --git a/go_backend/extension_store.go b/go_backend/extension_store.go index bcd5eff8..27c14dec 100644 --- a/go_backend/extension_store.go +++ b/go_backend/extension_store.go @@ -12,7 +12,6 @@ import ( "time" ) -// Extension categories const ( CategoryMetadata = "metadata" CategoryDownload = "download" @@ -146,7 +145,6 @@ func InitExtensionStore(cacheDir string) *ExtensionStore { cacheDir: cacheDir, cacheTTL: cacheTTL, } - // Try to load from disk cache extensionStore.loadDiskCache() } return extensionStore @@ -209,7 +207,6 @@ func (s *ExtensionStore) FetchRegistry(forceRefresh bool) (*StoreRegistry, error s.cacheMu.Lock() defer s.cacheMu.Unlock() - // Return cached if valid and not forcing refresh if !forceRefresh && s.cache != nil && time.Since(s.cacheTime) < s.cacheTTL { LogDebug("ExtensionStore", "Using cached registry (%d extensions)", len(s.cache.Extensions)) return s.cache, nil @@ -224,7 +221,6 @@ func (s *ExtensionStore) FetchRegistry(forceRefresh bool) (*StoreRegistry, error client := &http.Client{Timeout: 30 * time.Second} resp, err := client.Get(s.registryURL) if err != nil { - // Return cached data if available on network error if s.cache != nil { LogWarn("ExtensionStore", "Network error, using cached registry: %v", err) return s.cache, nil diff --git a/go_backend/extension_test.go b/go_backend/extension_test.go index 4045279c..1b9df2c8 100644 --- a/go_backend/extension_test.go +++ b/go_backend/extension_test.go @@ -112,7 +112,6 @@ func TestExtensionRuntime_NetworkSandbox(t *testing.T) { runtime := NewExtensionRuntime(ext) - // Test allowed domains if err := runtime.validateDomain("https://api.allowed.com/path"); err != nil { t.Errorf("Expected api.allowed.com to be allowed, got error: %v", err) } @@ -121,7 +120,6 @@ func TestExtensionRuntime_NetworkSandbox(t *testing.T) { t.Errorf("Expected sub.wildcard.com to be allowed (wildcard), got error: %v", err) } - // Test blocked domains if err := runtime.validateDomain("https://blocked.com/path"); err == nil { t.Error("Expected blocked.com to be denied") } @@ -139,7 +137,7 @@ func TestExtensionRuntime_FileSandbox(t *testing.T) { Manifest: &ExtensionManifest{ Name: "test-ext", Permissions: ExtensionPermissions{ - File: true, // Enable file permission for test + File: true, }, }, DataDir: tempDir, @@ -147,7 +145,6 @@ func TestExtensionRuntime_FileSandbox(t *testing.T) { runtime := NewExtensionRuntime(ext) - // Test valid path within sandbox validPath, err := runtime.validatePath("test.txt") if err != nil { t.Errorf("Expected relative path to be valid, got error: %v", err) @@ -156,13 +153,11 @@ func TestExtensionRuntime_FileSandbox(t *testing.T) { t.Error("Expected non-empty path") } - // Test path traversal attack _, err = runtime.validatePath("../../../etc/passwd") if err == nil { t.Error("Expected path traversal to be blocked") } - // Test nested path within sandbox (should be allowed) nestedPath, err := runtime.validatePath("subdir/file.txt") if err != nil { t.Errorf("Expected nested path to be valid, got error: %v", err) @@ -171,26 +166,23 @@ func TestExtensionRuntime_FileSandbox(t *testing.T) { t.Error("Expected non-empty nested path") } - // Test absolute path should be blocked (security fix) - // Use platform-appropriate absolute path var absPath string if filepath.IsAbs("C:\\Windows\\System32") { - absPath = "C:\\Windows\\System32\\test.txt" // Windows + absPath = "C:\\Windows\\System32\\test.txt" } else { - absPath = "/etc/passwd" // Unix + absPath = "/etc/passwd" } _, err = runtime.validatePath(absPath) if err == nil { t.Error("Expected absolute path to be blocked") } - // Test that extension without file permission is blocked extNoFile := &LoadedExtension{ ID: "test-ext-no-file", Manifest: &ExtensionManifest{ Name: "test-ext-no-file", Permissions: ExtensionPermissions{ - File: false, // No file permission + File: false, }, }, DataDir: tempDir, @@ -215,7 +207,6 @@ func TestExtensionRuntime_UtilityFunctions(t *testing.T) { vm := goja.New() runtime.RegisterAPIs(vm) - // Test base64 encode/decode result, err := vm.RunString(`utils.base64Encode("hello")`) if err != nil { t.Fatalf("base64Encode failed: %v", err) @@ -232,7 +223,6 @@ func TestExtensionRuntime_UtilityFunctions(t *testing.T) { t.Errorf("Expected 'hello', got '%s'", result.String()) } - // Test MD5 result, err = vm.RunString(`utils.md5("hello")`) if err != nil { t.Fatalf("md5 failed: %v", err) @@ -241,7 +231,6 @@ func TestExtensionRuntime_UtilityFunctions(t *testing.T) { t.Errorf("Expected '5d41402abc4b2a76b9719d911017c592', got '%s'", result.String()) } - // Test JSON parse/stringify result, err = vm.RunString(`utils.stringifyJSON({name: "test", value: 123})`) if err != nil { t.Fatalf("stringifyJSON failed: %v", err) @@ -267,7 +256,6 @@ func TestExtensionRuntime_SSRFProtection(t *testing.T) { runtime := NewExtensionRuntime(ext) - // Test that private IPs are blocked (SSRF protection) privateIPs := []string{ "http://localhost/admin", "http://127.0.0.1/admin", @@ -285,7 +273,6 @@ func TestExtensionRuntime_SSRFProtection(t *testing.T) { } } - // Test that allowed public domain still works if err := runtime.validateDomain("https://api.example.com/path"); err != nil { t.Errorf("Expected api.example.com to be allowed, got error: %v", err) } @@ -296,7 +283,6 @@ func TestIsPrivateIP(t *testing.T) { host string expected bool }{ - // Private IPs should be blocked {"localhost", true}, {"127.0.0.1", true}, {"127.0.0.2", true}, @@ -306,18 +292,17 @@ func TestIsPrivateIP(t *testing.T) { {"172.31.255.255", true}, {"192.168.0.1", true}, {"192.168.255.255", true}, - {"169.254.169.254", true}, // AWS metadata + {"169.254.169.254", true}, {"router.local", true}, {"mydevice.local", true}, - // Public IPs should be allowed {"8.8.8.8", false}, {"1.1.1.1", false}, {"api.example.com", false}, {"google.com", false}, - {"172.15.0.1", false}, // Just outside 172.16-31 range - {"172.32.0.1", false}, // Just outside 172.16-31 range - {"192.167.0.1", false}, // Not 192.168.x.x + {"172.15.0.1", false}, + {"172.32.0.1", false}, + {"192.167.0.1", false}, } for _, tt := range tests { diff --git a/go_backend/extension_timeout.go b/go_backend/extension_timeout.go index a55f0464..79fbdaa6 100644 --- a/go_backend/extension_timeout.go +++ b/go_backend/extension_timeout.go @@ -10,7 +10,6 @@ import ( "github.com/dop251/goja" ) -// JSExecutionError represents an error during JS execution type JSExecutionError struct { Message string IsTimeout bool @@ -20,8 +19,6 @@ func (e *JSExecutionError) Error() string { return e.Message } -// RunWithTimeout executes JavaScript code with a timeout -// Returns the result value and any error (including timeout) func RunWithTimeout(vm *goja.Runtime, script string, timeout time.Duration) (goja.Value, error) { if timeout <= 0 { timeout = DefaultJSTimeout @@ -30,22 +27,18 @@ func RunWithTimeout(vm *goja.Runtime, script string, timeout time.Duration) (goj ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() - // Channel to receive result type result struct { value goja.Value err error } resultCh := make(chan result, 1) - // Track if we've interrupted var interrupted bool var interruptMu sync.Mutex - // Run script in goroutine go func() { defer func() { if r := recover(); r != nil { - // Check if this was our interrupt interruptMu.Lock() wasInterrupted := interrupted interruptMu.Unlock() @@ -65,22 +58,18 @@ func RunWithTimeout(vm *goja.Runtime, script string, timeout time.Duration) (goj resultCh <- result{val, err} }() - // Wait for result or timeout select { case res := <-resultCh: return res.value, res.err case <-ctx.Done(): - // Timeout - interrupt the VM interruptMu.Lock() interrupted = true interruptMu.Unlock() vm.Interrupt("execution timeout") - // Wait a bit for the goroutine to finish select { case res := <-resultCh: - // If we got a result after interrupt, it might be the timeout error if res.err != nil { return nil, res.err } @@ -89,7 +78,6 @@ func RunWithTimeout(vm *goja.Runtime, script string, timeout time.Duration) (goj IsTimeout: true, } case <-time.After(1 * time.Second): - // Force return timeout error return nil, &JSExecutionError{ Message: "execution timeout exceeded (force)", IsTimeout: true, @@ -109,7 +97,6 @@ func RunWithTimeoutAndRecover(vm *goja.Runtime, script string, timeout time.Dura return result, err } -// IsTimeoutError checks if an error is a timeout error func IsTimeoutError(err error) bool { if jsErr, ok := err.(*JSExecutionError); ok { return jsErr.IsTimeout diff --git a/go_backend/httputil.go b/go_backend/httputil.go index 824e0685..4ac2d30c 100644 --- a/go_backend/httputil.go +++ b/go_backend/httputil.go @@ -16,7 +16,6 @@ import ( ) func getRandomUserAgent() string { - // Chrome version 120-145 (modern range) chromeVersion := rand.Intn(26) + 120 chromeBuild := rand.Intn(1500) + 6000 chromePatch := rand.Intn(200) + 100 @@ -118,7 +117,6 @@ func DoRequestWithRetry(client *http.Client, req *http.Request, config RetryConf requestURL := req.URL.String() for attempt := 0; attempt <= config.MaxRetries; attempt++ { - // Clone request for retry (body needs to be re-readable) reqCopy := req.Clone(req.Context()) reqCopy.Header.Set("User-Agent", getRandomUserAgent()) @@ -126,9 +124,7 @@ func DoRequestWithRetry(client *http.Client, req *http.Request, config RetryConf if err != nil { lastErr = err - // Check for ISP blocking on network errors if CheckAndLogISPBlocking(err, requestURL, "HTTP") { - // Don't retry if ISP blocking is detected - it won't help return nil, WrapErrorWithISPCheck(err, requestURL, "HTTP") } diff --git a/go_backend/httputil_utls.go b/go_backend/httputil_utls.go index ddf7185f..bf5ec9ed 100644 --- a/go_backend/httputil_utls.go +++ b/go_backend/httputil_utls.go @@ -35,7 +35,6 @@ func newUTLSTransport() *utlsTransport { } func (t *utlsTransport) RoundTrip(req *http.Request) (*http.Response, error) { - // For non-HTTPS, use standard transport if req.URL.Scheme != "https" { return sharedTransport.RoundTrip(req) } @@ -44,29 +43,24 @@ func (t *utlsTransport) RoundTrip(req *http.Request) (*http.Response, error) { port := t.getPort(req.URL) addr := net.JoinHostPort(host, port) - // Dial TCP connection conn, err := t.dialer.DialContext(req.Context(), "tcp", addr) if err != nil { return nil, err } - // Create uTLS connection with Chrome fingerprint (supports HTTP/2 ALPN) tlsConn := utls.UClient(conn, &utls.Config{ ServerName: host, - NextProtos: []string{"h2", "http/1.1"}, // Prefer HTTP/2 + NextProtos: []string{"h2", "http/1.1"}, }, utls.HelloChrome_Auto) - // Perform TLS handshake if err := tlsConn.Handshake(); err != nil { conn.Close() return nil, err } - // Check if server supports HTTP/2 negotiatedProto := tlsConn.ConnectionState().NegotiatedProtocol if negotiatedProto == "h2" { - // Use HTTP/2 transport h2Transport := &http2.Transport{ DialTLSContext: func(ctx context.Context, network, addr string, cfg *tls.Config) (net.Conn, error) { return tlsConn, nil @@ -77,7 +71,6 @@ func (t *utlsTransport) RoundTrip(req *http.Request) (*http.Response, error) { return h2Transport.RoundTrip(req) } - // Fallback to HTTP/1.1 transport := &http.Transport{ DialTLSContext: func(ctx context.Context, network, addr string) (net.Conn, error) { return tlsConn, nil diff --git a/go_backend/library_scan.go b/go_backend/library_scan.go index 27828d5e..a0ceecfb 100644 --- a/go_backend/library_scan.go +++ b/go_backend/library_scan.go @@ -31,7 +31,6 @@ type LibraryScanResult struct { Format string `json:"format,omitempty"` } -// LibraryScanProgress reports progress during scan type LibraryScanProgress struct { TotalFiles int `json:"total_files"` ScannedFiles int `json:"scanned_files"` @@ -50,7 +49,6 @@ var ( libraryCoverCacheMu sync.RWMutex ) -// supportedAudioFormats lists file extensions we can read metadata from var supportedAudioFormats = map[string]bool{ ".flac": true, ".m4a": true, @@ -59,15 +57,12 @@ var supportedAudioFormats = map[string]bool{ ".ogg": true, } -// SetLibraryCoverCacheDir sets the directory to cache extracted cover art func SetLibraryCoverCacheDir(cacheDir string) { libraryCoverCacheMu.Lock() libraryCoverCacheDir = cacheDir libraryCoverCacheMu.Unlock() } -// ScanLibraryFolder scans a folder recursively for audio files and reads their metadata -// Returns JSON array of LibraryScanResult func ScanLibraryFolder(folderPath string) (string, error) { if folderPath == "" { return "[]", fmt.Errorf("folder path is empty") diff --git a/go_backend/logbuffer.go b/go_backend/logbuffer.go index 382f18dd..5d7438d7 100644 --- a/go_backend/logbuffer.go +++ b/go_backend/logbuffer.go @@ -27,7 +27,6 @@ var ( logBufferOnce sync.Once ) -// GetLogBuffer returns the singleton log buffer instance func GetLogBuffer() *LogBuffer { logBufferOnce.Do(func() { globalLogBuffer = &LogBuffer{ @@ -45,7 +44,6 @@ func (lb *LogBuffer) SetLoggingEnabled(enabled bool) { lb.loggingEnabled = enabled } -// IsLoggingEnabled returns whether logging is enabled func (lb *LogBuffer) IsLoggingEnabled() bool { lb.mu.RLock() defer lb.mu.RUnlock() @@ -75,7 +73,6 @@ func (lb *LogBuffer) Add(level, tag, message string) { fmt.Printf("[%s] %s\n", tag, message) } -// GetAll returns all log entries as JSON func (lb *LogBuffer) GetAll() string { lb.mu.RLock() defer lb.mu.RUnlock() @@ -99,21 +96,18 @@ func (lb *LogBuffer) getSince(index int) ([]LogEntry, int) { return entries, len(lb.entries) } -// Clear clears all log entries func (lb *LogBuffer) Clear() { lb.mu.Lock() defer lb.mu.Unlock() lb.entries = lb.entries[:0] } -// Count returns the number of log entries func (lb *LogBuffer) Count() int { lb.mu.RLock() defer lb.mu.RUnlock() return len(lb.entries) } -// Helper functions for logging with different levels func LogDebug(tag, format string, args ...interface{}) { GetLogBuffer().Add("DEBUG", tag, fmt.Sprintf(format, args...)) } @@ -163,15 +157,10 @@ func GoLog(format string, args ...interface{}) { GetLogBuffer().Add(level, tag, message) } -// Exported functions for Flutter - -// GetLogs returns all logs as JSON array func GetLogs() string { return GetLogBuffer().GetAll() } -// GetLogsSince returns logs since the given index -// Returns JSON: {"logs": [...], "next_index": N} func GetLogsSince(index int) string { entries, nextIndex := GetLogBuffer().getSince(index) logsJson, _ := json.Marshal(entries) @@ -179,17 +168,14 @@ func GetLogsSince(index int) string { return result } -// ClearLogs clears all logs func ClearLogs() { GetLogBuffer().Clear() } -// GetLogCount returns the number of log entries func GetLogCount() int { return GetLogBuffer().Count() } -// SetLoggingEnabled enables or disables logging from Flutter func SetLoggingEnabled(enabled bool) { GetLogBuffer().SetLoggingEnabled(enabled) } diff --git a/go_backend/progress.go b/go_backend/progress.go index 8960dddb..95ba3fde 100644 --- a/go_backend/progress.go +++ b/go_backend/progress.go @@ -187,13 +187,13 @@ type ItemProgressWriter struct { writer interface{ Write([]byte) (int, error) } itemID string current int64 - lastReported int64 // Track last reported bytes for threshold-based updates - startTime time.Time // Track start time for speed calculation - lastTime time.Time // Track last update time for speed calculation - lastBytes int64 // Track bytes at last speed calculation + lastReported int64 + startTime time.Time + lastTime time.Time + lastBytes int64 } -const progressUpdateThreshold = 64 * 1024 // Update progress every 64KB +const progressUpdateThreshold = 64 * 1024 func NewItemProgressWriter(w interface{ Write([]byte) (int, error) }, itemID string) *ItemProgressWriter { now := time.Now() diff --git a/go_backend/spotify.go b/go_backend/spotify.go index 7bde0f60..e16f3305 100644 --- a/go_backend/spotify.go +++ b/go_backend/spotify.go @@ -934,14 +934,14 @@ func (c *SpotifyMetadataClient) randomUserAgent() string { defer c.rngMu.Unlock() macMajor := c.rng.Intn(4) + 11 - macMinor := c.rng.Intn(5) + 4 // 4-8 - webkitMajor := c.rng.Intn(7) + 530 // 530-536 - webkitMinor := c.rng.Intn(7) + 30 // 30-36 - chromeMajor := c.rng.Intn(25) + 80 // 80-104 - chromeBuild := c.rng.Intn(1500) + 3000 // 3000-4499 - chromePatch := c.rng.Intn(65) + 60 // 60-124 - safariMajor := c.rng.Intn(7) + 530 // 530-536 - safariMinor := c.rng.Intn(6) + 30 // 30-35 + macMinor := c.rng.Intn(5) + 4 + webkitMajor := c.rng.Intn(7) + 530 + webkitMinor := c.rng.Intn(7) + 30 + chromeMajor := c.rng.Intn(25) + 80 + chromeBuild := c.rng.Intn(1500) + 3000 + chromePatch := c.rng.Intn(65) + 60 + safariMajor := c.rng.Intn(7) + 530 + safariMinor := c.rng.Intn(6) + 30 return fmt.Sprintf( "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_%d_%d) AppleWebKit/%d.%d (KHTML, like Gecko) Chrome/%d.0.%d.%d Safari/%d.%d", diff --git a/go_backend/tidal.go b/go_backend/tidal.go index 92ce4e42..2343c0ee 100644 --- a/go_backend/tidal.go +++ b/go_backend/tidal.go @@ -317,7 +317,6 @@ func (t *TidalDownloader) SearchTrackByISRC(isrc string) (*TidalTrack, error) { return nil, err } - // Find exact ISRC match for i := range result.Items { if result.Items[i].ISRC == isrc { return &result.Items[i], nil @@ -341,7 +340,6 @@ func (t *TidalDownloader) SearchTrackByMetadataWithISRC(trackName, artistName, s // Build search queries - multiple strategies (same as PC version) queries := []string{} - // Strategy 1: Artist + Track name (original) if artistName != "" && trackName != "" { queries = append(queries, artistName+" "+trackName) } @@ -584,7 +582,6 @@ type tidalAPIResult struct { duration time.Duration } -// Returns the first successful result (supports both v1 and v2 API formats) func getDownloadURLParallel(apis []string, trackID int64, quality string) (string, TidalDownloadInfo, error) { if len(apis) == 0 { return "", TidalDownloadInfo{}, fmt.Errorf("no APIs available") diff --git a/lib/providers/explore_provider.dart b/lib/providers/explore_provider.dart index b4e4e718..9af13ec2 100644 --- a/lib/providers/explore_provider.dart +++ b/lib/providers/explore_provider.dart @@ -5,7 +5,6 @@ import 'package:spotiflac_android/providers/extension_provider.dart'; final _log = AppLogger('ExploreProvider'); -/// Represents an item in a Spotify home section class ExploreItem { final String id; final String uri; @@ -50,7 +49,6 @@ class ExploreItem { } } -/// Represents a section in Spotify home feed class ExploreSection { final String uri; final String title; @@ -79,7 +77,6 @@ class ExploreSection { } } -/// State for explore/home feed class ExploreState { final bool isLoading; final String? error; @@ -114,7 +111,6 @@ class ExploreState { } } -/// Calculate greeting based on local device time String _getLocalGreeting() { final hour = DateTime.now().hour; if (hour >= 5 && hour < 12) { @@ -139,7 +135,6 @@ bool _isYTMusicQuickPicksItems(List items) { return true; } -/// Provider for explore/home feed state class ExploreNotifier extends Notifier { @override ExploreState build() { @@ -150,7 +145,6 @@ class ExploreNotifier extends Notifier { Future fetchHomeFeed({bool forceRefresh = false}) async { _log.i('fetchHomeFeed called, forceRefresh=$forceRefresh'); - // Don't refetch if we have data and it's less than 5 minutes old if (!forceRefresh && state.hasContent && state.lastFetched != null && @@ -167,11 +161,9 @@ class ExploreNotifier extends Notifier { state = state.copyWith(isLoading: true, error: null); try { - // Find any extension with homeFeed capability final extState = ref.read(extensionProvider); _log.d('Extensions count: ${extState.extensions.length}'); - // Look for extensions with homeFeed capability (prefer spotify-web) Extension? targetExt; for (final extension in extState.extensions) { if (!extension.enabled || !extension.hasHomeFeed) { @@ -225,14 +217,11 @@ class ExploreNotifier extends Notifier { _log.i('Fetched ${sections.length} sections'); - // Debug: log first section items if (sections.isNotEmpty && sections.first.items.isNotEmpty) { final firstItem = sections.first.items.first; _log.d('First item: name=${firstItem.name}, artists=${firstItem.artists}, type=${firstItem.type}'); } - // Always use local device time for greeting to avoid timezone issues - // Extension greeting may use wrong timezone (UTC or Spotify account timezone) final localGreeting = _getLocalGreeting(); _log.d('Greeting from extension: $greeting, using local: $localGreeting'); @@ -251,15 +240,14 @@ class ExploreNotifier extends Notifier { } } - /// Clear cached data void clear() { state = const ExploreState(); } - /// Refresh home feed Future refresh() => fetchHomeFeed(forceRefresh: true); } + final exploreProvider = NotifierProvider(() { return ExploreNotifier(); }); diff --git a/lib/providers/extension_provider.dart b/lib/providers/extension_provider.dart index e9893c9c..7ee38ca3 100644 --- a/lib/providers/extension_provider.dart +++ b/lib/providers/extension_provider.dart @@ -452,7 +452,6 @@ class ExtensionNotifier extends Notifier { return const ExtensionState(); } - /// Initialize the extension system Future initialize(String extensionsDir, String dataDir) async { if (state.isInitialized) return; @@ -485,7 +484,6 @@ class ExtensionNotifier extends Notifier { } } - /// Refresh the list of installed extensions Future refreshExtensions() async { try { final list = await PlatformBridge.getInstalledExtensions(); @@ -493,7 +491,6 @@ class ExtensionNotifier extends Notifier { state = state.copyWith(extensions: extensions); _log.d('Loaded ${extensions.length} extensions'); - // Log search behavior for extensions that have it for (final ext in extensions) { if (ext.searchBehavior != null) { _log.d('Extension ${ext.id}: thumbnailRatio=${ext.searchBehavior!.thumbnailRatio}'); @@ -505,6 +502,7 @@ class ExtensionNotifier extends Notifier { } } + void clearError() { state = state.copyWith(error: null); } @@ -550,7 +548,6 @@ class ExtensionNotifier extends Notifier { } } - /// Uninstall/remove an extension Future removeExtension(String extensionId) async { state = state.copyWith(isLoading: true, error: null); @@ -567,6 +564,7 @@ class ExtensionNotifier extends Notifier { } } + Future setExtensionEnabled(String extensionId, bool enabled) async { try { await PlatformBridge.setExtensionEnabled(extensionId, enabled); @@ -603,7 +601,6 @@ class ExtensionNotifier extends Notifier { } } - /// Get settings for an extension Future> getExtensionSettings(String extensionId) async { try { return await PlatformBridge.getExtensionSettings(extensionId); @@ -623,7 +620,6 @@ class ExtensionNotifier extends Notifier { } } - /// Load provider priority order Future loadProviderPriority() async { try { final priority = await PlatformBridge.getProviderPriority(); @@ -633,6 +629,7 @@ class ExtensionNotifier extends Notifier { } } + Future setProviderPriority(List priority) async { try { await PlatformBridge.setProviderPriority(priority); @@ -644,7 +641,6 @@ class ExtensionNotifier extends Notifier { } } - /// Load metadata provider priority order Future loadMetadataProviderPriority() async { try { final priority = await PlatformBridge.getMetadataProviderPriority(); @@ -665,7 +661,6 @@ class ExtensionNotifier extends Notifier { } } - /// Cleanup all extensions (call on app close) Future cleanup() async { try { await PlatformBridge.cleanupExtensions(); @@ -683,7 +678,6 @@ class ExtensionNotifier extends Notifier { } } - /// Get all enabled extensions List get enabledExtensions { return state.extensions.where((ext) => ext.enabled).toList(); } @@ -698,7 +692,6 @@ class ExtensionNotifier extends Notifier { return providers; } - /// Get all metadata providers (built-in + extensions) List getAllMetadataProviders() { final providers = ['deezer', 'spotify']; for (final ext in state.extensions) { @@ -708,6 +701,7 @@ class ExtensionNotifier extends Notifier { } return providers; } + List get searchProviders { return state.extensions.where((ext) => ext.enabled && ext.hasCustomSearch).toList(); } diff --git a/lib/providers/store_provider.dart b/lib/providers/store_provider.dart index 3fdd824f..e967825a 100644 --- a/lib/providers/store_provider.dart +++ b/lib/providers/store_provider.dart @@ -7,8 +7,6 @@ import 'package:spotiflac_android/providers/extension_provider.dart'; final _log = AppLogger('StoreProvider'); final RegExp _leadingVersionPrefix = RegExp(r'^v'); -/// Compare two semantic version strings -/// Returns: -1 if v1 < v2, 0 if equal, 1 if v1 > v2 int compareVersions(String v1, String v2) { final parts1 = v1.replaceAll(_leadingVersionPrefix, '').split('.'); final parts2 = v2.replaceAll(_leadingVersionPrefix, '').split('.'); @@ -25,8 +23,8 @@ int compareVersions(String v1, String v2) { return 0; } -/// Extension categories class StoreCategory { + static const String metadata = 'metadata'; static const String download = 'download'; static const String utility = 'utility'; @@ -111,13 +109,13 @@ class StoreExtension { ); } - /// Check if this extension requires a higher app version than current bool get requiresNewerApp { if (minAppVersion == null || minAppVersion!.isEmpty) return false; return compareVersions(minAppVersion!, AppInfo.version) > 0; } } + class StoreState { final List extensions; final String? selectedCategory; @@ -164,7 +162,6 @@ class StoreState { ); } - /// Get filtered extensions based on category and search List get filteredExtensions { var result = extensions; @@ -186,13 +183,11 @@ class StoreState { return result; } - /// Count of extensions with updates available int get updatesAvailableCount { return extensions.where((e) => e.hasUpdate).length; } } -/// Provider for managing extension store class StoreNotifier extends Notifier { @override StoreState build() { @@ -215,7 +210,6 @@ class StoreNotifier extends Notifier { } } - /// Refresh extensions from store Future refresh({bool forceRefresh = false}) async { state = state.copyWith(isLoading: true, clearError: true); @@ -240,7 +234,6 @@ class StoreNotifier extends Notifier { } } - /// Set search query void setSearchQuery(String query) { state = state.copyWith(searchQuery: query); } @@ -249,7 +242,6 @@ class StoreNotifier extends Notifier { state = state.copyWith(searchQuery: '', clearCategory: true); } - /// Download and install extension Future installExtension(String extensionId, String tempDir, String extensionsDir) async { state = state.copyWith(isDownloading: true, downloadingId: extensionId, clearError: true); @@ -275,6 +267,7 @@ class StoreNotifier extends Notifier { } } + Future updateExtension(String extensionId, String tempDir) async { state = state.copyWith(isDownloading: true, downloadingId: extensionId, clearError: true); diff --git a/lib/providers/theme_provider.dart b/lib/providers/theme_provider.dart index 7c38b33c..ad95b21b 100644 --- a/lib/providers/theme_provider.dart +++ b/lib/providers/theme_provider.dart @@ -3,23 +3,19 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:spotiflac_android/models/theme_settings.dart'; -/// Provider for theme settings state management final themeProvider = NotifierProvider(() { return ThemeNotifier(); }); -/// Notifier for managing theme settings with persistence class ThemeNotifier extends Notifier { final Future _prefs = SharedPreferences.getInstance(); @override ThemeSettings build() { - // Load settings asynchronously on first access _loadFromStorage(); return const ThemeSettings(); } - /// Load theme settings from SharedPreferences Future _loadFromStorage() async { try { final prefs = await _prefs; @@ -39,7 +35,6 @@ class ThemeNotifier extends Notifier { } } - /// Save current settings to SharedPreferences Future _saveToStorage() async { try { final prefs = await _prefs; @@ -52,13 +47,11 @@ class ThemeNotifier extends Notifier { } } - /// Set theme mode (light, dark, or system) Future setThemeMode(ThemeMode mode) async { state = state.copyWith(themeMode: mode); await _saveToStorage(); } - /// Enable or disable dynamic color from wallpaper Future setUseDynamicColor(bool value) async { state = state.copyWith(useDynamicColor: value); await _saveToStorage(); @@ -70,19 +63,16 @@ class ThemeNotifier extends Notifier { await _saveToStorage(); } - /// Set seed color from int value Future setSeedColorValue(int colorValue) async { state = state.copyWith(seedColorValue: colorValue); await _saveToStorage(); } - /// Enable or disable AMOLED mode (pure black background) Future setUseAmoled(bool value) async { state = state.copyWith(useAmoled: value); await _saveToStorage(); } - /// Helper to convert string to ThemeMode ThemeMode _themeModeFromString(String? value) { if (value == null) return ThemeMode.system; return ThemeMode.values.firstWhere( @@ -91,3 +81,4 @@ class ThemeNotifier extends Notifier { ); } } + diff --git a/lib/services/palette_service.dart b/lib/services/palette_service.dart index 358221ad..89e0e615 100644 --- a/lib/services/palette_service.dart +++ b/lib/services/palette_service.dart @@ -9,7 +9,6 @@ class PaletteService { static final PaletteService instance = PaletteService._(); PaletteService._(); - /// Cache for already computed colors final Map _colorCache = {}; /// Extract dominant color from a network image URL @@ -47,7 +46,6 @@ class PaletteService { } } - /// Extract dominant color from a local file path Future extractDominantColorFromFile(String? filePath) async { if (filePath == null || filePath.isEmpty) return null; @@ -81,12 +79,10 @@ class PaletteService { } } - /// Clear the color cache void clearCache() { _colorCache.clear(); } - /// Get cached color without computing Color? getCached(String? imageUrl) { if (imageUrl == null) return null; return _colorCache[imageUrl]; diff --git a/lib/widgets/settings_group.dart b/lib/widgets/settings_group.dart index 9d7eafb5..3db24a9d 100644 --- a/lib/widgets/settings_group.dart +++ b/lib/widgets/settings_group.dart @@ -43,8 +43,8 @@ class SettingsGroup extends StatelessWidget { } } -/// A single settings item that can be used inside SettingsGroup class SettingsItem extends StatelessWidget { + final IconData? icon; final String title; final String? subtitle; @@ -125,8 +125,8 @@ class SettingsItem extends StatelessWidget { } } -/// A switch settings item for SettingsGroup class SettingsSwitchItem extends StatelessWidget { + final IconData? icon; final String title; final String? subtitle; @@ -213,8 +213,8 @@ class SettingsSwitchItem extends StatelessWidget { } } -/// Section header for settings groups class SettingsSectionHeader extends StatelessWidget { + final String title; const SettingsSectionHeader({super.key, required this.title});