diff --git a/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt b/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt index 327bc820..53cc7fa3 100644 --- a/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt +++ b/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt @@ -2198,14 +2198,6 @@ class MainActivity: FlutterFragmentActivity() { } result.success(null) } - "checkAvailability" -> { - val spotifyId = call.argument("spotify_id") ?: "" - val isrc = call.argument("isrc") ?: "" - val response = withContext(Dispatchers.IO) { - Gobackend.checkAvailability(spotifyId, isrc) - } - result.success(response) - } "downloadByStrategy" -> { val requestJson = call.arguments as String val response = withContext(Dispatchers.IO) { @@ -3038,13 +3030,6 @@ class MainActivity: FlutterFragmentActivity() { } result.success(payload) } - "preWarmTrackCache" -> { - val tracksJson = call.argument("tracks") ?: "[]" - withContext(Dispatchers.IO) { - Gobackend.preWarmTrackCacheJSON(tracksJson) - } - result.success(null) - } "getTrackCacheSize" -> { val size = withContext(Dispatchers.IO) { Gobackend.getTrackCacheSize() @@ -3097,22 +3082,6 @@ class MainActivity: FlutterFragmentActivity() { } result.success(response) } - "checkAvailabilityFromDeezerID" -> { - val deezerTrackId = call.argument("deezer_track_id") ?: "" - val response = withContext(Dispatchers.IO) { - Gobackend.checkAvailabilityFromDeezerID(deezerTrackId) - } - result.success(response) - } - "checkAvailabilityByPlatformID" -> { - val platform = call.argument("platform") ?: "" - val entityType = call.argument("entity_type") ?: "" - val entityId = call.argument("entity_id") ?: "" - val response = withContext(Dispatchers.IO) { - Gobackend.checkAvailabilityByPlatformID(platform, entityType, entityId) - } - result.success(response) - } "getSpotifyIDFromDeezerTrack" -> { val deezerTrackId = call.argument("deezer_track_id") ?: "" val response = withContext(Dispatchers.IO) { diff --git a/go_backend/audio_metadata.go b/go_backend/audio_metadata.go index 9cb0c1c9..0c8aba26 100644 --- a/go_backend/audio_metadata.go +++ b/go_backend/audio_metadata.go @@ -186,7 +186,7 @@ func parseID3v22Frames(data []byte, metadata *AudioMetadata, tagUnsync bool) { case "TCR": metadata.Copyright = value case "ULT": - if v := extractLyricsFrame(frameData); v != "" && metadata.Lyrics == "" { + if v := extractLangTextFrame(frameData); v != "" && metadata.Lyrics == "" { metadata.Lyrics = v } case "TXX": @@ -307,11 +307,11 @@ func parseID3v23Frames(data []byte, metadata *AudioMetadata, version byte, tagUn case "TCOP": metadata.Copyright = value case "COMM": - if v := extractCommentFrame(frameData); v != "" { + if v := extractLangTextFrame(frameData); v != "" { metadata.Comment = v } case "USLT": - if v := extractLyricsFrame(frameData); v != "" && metadata.Lyrics == "" { + if v := extractLangTextFrame(frameData); v != "" && metadata.Lyrics == "" { metadata.Lyrics = v } case "TXXX": @@ -391,7 +391,9 @@ func extractTextFrame(data []byte) string { } } -func extractCommentFrame(data []byte) string { +// extractLangTextFrame decodes ID3 frames with an encoding byte, 3-byte +// language code, and null-terminated descriptor before the text (COMM, USLT). +func extractLangTextFrame(data []byte) string { if len(data) < 5 { return "" } @@ -426,42 +428,6 @@ func extractCommentFrame(data []byte) string { return extractTextFrame(framed) } -func extractLyricsFrame(data []byte) string { - if len(data) < 5 { - return "" - } - - encoding := data[0] - rest := data[4:] - - var text []byte - switch encoding { - case 1, 2: - for i := 0; i+1 < len(rest); i += 2 { - if rest[i] == 0 && rest[i+1] == 0 { - text = rest[i+2:] - break - } - } - default: - idx := bytes.IndexByte(rest, 0) - if idx >= 0 && idx+1 < len(rest) { - text = rest[idx+1:] - } else { - text = rest - } - } - - if len(text) == 0 { - return "" - } - - framed := make([]byte, 1+len(text)) - framed[0] = encoding - copy(framed[1:], text) - return extractTextFrame(framed) -} - func extractUserTextFrame(data []byte) (string, string) { if len(data) < 2 { return "", "" @@ -582,11 +548,6 @@ func cleanGenre(genre string) string { return genre } -func parseTrackNumber(s string) int { - num, _ := parseIndexPair(s) - return num -} - func parseIndexPair(s string) (int, int) { s = strings.TrimSpace(s) if s == "" { @@ -1554,10 +1515,6 @@ func parseFLACPictureBlock(data []byte) ([]byte, string) { return imageData, mimeType } -func extractAnyCoverArt(filePath string) ([]byte, string, error) { - return extractAnyCoverArtWithHint(filePath, "") -} - func extractAnyCoverArtWithHint(filePath, displayNameHint string) ([]byte, string, error) { ext := strings.ToLower(filepath.Ext(filePath)) if ext == "" { @@ -1605,14 +1562,6 @@ func extractAnyCoverArtWithHint(filePath, displayNameHint string) ([]byte, strin } } -func SaveCoverToCache(filePath, cacheDir string) (string, error) { - return SaveCoverToCacheWithHintAndKey(filePath, "", cacheDir, "") -} - -func SaveCoverToCacheWithHint(filePath, displayNameHint, cacheDir string) (string, error) { - return SaveCoverToCacheWithHintAndKey(filePath, displayNameHint, cacheDir, "") -} - func resolveLibraryCoverCacheKey(filePath, explicitKey string) string { explicitKey = strings.TrimSpace(explicitKey) if explicitKey != "" { diff --git a/go_backend/audio_metadata_supplement_test.go b/go_backend/audio_metadata_supplement_test.go index eb83b07d..c752f01f 100644 --- a/go_backend/audio_metadata_supplement_test.go +++ b/go_backend/audio_metadata_supplement_test.go @@ -85,9 +85,6 @@ func TestAudioMetadataID3ParsingBranches(t *testing.T) { if n, total := parseIndexPair(" 8 / 10 "); n != 8 || total != 10 { t.Fatalf("parseIndexPair = %d/%d", n, total) } - if got := parseTrackNumber("9/11"); got != 9 { - t.Fatalf("parseTrackNumber = %d", got) - } if got := removeUnsync([]byte{0xff, 0x00, 0xe0}); !bytes.Equal(got, []byte{0xff, 0xe0}) { t.Fatalf("removeUnsync = %#v", got) } @@ -424,9 +421,6 @@ func TestOggMetadataQualityAndCoverHelpers(t *testing.T) { if image, mime, err := extractAnyCoverArtWithHint(coverPath, "cover.opus"); err != nil || mime != "image/png" || len(image) == 0 { t.Fatalf("extractAnyCoverArtWithHint = %s/%#v/%v", mime, image, err) } - if image, mime, err := extractAnyCoverArt(coverPath); err != nil || mime != "image/png" || len(image) == 0 { - t.Fatalf("extractAnyCoverArt = %s/%#v/%v", mime, image, err) - } extractedCoverPath := filepath.Join(dir, "extracted.png") if err := ExtractCoverToFile(coverPath, extractedCoverPath); err != nil { t.Fatalf("ExtractCoverToFile = %v", err) @@ -438,17 +432,6 @@ func TestOggMetadataQualityAndCoverHelpers(t *testing.T) { if err != nil || cachePath == "" { t.Fatalf("SaveCoverToCacheWithHintAndKey = %q/%v", cachePath, err) } - cacheDir := filepath.Join(dir, "cache") - if path, err := SaveCoverToCache(coverPath, cacheDir); err != nil || !strings.HasSuffix(path, ".png") { - t.Fatalf("SaveCoverToCache = %q/%v", path, err) - } - if path, err := SaveCoverToCacheWithHint(coverPath, "cover.opus", cacheDir); err != nil || path == "" { - t.Fatalf("SaveCoverToCacheWithHint = %q/%v", path, err) - } - hitPath, err := SaveCoverToCache(coverPath, cacheDir) - if err != nil || hitPath == "" { - t.Fatalf("SaveCoverToCache cache hit = %q/%v", hitPath, err) - } if _, err := SaveCoverToCacheWithHintAndKey(filepath.Join(dir, "missing.opus"), "missing.opus", dir, "missing"); err == nil { t.Fatal("expected missing cover cache error") } diff --git a/go_backend/cover.go b/go_backend/cover.go index c274bedc..0c8c3830 100644 --- a/go_backend/cover.go +++ b/go_backend/cover.go @@ -247,17 +247,3 @@ func upgradeQobuzCover(coverURL string) string { } return upgraded } - -func GetCoverFromSpotify(imageURL string, maxQuality bool) string { - if imageURL == "" { - return "" - } - - result := convertSmallToMedium(imageURL) - - if maxQuality { - result = upgradeToMaxQuality(result) - } - - return result -} diff --git a/go_backend/exports.go b/go_backend/exports.go index 1a519c18..eee7b700 100644 --- a/go_backend/exports.go +++ b/go_backend/exports.go @@ -40,16 +40,6 @@ func ReleaseMemory() { debug.FreeOSMemory() } -func CheckAvailability(spotifyID, isrc string) (string, error) { - client := NewSongLinkClient() - availability, err := client.CheckTrackAvailability(spotifyID, isrc) - if err != nil { - return "", err - } - - return marshalJSONString(availability) -} - // SetSongLinkNetworkOptions is kept for backward compatibility. func SetSongLinkNetworkOptions(allowHTTP, insecureTLS bool) { SetNetworkCompatibilityOptions(allowHTTP, insecureTLS) diff --git a/go_backend/exports_deezer.go b/go_backend/exports_deezer.go index fbbc60b1..2b4443c4 100644 --- a/go_backend/exports_deezer.go +++ b/go_backend/exports_deezer.go @@ -2,53 +2,19 @@ package gobackend import ( "context" - "encoding/json" "fmt" "strings" "time" ) -func PreWarmTrackCacheJSON(tracksJSON string) (string, error) { - var tracks []struct { - ISRC string `json:"isrc"` - TrackName string `json:"track_name"` - ArtistName string `json:"artist_name"` - SpotifyID string `json:"spotify_id"` - Service string `json:"service"` - } - - if err := json.Unmarshal([]byte(tracksJSON), &tracks); err != nil { - return errorResponse("Invalid JSON: " + err.Error()) - } - - requests := make([]PreWarmCacheRequest, len(tracks)) - for i, t := range tracks { - requests[i] = PreWarmCacheRequest{ - ISRC: t.ISRC, - TrackName: t.TrackName, - ArtistName: t.ArtistName, - SpotifyID: t.SpotifyID, - Service: t.Service, - } - } - - go PreWarmTrackCache(requests) - - resp := map[string]any{ - "success": true, - "message": fmt.Sprintf("Pre-warming cache for %d tracks in background", len(tracks)), - } - - s, _ := marshalJSONString(resp) - return s, nil -} - +// GetTrackCacheSize and ClearTrackIDCache back the Settings cache screen. The +// track-ID cache is currently a no-op, so these report an empty cache and clear +// nothing, but the gomobile export contract is kept for the Dart/Kotlin callers. func GetTrackCacheSize() int { - return GetCacheSize() + return 0 } func ClearTrackIDCache() { - ClearTrackCache() } func GetDeezerRelatedArtists(artistID string, limit int) (string, error) { @@ -237,26 +203,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) } -func CheckAvailabilityFromDeezerID(deezerTrackID string) (string, error) { - client := NewSongLinkClient() - availability, err := client.CheckAvailabilityFromDeezer(deezerTrackID) - if err != nil { - return "", err - } - - return marshalJSONString(availability) -} - -func CheckAvailabilityByPlatformID(platform, entityType, entityID string) (string, error) { - client := NewSongLinkClient() - availability, err := client.CheckAvailabilityByPlatform(platform, entityType, entityID) - if err != nil { - return "", err - } - - return marshalJSONString(availability) -} - func GetSpotifyIDFromDeezerTrack(deezerTrackID string) (string, error) { client := NewSongLinkClient() return client.GetSpotifyIDFromDeezer(deezerTrackID) diff --git a/go_backend/exports_download.go b/go_backend/exports_download.go index 5acf503f..c176d87d 100644 --- a/go_backend/exports_download.go +++ b/go_backend/exports_download.go @@ -358,10 +358,6 @@ func applySongLinkRegionFromRequest(req *DownloadRequest) { SetSongLinkRegion(req.SongLinkRegion) } -func DownloadTrack(requestJSON string) (string, error) { - return errorResponse("Built-in download providers have been retired. Use downloadByStrategy with extension providers.") -} - // DownloadByStrategy routes all download requests through extension providers. func DownloadByStrategy(requestJSON string) (string, error) { var req DownloadRequest @@ -385,10 +381,6 @@ func DownloadByStrategy(requestJSON string) (string, error) { return errorResponse("Extension providers are disabled; built-in download providers have been retired") } -func DownloadWithFallback(requestJSON string) (string, error) { - return errorResponse("Built-in fallback has been retired. Use extension fallback through downloadByStrategy.") -} - func GetDownloadProgress() string { progress := getProgress() jsonBytes, _ := json.Marshal(progress) diff --git a/go_backend/exports_extensions.go b/go_backend/exports_extensions.go index 420e7855..328876ef 100644 --- a/go_backend/exports_extensions.go +++ b/go_backend/exports_extensions.go @@ -394,11 +394,6 @@ func SetExtensionFallbackProviderIDsJSON(providerIDsJSON string) error { return nil } -func GetExtensionFallbackProviderIDsJSON() (string, error) { - providerIDs := GetExtensionFallbackProviderIDs() - return marshalJSONString(providerIDs) -} - func SetMetadataProviderPriorityJSON(priorityJSON string) error { var priority []string if err := json.Unmarshal([]byte(priorityJSON), &priority); err != nil { diff --git a/go_backend/exports_library.go b/go_backend/exports_library.go index f14bee9f..b342989e 100644 --- a/go_backend/exports_library.go +++ b/go_backend/exports_library.go @@ -28,10 +28,6 @@ func ReadAudioMetadataJSON(filePath string) (string, error) { return ReadAudioMetadata(filePath) } -func ReadAudioMetadataWithHintJSON(filePath, displayName string) (string, error) { - return ReadAudioMetadataWithDisplayName(filePath, displayName) -} - func ReadAudioMetadataWithHintAndCoverCacheKeyJSON(filePath, displayName, coverCacheKey string) (string, error) { return ReadAudioMetadataWithDisplayNameAndCoverCacheKey(filePath, displayName, coverCacheKey) } diff --git a/go_backend/exports_songlink_lyrics_supplement_test.go b/go_backend/exports_songlink_lyrics_supplement_test.go index 5d2c635f..be0abe81 100644 --- a/go_backend/exports_songlink_lyrics_supplement_test.go +++ b/go_backend/exports_songlink_lyrics_supplement_test.go @@ -1,7 +1,6 @@ package gobackend import ( - "context" "io" "net/http" "os" @@ -55,13 +54,9 @@ func TestLyricsExportWrappersWithoutNetwork(t *testing.T) { func TestSongLinkExportWrappersWithFakeClient(t *testing.T) { origClient := globalSongLinkClient origRetryConfig := songLinkRetryConfig - origSearchByISRC := songLinkSearchByISRC - origCheckFromDeezer := songLinkCheckAvailabilityFromDeezer defer func() { globalSongLinkClient = origClient songLinkRetryConfig = origRetryConfig - songLinkSearchByISRC = origSearchByISRC - songLinkCheckAvailabilityFromDeezer = origCheckFromDeezer SetSongLinkNetworkOptions(false, false) }() songLinkRetryConfig = func() RetryConfig { @@ -82,15 +77,6 @@ func TestSongLinkExportWrappersWithFakeClient(t *testing.T) { songLinkClientOnce.Do(func() {}) SetSongLinkNetworkOptions(true, true) - if availabilityJSON, err := CheckAvailability("spotify-1", ""); err != nil || !strings.Contains(availabilityJSON, `"deezer_id":"101"`) { - t.Fatalf("CheckAvailability = %q/%v", availabilityJSON, err) - } - if availabilityJSON, err := CheckAvailabilityFromDeezerID("101"); err != nil || !strings.Contains(availabilityJSON, `"spotify_id":"spotify-1"`) { - t.Fatalf("CheckAvailabilityFromDeezerID = %q/%v", availabilityJSON, err) - } - if availabilityJSON, err := CheckAvailabilityByPlatformID("deezer", "song", "101"); err != nil || !strings.Contains(availabilityJSON, `"tidal_url"`) { - t.Fatalf("CheckAvailabilityByPlatformID = %q/%v", availabilityJSON, err) - } if spotifyID, err := GetSpotifyIDFromDeezerTrack("101"); err != nil || spotifyID != "spotify-1" { t.Fatalf("GetSpotifyIDFromDeezerTrack = %q/%v", spotifyID, err) } @@ -122,15 +108,6 @@ func TestSongLinkExportWrappersWithFakeClient(t *testing.T) { t.Fatalf("CheckAvailabilityFromURL = %#v/%v", availability, err) } - songLinkSearchByISRC = func(ctx context.Context, isrc string) (*TrackMetadata, error) { - return &TrackMetadata{SpotifyID: "deezer:101", ExternalURL: "https://www.deezer.com/track/101"}, nil - } - songLinkCheckAvailabilityFromDeezer = func(s *SongLinkClient, deezerTrackID string) (*TrackAvailability, error) { - return &TrackAvailability{SpotifyID: "spotify-1", Deezer: true, DeezerID: deezerTrackID}, nil - } - if availabilityJSON, err := CheckAvailability("", "USRC17607839"); err != nil || !strings.Contains(availabilityJSON, `"deezer_id":"101"`) { - t.Fatalf("CheckAvailability by ISRC = %q/%v", availabilityJSON, err) - } if songLinkExtractDeezerTrackID(nil) != "" || songLinkExtractDeezerTrackID(&TrackMetadata{ExternalURL: "https://www.deezer.com/track/202"}) != "202" { t.Fatal("songLinkExtractDeezerTrackID mismatch") } diff --git a/go_backend/exports_supplement_test.go b/go_backend/exports_supplement_test.go index 99fbfa28..e82b96dc 100644 --- a/go_backend/exports_supplement_test.go +++ b/go_backend/exports_supplement_test.go @@ -91,18 +91,12 @@ func TestExportsJSONWrappersAndExtensionManagerSurface(t *testing.T) { manager.mu.Unlock() }() - if response, err := DownloadTrack(`{}`); err != nil || !strings.Contains(response, "retired") { - t.Fatalf("DownloadTrack = %q/%v", response, err) - } if response, err := DownloadByStrategy(`not-json`); err != nil || !strings.Contains(response, "Invalid request") { t.Fatalf("DownloadByStrategy invalid = %q/%v", response, err) } if response, err := DownloadByStrategy(`{"use_extensions":false}`); err != nil || !strings.Contains(response, "disabled") { t.Fatalf("DownloadByStrategy disabled = %q/%v", response, err) } - if response, err := DownloadWithFallback(`{}`); err != nil || !strings.Contains(response, "retired") { - t.Fatalf("DownloadWithFallback = %q/%v", response, err) - } InitItemProgress("item-1") FinishItemProgress("item-1") @@ -183,12 +177,6 @@ func TestExportsJSONWrappersAndExtensionManagerSurface(t *testing.T) { t.Fatalf("SanitizeFilename = %q", got) } - if response, err := PreWarmTrackCacheJSON(`not-json`); err != nil || !strings.Contains(response, "Invalid JSON") { - t.Fatalf("PreWarmTrackCacheJSON invalid = %q/%v", response, err) - } - if response, err := PreWarmTrackCacheJSON(`[{"isrc":"ISRC","track_name":"Song","artist_name":"Artist"}]`); err != nil || !strings.Contains(response, "success") { - t.Fatalf("PreWarmTrackCacheJSON = %q/%v", response, err) - } if GetTrackCacheSize() != 0 { t.Fatal("expected empty track cache") } @@ -219,9 +207,6 @@ func TestExportsJSONWrappersAndExtensionManagerSurface(t *testing.T) { if err := SetExtensionFallbackProviderIDsJSON(`["coverage-ext"]`); err != nil { t.Fatalf("SetExtensionFallbackProviderIDsJSON: %v", err) } - if jsonText, err := GetExtensionFallbackProviderIDsJSON(); err != nil || !strings.Contains(jsonText, "coverage-ext") { - t.Fatalf("GetExtensionFallbackProviderIDsJSON = %q/%v", jsonText, err) - } if err := SetExtensionFallbackProviderIDsJSON(""); err != nil { t.Fatalf("reset extension fallback IDs: %v", err) } @@ -484,9 +469,6 @@ func TestExportsJSONWrappersAndExtensionManagerSurface(t *testing.T) { if metadataJSON, err := ReadAudioMetadataJSON(filepath.Join(libraryDir, "missing.mp3")); err != nil || metadataJSON == "" { t.Fatalf("ReadAudioMetadataJSON = %q/%v", metadataJSON, err) } - if metadataJSON, err := ReadAudioMetadataWithHintJSON(filepath.Join(libraryDir, "missing.mp3"), "Missing"); err != nil || metadataJSON == "" { - t.Fatalf("ReadAudioMetadataWithHintJSON = %q/%v", metadataJSON, err) - } if metadataJSON, err := ReadAudioMetadataWithHintAndCoverCacheKeyJSON(filepath.Join(libraryDir, "missing.mp3"), "Missing", "key"); err != nil || metadataJSON == "" { t.Fatalf("ReadAudioMetadataWithHintAndCoverCacheKeyJSON = %q/%v", metadataJSON, err) } diff --git a/go_backend/extension_health_misc_supplement_test.go b/go_backend/extension_health_misc_supplement_test.go index 776a5b0b..94bcf6d8 100644 --- a/go_backend/extension_health_misc_supplement_test.go +++ b/go_backend/extension_health_misc_supplement_test.go @@ -71,11 +71,7 @@ func TestExtensionHealthClassificationAndValidation(t *testing.T) { } } -func TestCoverRomajiParallelAndIDHSHelpers(t *testing.T) { - spotify := "https://i.scdn.co/image/ab67616d00001e02abcdef" - if got := GetCoverFromSpotify(spotify, true); !strings.Contains(got, spotifySizeMax) { - t.Fatalf("spotify cover = %q", got) - } +func TestCoverAndIDHSHelpers(t *testing.T) { if got := upgradeToMaxQuality("https://cdn-images.dzcdn.net/images/cover/abc/500x500-000000-80-0-0.jpg"); !strings.Contains(got, "1800x1800") { t.Fatalf("deezer cover = %q", got) } @@ -89,32 +85,6 @@ func TestCoverRomajiParallelAndIDHSHelpers(t *testing.T) { t.Fatalf("expected empty cover error") } - if !ContainsJapanese("カタカナ") || ContainsJapanese("abc") { - t.Fatal("unexpected Japanese detection") - } - if got := JapaneseToRomaji("きゃット"); got != "kyatto" { - t.Fatalf("romaji = %q", got) - } - if got := BuildSearchQuery("きゃ! song", "アーティスト"); got != "atisuto kya song" { - t.Fatalf("query = %q", got) - } - if got := CleanToASCII("A, B. C!"); got != "A B C" { - t.Fatalf("ascii = %q", got) - } - - if err := PreWarmCache(`not-json`); err == nil { - t.Fatal("expected prewarm JSON error") - } - if err := PreWarmCache(`[{"isrc":"ISRC","track_name":"Song","artist_name":"Artist","spotify_id":"sp","service":"tidal"}]`); err != nil { - t.Fatalf("PreWarmCache: %v", err) - } - if result := FetchCoverAndLyricsParallel("", false, "", "", "", false, 0); result == nil || result.CoverErr != nil || result.LyricsErr != nil { - t.Fatalf("parallel result = %#v", result) - } - if ClearTrackCache(); GetCacheSize() != 0 { - t.Fatal("expected empty cache size") - } - client := &IDHSClient{client: &http.Client{Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) { if req.Method != http.MethodPost { t.Fatalf("method = %s", req.Method) diff --git a/go_backend/extension_runtime_file.go b/go_backend/extension_runtime_file.go index 40b798e3..c2143129 100644 --- a/go_backend/extension_runtime_file.go +++ b/go_backend/extension_runtime_file.go @@ -18,13 +18,6 @@ var ( allowedDownloadDirsMu sync.RWMutex ) -func SetAllowedDownloadDirs(dirs []string) { - allowedDownloadDirsMu.Lock() - defer allowedDownloadDirsMu.Unlock() - allowedDownloadDirs = dirs - GoLog("[Extension] Allowed download directories set: %v\n", dirs) -} - func AddAllowedDownloadDir(dir string) { allowedDownloadDirsMu.Lock() defer allowedDownloadDirsMu.Unlock() @@ -34,6 +27,15 @@ func AddAllowedDownloadDir(dir string) { } } +// SetAllowedDownloadDirs replaces the whole allow-list in one call (passing nil +// clears it). Used by tests to reset the sandbox between cases; production code +// appends via AddAllowedDownloadDir. +func SetAllowedDownloadDirs(dirs []string) { + allowedDownloadDirsMu.Lock() + defer allowedDownloadDirsMu.Unlock() + allowedDownloadDirs = dirs +} + func isPathInAllowedDirs(absPath string) bool { allowedDownloadDirsMu.RLock() defer allowedDownloadDirsMu.RUnlock() diff --git a/go_backend/extension_runtime_storage.go b/go_backend/extension_runtime_storage.go index acdc52c4..70d00b00 100644 --- a/go_backend/extension_runtime_storage.go +++ b/go_backend/extension_runtime_storage.go @@ -98,16 +98,6 @@ func (r *extensionRuntime) ensureStorageLoaded() error { return nil } -func (r *extensionRuntime) loadStorage() (map[string]any, error) { - if err := r.ensureStorageLoaded(); err != nil { - return nil, err - } - - r.storageMu.RLock() - defer r.storageMu.RUnlock() - return cloneInterfaceMap(r.storageCache), nil -} - func (r *extensionRuntime) queueStorageFlushLocked(delay time.Duration) { if r.storageClosed { return diff --git a/go_backend/extension_runtime_supplement_test.go b/go_backend/extension_runtime_supplement_test.go index 39ac5498..d35192d2 100644 --- a/go_backend/extension_runtime_supplement_test.go +++ b/go_backend/extension_runtime_supplement_test.go @@ -635,10 +635,6 @@ func TestExtensionStoreSettingsAndRuntimeStorage(t *testing.T) { if ok := runtime.storageSet(goja.FunctionCall{Arguments: []goja.Value{vm.ToValue("key"), vm.ToValue(map[string]any{"nested": "value"})}}); !ok.ToBoolean() { t.Fatal("storageSet equal false") } - loaded, err := runtime.loadStorage() - if err != nil || loaded["key"] == nil { - t.Fatalf("loadStorage = %#v/%v", loaded, err) - } if err := runtime.flushStorageNow(); err != nil { t.Fatalf("flushStorageNow: %v", err) } diff --git a/go_backend/extension_timeout.go b/go_backend/extension_timeout.go index c5163320..a1b6fbe9 100644 --- a/go_backend/extension_timeout.go +++ b/go_backend/extension_timeout.go @@ -19,10 +19,6 @@ func (e *JSExecutionError) Error() string { return e.Message } -func RunWithTimeout(vm *goja.Runtime, script string, timeout time.Duration) (goja.Value, error) { - return RunWithTimeoutContext(context.Background(), vm, script, timeout) -} - func RunWithTimeoutContext(ctx context.Context, vm *goja.Runtime, script string, timeout time.Duration) (goja.Value, error) { if vm == nil { return nil, fmt.Errorf("extension runtime unavailable") diff --git a/go_backend/httputil_supplement_test.go b/go_backend/httputil_supplement_test.go index d600764a..d5222f8d 100644 --- a/go_backend/httputil_supplement_test.go +++ b/go_backend/httputil_supplement_test.go @@ -158,9 +158,6 @@ func TestRateLimiterHelpers(t *testing.T) { if limiter.Available() != 0 { t.Fatalf("available after acquire = %d", limiter.Available()) } - if GetSongLinkRateLimiter() == nil { - t.Fatal("expected global limiter") - } } func mustNewRequest(t *testing.T, rawURL string) *http.Request { diff --git a/go_backend/log_progress_timeout_supplement_test.go b/go_backend/log_progress_timeout_supplement_test.go index 7b0ac755..ebb2246e 100644 --- a/go_backend/log_progress_timeout_supplement_test.go +++ b/go_backend/log_progress_timeout_supplement_test.go @@ -96,18 +96,8 @@ func TestProgressItemHelpersAndWriter(t *testing.T) { } func TestRunWithTimeoutBranches(t *testing.T) { - if _, err := RunWithTimeout(nil, "1 + 1", time.Millisecond); err == nil { - t.Fatal("expected nil VM error") - } - - vm := goja.New() - value, err := RunWithTimeout(vm, "1 + 2", time.Second) - if err != nil || value.ToInteger() != 3 { - t.Fatalf("RunWithTimeout success = %v/%v", value, err) - } - timeoutVM := goja.New() - _, err = RunWithTimeoutAndRecover(timeoutVM, "for (;;) {}", 10*time.Millisecond) + _, err := RunWithTimeoutAndRecover(timeoutVM, "for (;;) {}", 10*time.Millisecond) if err == nil { t.Fatal("expected timeout error") } diff --git a/go_backend/metadata.go b/go_backend/metadata.go index 0d463c19..69a4355f 100644 --- a/go_backend/metadata.go +++ b/go_backend/metadata.go @@ -218,19 +218,19 @@ func parseFlacFile(filePath string) (*flac.File, error) { return f, nil } -func EmbedMetadata(filePath string, metadata Metadata, coverPath string) error { +// updateFlacVorbis parses a FLAC file, hands the parsed file and its Vorbis +// comment block (created if absent) to mutate, then marshals the block back +// into the file and saves it. Shared scaffold for all FLAC tag writers. +func updateFlacVorbis(filePath string, mutate func(f *flac.File, cmt *flacvorbis.MetaDataBlockVorbisComment) error) error { f, err := parseFlacFile(filePath) if err != nil { return fmt.Errorf("failed to parse FLAC file: %w", err) } defer f.Close() - var cmtIdx int = -1 var cmt *flacvorbis.MetaDataBlockVorbisComment - - for idx, meta := range f.Meta { + for _, meta := range f.Meta { if meta.Type == flac.VorbisComment { - cmtIdx = idx cmt, err = flacvorbis.ParseFromMetaDataBlock(*meta) if err != nil { return fmt.Errorf("failed to parse vorbis comment: %w", err) @@ -238,99 +238,83 @@ func EmbedMetadata(filePath string, metadata Metadata, coverPath string) error { break } } - if cmt == nil { cmt = flacvorbis.New() } - writeVorbisMetadata(cmt, metadata) - - cmtBlock := cmt.Marshal() - if cmtIdx >= 0 { - f.Meta[cmtIdx] = &cmtBlock - } else { - f.Meta = append(f.Meta, &cmtBlock) + if err := mutate(f, cmt); err != nil { + return err } - if coverPath != "" { - if fileExists(coverPath) { - coverData, err := os.ReadFile(coverPath) - if err != nil { - fmt.Printf("[Metadata] Warning: Failed to read cover file %s: %v\n", coverPath, err) - } else { - for i := len(f.Meta) - 1; i >= 0; i-- { - if f.Meta[i].Type == flac.Picture { - f.Meta = append(f.Meta[:i], f.Meta[i+1:]...) - } - } - - picBlock, err := buildPictureBlock(coverPath, coverData) - if err != nil { - fmt.Printf("[Metadata] Warning: skipping cover art: %v\n", err) - } else { - f.Meta = append(f.Meta, &picBlock) - fmt.Printf("[Metadata] Cover art embedded successfully (%d bytes)\n", len(coverData)) - } - } - } else { - fmt.Printf("[Metadata] Warning: Cover file does not exist: %s\n", coverPath) + // Re-scan for the block index: mutate may have removed blocks. + cmtBlock := cmt.Marshal() + replaced := false + for idx, meta := range f.Meta { + if meta.Type == flac.VorbisComment { + f.Meta[idx] = &cmtBlock + replaced = true + break } } + if !replaced { + f.Meta = append(f.Meta, &cmtBlock) + } return saveFlacFile(f, filePath) } -func EmbedMetadataWithCoverData(filePath string, metadata Metadata, coverData []byte) error { - f, err := parseFlacFile(filePath) +// replaceFlacPictures strips all Picture blocks and appends a new front cover +// built from coverData. On error the pictures stay removed and no cover is added. +func replaceFlacPictures(f *flac.File, coverPath string, coverData []byte) error { + for i := len(f.Meta) - 1; i >= 0; i-- { + if f.Meta[i].Type == flac.Picture { + f.Meta = append(f.Meta[:i], f.Meta[i+1:]...) + } + } + + picBlock, err := buildPictureBlock(coverPath, coverData) if err != nil { - return fmt.Errorf("failed to parse FLAC file: %w", err) + return err } - defer f.Close() + f.Meta = append(f.Meta, &picBlock) + return nil +} - var cmtIdx int = -1 - var cmt *flacvorbis.MetaDataBlockVorbisComment +func EmbedMetadata(filePath string, metadata Metadata, coverPath string) error { + return updateFlacVorbis(filePath, func(f *flac.File, cmt *flacvorbis.MetaDataBlockVorbisComment) error { + writeVorbisMetadata(cmt, metadata) - for idx, meta := range f.Meta { - if meta.Type == flac.VorbisComment { - cmtIdx = idx - cmt, err = flacvorbis.ParseFromMetaDataBlock(*meta) - if err != nil { - return fmt.Errorf("failed to parse vorbis comment: %w", err) - } - break - } - } - - if cmt == nil { - cmt = flacvorbis.New() - } - - writeVorbisMetadata(cmt, metadata) - - cmtBlock := cmt.Marshal() - if cmtIdx >= 0 { - f.Meta[cmtIdx] = &cmtBlock - } else { - f.Meta = append(f.Meta, &cmtBlock) - } - - if len(coverData) > 0 { - for i := len(f.Meta) - 1; i >= 0; i-- { - if f.Meta[i].Type == flac.Picture { - f.Meta = append(f.Meta[:i], f.Meta[i+1:]...) + if coverPath != "" { + if fileExists(coverPath) { + coverData, err := os.ReadFile(coverPath) + if err != nil { + fmt.Printf("[Metadata] Warning: Failed to read cover file %s: %v\n", coverPath, err) + } else if err := replaceFlacPictures(f, coverPath, coverData); err != nil { + fmt.Printf("[Metadata] Warning: skipping cover art: %v\n", err) + } else { + fmt.Printf("[Metadata] Cover art embedded successfully (%d bytes)\n", len(coverData)) + } + } else { + fmt.Printf("[Metadata] Warning: Cover file does not exist: %s\n", coverPath) } } + return nil + }) +} - picBlock, err := buildPictureBlock("", coverData) - if err != nil { - fmt.Printf("[Metadata] Warning: skipping cover art: %v\n", err) - } else { - f.Meta = append(f.Meta, &picBlock) - fmt.Printf("[Metadata] Cover art embedded successfully (%d bytes)\n", len(coverData)) +func EmbedMetadataWithCoverData(filePath string, metadata Metadata, coverData []byte) error { + return updateFlacVorbis(filePath, func(f *flac.File, cmt *flacvorbis.MetaDataBlockVorbisComment) error { + writeVorbisMetadata(cmt, metadata) + + if len(coverData) > 0 { + if err := replaceFlacPictures(f, "", coverData); err != nil { + fmt.Printf("[Metadata] Warning: skipping cover art: %v\n", err) + } else { + fmt.Printf("[Metadata] Cover art embedded successfully (%d bytes)\n", len(coverData)) + } } - } - - return saveFlacFile(f, filePath) + return nil + }) } func ReadMetadata(filePath string) (*Metadata, error) { @@ -424,55 +408,17 @@ func ReadMetadata(filePath string) (*Metadata, error) { // absent from the map are left untouched. This is the correct function for // partial edits (e.g. writing only ReplayGain tags) and full editor saves alike. func EditFlacFields(filePath string, fields map[string]string) error { - f, err := parseFlacFile(filePath) - if err != nil { - return fmt.Errorf("failed to parse FLAC file: %w", err) - } - defer f.Close() + return updateFlacVorbis(filePath, func(f *flac.File, cmt *flacvorbis.MetaDataBlockVorbisComment) error { + applyVorbisFieldEdits(cmt, fields) - var cmtIdx int = -1 - var cmt *flacvorbis.MetaDataBlockVorbisComment - - for idx, meta := range f.Meta { - if meta.Type == flac.VorbisComment { - cmtIdx = idx - cmt, err = flacvorbis.ParseFromMetaDataBlock(*meta) - if err != nil { - return fmt.Errorf("failed to parse vorbis comment: %w", err) - } - break - } - } - if cmt == nil { - cmt = flacvorbis.New() - } - - applyVorbisFieldEdits(cmt, fields) - - cmtBlock := cmt.Marshal() - if cmtIdx >= 0 { - f.Meta[cmtIdx] = &cmtBlock - } else { - f.Meta = append(f.Meta, &cmtBlock) - } - - coverPath := strings.TrimSpace(fields["cover_path"]) - if coverPath != "" && fileExists(coverPath) { - coverData, err := os.ReadFile(coverPath) - if err == nil && len(coverData) > 0 { - for i := len(f.Meta) - 1; i >= 0; i-- { - if f.Meta[i].Type == flac.Picture { - f.Meta = append(f.Meta[:i], f.Meta[i+1:]...) - } - } - picBlock, err := buildPictureBlock("", coverData) - if err == nil { - f.Meta = append(f.Meta, &picBlock) + coverPath := strings.TrimSpace(fields["cover_path"]) + if coverPath != "" && fileExists(coverPath) { + if coverData, err := os.ReadFile(coverPath); err == nil && len(coverData) > 0 { + _ = replaceFlacPictures(f, "", coverData) } } - } - - return saveFlacFile(f, filePath) + return nil + }) } // applyVorbisFieldEdits applies the editor's set-or-clear field semantics to a @@ -711,45 +657,11 @@ func setOrClearArtistComments(cmt *flacvorbis.MetaDataBlockVorbisComment, key, v // the last value survives when multiple -metadata ARTIST=X flags are used. // The native go-flac writer correctly handles multiple Vorbis comments. func RewriteSplitArtistTags(filePath, artist, albumArtist string) error { - if !shouldSplitVorbisArtistTags(artistTagModeSplitVorbis) { + return updateFlacVorbis(filePath, func(_ *flac.File, cmt *flacvorbis.MetaDataBlockVorbisComment) error { + setArtistComments(cmt, "ARTIST", artist, artistTagModeSplitVorbis) + setArtistComments(cmt, "ALBUMARTIST", albumArtist, artistTagModeSplitVorbis) return nil - } - - f, err := parseFlacFile(filePath) - if err != nil { - return fmt.Errorf("failed to parse FLAC file: %w", err) - } - defer f.Close() - - var cmtIdx int = -1 - var cmt *flacvorbis.MetaDataBlockVorbisComment - - for idx, meta := range f.Meta { - if meta.Type == flac.VorbisComment { - cmtIdx = idx - cmt, err = flacvorbis.ParseFromMetaDataBlock(*meta) - if err != nil { - return fmt.Errorf("failed to parse vorbis comment: %w", err) - } - break - } - } - - if cmt == nil { - cmt = flacvorbis.New() - } - - setArtistComments(cmt, "ARTIST", artist, artistTagModeSplitVorbis) - setArtistComments(cmt, "ALBUMARTIST", albumArtist, artistTagModeSplitVorbis) - - cmtMeta := cmt.Marshal() - if cmtIdx >= 0 { - f.Meta[cmtIdx] = &cmtMeta - } else { - f.Meta = append(f.Meta, &cmtMeta) - } - - return saveFlacFile(f, filePath) + }) } func removeCommentKey(cmt *flacvorbis.MetaDataBlockVorbisComment, key string) { @@ -884,87 +796,11 @@ func ExtractCoverArt(filePath string) ([]byte, error) { } func EmbedLyrics(filePath string, lyrics string) error { - f, err := parseFlacFile(filePath) - if err != nil { - return fmt.Errorf("failed to parse FLAC file: %w", err) - } - defer f.Close() - - var cmtIdx int = -1 - var cmt *flacvorbis.MetaDataBlockVorbisComment - - for idx, meta := range f.Meta { - if meta.Type == flac.VorbisComment { - cmtIdx = idx - cmt, err = flacvorbis.ParseFromMetaDataBlock(*meta) - if err != nil { - return fmt.Errorf("failed to parse vorbis comment: %w", err) - } - break - } - } - - if cmt == nil { - cmt = flacvorbis.New() - } - - setComment(cmt, "LYRICS", lyrics) - setComment(cmt, "UNSYNCEDLYRICS", lyrics) - - cmtBlock := cmt.Marshal() - if cmtIdx >= 0 { - f.Meta[cmtIdx] = &cmtBlock - } else { - f.Meta = append(f.Meta, &cmtBlock) - } - - return saveFlacFile(f, filePath) -} - -func EmbedGenreLabel(filePath string, genre, label string) error { - if genre == "" && label == "" { + return updateFlacVorbis(filePath, func(_ *flac.File, cmt *flacvorbis.MetaDataBlockVorbisComment) error { + setComment(cmt, "LYRICS", lyrics) + setComment(cmt, "UNSYNCEDLYRICS", lyrics) return nil - } - - f, err := parseFlacFile(filePath) - if err != nil { - return fmt.Errorf("failed to parse FLAC file: %w", err) - } - defer f.Close() - - var cmtIdx int = -1 - var cmt *flacvorbis.MetaDataBlockVorbisComment - - for idx, meta := range f.Meta { - if meta.Type == flac.VorbisComment { - cmtIdx = idx - cmt, err = flacvorbis.ParseFromMetaDataBlock(*meta) - if err != nil { - return fmt.Errorf("failed to parse vorbis comment: %w", err) - } - break - } - } - - if cmt == nil { - cmt = flacvorbis.New() - } - - if genre != "" { - setComment(cmt, "GENRE", genre) - } - if label != "" { - setComment(cmt, "ORGANIZATION", label) - } - - cmtBlock := cmt.Marshal() - if cmtIdx >= 0 { - f.Meta[cmtIdx] = &cmtBlock - } else { - f.Meta = append(f.Meta, &cmtBlock) - } - - return saveFlacFile(f, filePath) + }) } func ExtractLyrics(filePath string) (string, error) { diff --git a/go_backend/misc_coverage_supplement_test.go b/go_backend/misc_coverage_supplement_test.go index 75e53cb5..b187f163 100644 --- a/go_backend/misc_coverage_supplement_test.go +++ b/go_backend/misc_coverage_supplement_test.go @@ -89,7 +89,6 @@ func TestMoreSmallConstructorsRuntimeAndMetadataHelpers(t *testing.T) { if NewIDHSClient().client == nil { t.Fatal("expected IDHS HTTP client") } - ClearTrackCache() vm := goja.New() runtime := &extensionRuntime{extensionID: "misc-runtime", vm: vm, settings: map[string]any{}} @@ -159,15 +158,6 @@ func TestMoreSmallConstructorsRuntimeAndMetadataHelpers(t *testing.T) { if string(mustReadFile(t, coverPath)) != "cover" { t.Fatal("downloaded cover mismatch") } - - parallel := FetchCoverAndLyricsParallel(server.URL+"/cover.jpg", false, "spotify-1", "Song Instrumental", "Artist", true, 180000) - if string(parallel.CoverData) != "cover" || parallel.CoverErr != nil || parallel.LyricsErr == nil { - t.Fatalf("FetchCoverAndLyricsParallel = %#v", parallel) - } - emptyParallel := FetchCoverAndLyricsParallel("", false, "", "", "", false, 0) - if emptyParallel.CoverData != nil || emptyParallel.LyricsData != nil { - t.Fatalf("empty FetchCoverAndLyricsParallel = %#v", emptyParallel) - } } func TestExtensionHealthInitializeVMAndCustomSearchWrappers(t *testing.T) { diff --git a/go_backend/parallel.go b/go_backend/parallel.go deleted file mode 100644 index 611b971d..00000000 --- a/go_backend/parallel.go +++ /dev/null @@ -1,114 +0,0 @@ -package gobackend - -import ( - "encoding/json" - "fmt" - "sync" -) - -type ParallelDownloadResult struct { - CoverData []byte - LyricsData *LyricsResponse - LyricsLRC string - CoverErr error - LyricsErr error -} - -func FetchCoverAndLyricsParallel( - coverURL string, - maxQualityCover bool, - spotifyID string, - trackName string, - artistName string, - embedLyrics bool, - durationMs int64, -) *ParallelDownloadResult { - result := &ParallelDownloadResult{} - var wg sync.WaitGroup - var resultMu sync.Mutex - - if coverURL != "" { - wg.Add(1) - go func() { - defer wg.Done() - data, err := downloadCoverToMemory(coverURL, maxQualityCover) - resultMu.Lock() - if err != nil { - result.CoverErr = err - } else { - result.CoverData = data - } - resultMu.Unlock() - }() - } - - if embedLyrics { - wg.Add(1) - go func() { - defer wg.Done() - client := NewLyricsClient() - durationSec := float64(durationMs) / 1000.0 - lyrics, err := client.FetchLyricsAllSources(spotifyID, trackName, artistName, durationSec) - resultMu.Lock() - if err != nil { - result.LyricsErr = err - } else if lyrics != nil && len(lyrics.Lines) > 0 { - result.LyricsData = lyrics - result.LyricsLRC = convertToLRCWithMetadata(lyrics, trackName, artistName) - } else { - result.LyricsErr = fmt.Errorf("no lyrics found") - } - resultMu.Unlock() - }() - } - - wg.Wait() - return result -} - -type PreWarmCacheRequest struct { - ISRC string - TrackName string - ArtistName string - SpotifyID string - Service string -} - -func PreWarmTrackCache(requests []PreWarmCacheRequest) { - _ = requests -} - -func PreWarmCache(tracksJSON string) error { - var tracks []struct { - ISRC string `json:"isrc"` - TrackName string `json:"track_name"` - ArtistName string `json:"artist_name"` - SpotifyID string `json:"spotify_id"` - Service string `json:"service"` - } - - if err := json.Unmarshal([]byte(tracksJSON), &tracks); err != nil { - return fmt.Errorf("failed to parse tracks JSON: %w", err) - } - - requests := make([]PreWarmCacheRequest, len(tracks)) - for i, t := range tracks { - requests[i] = PreWarmCacheRequest{ - ISRC: t.ISRC, - TrackName: t.TrackName, - ArtistName: t.ArtistName, - SpotifyID: t.SpotifyID, - Service: t.Service, - } - } - - go PreWarmTrackCache(requests) - return nil -} - -func ClearTrackCache() { -} - -func GetCacheSize() int { - return 0 -} diff --git a/go_backend/progress.go b/go_backend/progress.go index 4abcd5b4..b697828c 100644 --- a/go_backend/progress.go +++ b/go_backend/progress.go @@ -193,17 +193,6 @@ func GetMultiProgressDelta(sinceSeq int64) string { return string(jsonBytes) } -func GetItemProgress(itemID string) string { - multiMu.RLock() - defer multiMu.RUnlock() - - if item, ok := multiProgress.Items[itemID]; ok { - jsonBytes, _ := json.Marshal(item) - return string(jsonBytes) - } - return "{}" -} - func StartItemProgress(itemID string) { multiMu.Lock() defer multiMu.Unlock() @@ -359,6 +348,17 @@ func RemoveItemProgress(itemID string) { markMultiProgressDirtyLocked() } +func GetItemProgress(itemID string) string { + multiMu.RLock() + defer multiMu.RUnlock() + + if item, ok := multiProgress.Items[itemID]; ok { + jsonBytes, _ := json.Marshal(item) + return string(jsonBytes) + } + return "{}" +} + func ClearAllItemProgress() { multiMu.Lock() defer multiMu.Unlock() diff --git a/go_backend/ratelimit.go b/go_backend/ratelimit.go index 662d86e4..91078c86 100644 --- a/go_backend/ratelimit.go +++ b/go_backend/ratelimit.go @@ -90,7 +90,3 @@ func (r *RateLimiter) Available() int { // Global SongLink rate limiter - 9 requests per minute (to be safe, limit is 10) var songLinkRateLimiter = NewRateLimiter(9, time.Minute) - -func GetSongLinkRateLimiter() *RateLimiter { - return songLinkRateLimiter -} diff --git a/go_backend/romaji.go b/go_backend/romaji.go deleted file mode 100644 index fce9524a..00000000 --- a/go_backend/romaji.go +++ /dev/null @@ -1,193 +0,0 @@ -package gobackend - -import ( - "strings" - "unicode" -) - -var hiraganaToRomaji = map[rune]string{ - 'あ': "a", 'い': "i", 'う': "u", 'え': "e", 'お': "o", - 'か': "ka", 'き': "ki", 'く': "ku", 'け': "ke", 'こ': "ko", - 'さ': "sa", 'し': "shi", 'す': "su", 'せ': "se", 'そ': "so", - 'た': "ta", 'ち': "chi", 'つ': "tsu", 'て': "te", 'と': "to", - 'な': "na", 'に': "ni", 'ぬ': "nu", 'ね': "ne", 'の': "no", - 'は': "ha", 'ひ': "hi", 'ふ': "fu", 'へ': "he", 'ほ': "ho", - 'ま': "ma", 'み': "mi", 'む': "mu", 'め': "me", 'も': "mo", - 'や': "ya", 'ゆ': "yu", 'よ': "yo", - 'ら': "ra", 'り': "ri", 'る': "ru", 'れ': "re", 'ろ': "ro", - 'わ': "wa", 'を': "wo", 'ん': "n", - 'が': "ga", 'ぎ': "gi", 'ぐ': "gu", 'げ': "ge", 'ご': "go", - 'ざ': "za", 'じ': "ji", 'ず': "zu", 'ぜ': "ze", 'ぞ': "zo", - 'だ': "da", 'ぢ': "ji", 'づ': "zu", 'で': "de", 'ど': "do", - 'ば': "ba", 'び': "bi", 'ぶ': "bu", 'べ': "be", 'ぼ': "bo", - 'ぱ': "pa", 'ぴ': "pi", 'ぷ': "pu", 'ぺ': "pe", 'ぽ': "po", - 'ゃ': "ya", 'ゅ': "yu", 'ょ': "yo", - 'っ': "", - 'ぁ': "a", 'ぃ': "i", 'ぅ': "u", 'ぇ': "e", 'ぉ': "o", -} - -var katakanaToRomaji = map[rune]string{ - 'ア': "a", 'イ': "i", 'ウ': "u", 'エ': "e", 'オ': "o", - 'カ': "ka", 'キ': "ki", 'ク': "ku", 'ケ': "ke", 'コ': "ko", - 'サ': "sa", 'シ': "shi", 'ス': "su", 'セ': "se", 'ソ': "so", - 'タ': "ta", 'チ': "chi", 'ツ': "tsu", 'テ': "te", 'ト': "to", - 'ナ': "na", 'ニ': "ni", 'ヌ': "nu", 'ネ': "ne", 'ノ': "no", - 'ハ': "ha", 'ヒ': "hi", 'フ': "fu", 'ヘ': "he", 'ホ': "ho", - 'マ': "ma", 'ミ': "mi", 'ム': "mu", 'メ': "me", 'モ': "mo", - 'ヤ': "ya", 'ユ': "yu", 'ヨ': "yo", - 'ラ': "ra", 'リ': "ri", 'ル': "ru", 'レ': "re", 'ロ': "ro", - 'ワ': "wa", 'ヲ': "wo", 'ン': "n", - 'ガ': "ga", 'ギ': "gi", 'グ': "gu", 'ゲ': "ge", 'ゴ': "go", - 'ザ': "za", 'ジ': "ji", 'ズ': "zu", 'ゼ': "ze", 'ゾ': "zo", - 'ダ': "da", 'ヂ': "ji", 'ヅ': "zu", 'デ': "de", 'ド': "do", - 'バ': "ba", 'ビ': "bi", 'ブ': "bu", 'ベ': "be", 'ボ': "bo", - 'パ': "pa", 'ピ': "pi", 'プ': "pu", 'ペ': "pe", 'ポ': "po", - 'ャ': "ya", 'ュ': "yu", 'ョ': "yo", - 'ッ': "", - 'ァ': "a", 'ィ': "i", 'ゥ': "u", 'ェ': "e", 'ォ': "o", - 'ー': "", - 'ヴ': "vu", -} - -var combinationHiragana = map[string]string{ - "きゃ": "kya", "きゅ": "kyu", "きょ": "kyo", - "しゃ": "sha", "しゅ": "shu", "しょ": "sho", - "ちゃ": "cha", "ちゅ": "chu", "ちょ": "cho", - "にゃ": "nya", "にゅ": "nyu", "にょ": "nyo", - "ひゃ": "hya", "ひゅ": "hyu", "ひょ": "hyo", - "みゃ": "mya", "みゅ": "myu", "みょ": "myo", - "りゃ": "rya", "りゅ": "ryu", "りょ": "ryo", - "ぎゃ": "gya", "ぎゅ": "gyu", "ぎょ": "gyo", - "じゃ": "ja", "じゅ": "ju", "じょ": "jo", - "びゃ": "bya", "びゅ": "byu", "びょ": "byo", - "ぴゃ": "pya", "ぴゅ": "pyu", "ぴょ": "pyo", -} - -var combinationKatakana = map[string]string{ - "キャ": "kya", "キュ": "kyu", "キョ": "kyo", - "シャ": "sha", "シュ": "shu", "ショ": "sho", - "チャ": "cha", "チュ": "chu", "チョ": "cho", - "ニャ": "nya", "ニュ": "nyu", "ニョ": "nyo", - "ヒャ": "hya", "ヒュ": "hyu", "ヒョ": "hyo", - "ミャ": "mya", "ミュ": "myu", "ミョ": "myo", - "リャ": "rya", "リュ": "ryu", "リョ": "ryo", - "ギャ": "gya", "ギュ": "gyu", "ギョ": "gyo", - "ジャ": "ja", "ジュ": "ju", "ジョ": "jo", - "ビャ": "bya", "ビュ": "byu", "ビョ": "byo", - "ピャ": "pya", "ピュ": "pyu", "ピョ": "pyo", - "ティ": "ti", "ディ": "di", "トゥ": "tu", "ドゥ": "du", - "ファ": "fa", "フィ": "fi", "フェ": "fe", "フォ": "fo", - "ウィ": "wi", "ウェ": "we", "ウォ": "wo", -} - -func ContainsJapanese(s string) bool { - for _, r := range s { - if isHiragana(r) || isKatakana(r) || isKanji(r) { - return true - } - } - return false -} - -func isHiragana(r rune) bool { - return r >= 0x3040 && r <= 0x309F -} - -func isKatakana(r rune) bool { - return r >= 0x30A0 && r <= 0x30FF -} - -func isKanji(r rune) bool { - return (r >= 0x4E00 && r <= 0x9FFF) || // CJK Unified Ideographs - (r >= 0x3400 && r <= 0x4DBF) // CJK Unified Ideographs Extension A -} - -func JapaneseToRomaji(text string) string { - if !ContainsJapanese(text) { - return text - } - - var result strings.Builder - runes := []rune(text) - i := 0 - - for i < len(runes) { - if i < len(runes)-1 && (runes[i] == 'っ' || runes[i] == 'ッ') { - nextRomaji := "" - if romaji, ok := hiraganaToRomaji[runes[i+1]]; ok { - nextRomaji = romaji - } else if romaji, ok := katakanaToRomaji[runes[i+1]]; ok { - nextRomaji = romaji - } - if len(nextRomaji) > 0 { - result.WriteByte(nextRomaji[0]) - } - i++ - continue - } - - if i < len(runes)-1 { - combo := string(runes[i : i+2]) - if romaji, ok := combinationHiragana[combo]; ok { - result.WriteString(romaji) - i += 2 - continue - } - if romaji, ok := combinationKatakana[combo]; ok { - result.WriteString(romaji) - i += 2 - continue - } - } - - r := runes[i] - if romaji, ok := hiraganaToRomaji[r]; ok { - result.WriteString(romaji) - } else if romaji, ok := katakanaToRomaji[r]; ok { - result.WriteString(romaji) - } else if isKanji(r) { - result.WriteRune(r) - } else { - result.WriteRune(r) - } - i++ - } - - return result.String() -} - -func BuildSearchQuery(trackName, artistName string) string { - trackRomaji := JapaneseToRomaji(trackName) - artistRomaji := JapaneseToRomaji(artistName) - - trackClean := cleanSearchQuery(trackRomaji) - artistClean := cleanSearchQuery(artistRomaji) - - return strings.TrimSpace(artistClean + " " + trackClean) -} - -func cleanSearchQuery(s string) string { - var result strings.Builder - for _, r := range s { - if unicode.IsLetter(r) || unicode.IsNumber(r) || unicode.IsSpace(r) { - result.WriteRune(r) - } else if r == '-' || r == '\'' { - result.WriteRune(r) - } - } - return strings.TrimSpace(result.String()) -} - -func CleanToASCII(s string) string { - var result strings.Builder - for _, r := range s { - if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || - (r >= '0' && r <= '9') || r == ' ' || r == '-' || r == '\'' { - result.WriteRune(r) - } else if r == ',' || r == '.' { - result.WriteRune(' ') - } - } - cleaned := strings.Join(strings.Fields(result.String()), " ") - return strings.TrimSpace(cleaned) -} diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index 7526ee34..d56d22b9 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -447,14 +447,6 @@ import Gobackend var error: NSError? switch call.method { - case "checkAvailability": - let args = call.arguments as! [String: Any] - let spotifyId = args["spotify_id"] as! String - let isrc = args["isrc"] as! String - let response = GobackendCheckAvailability(spotifyId, isrc, &error) - if let error = error { throw error } - return response - case "downloadByStrategy": let requestJson = call.arguments as! String let response = GobackendDownloadByStrategy(requestJson, &error) @@ -706,22 +698,6 @@ import Gobackend if let error = error { throw error } return response - case "checkAvailabilityFromDeezerID": - let args = call.arguments as! [String: Any] - let deezerTrackId = args["deezer_track_id"] as! String - let response = GobackendCheckAvailabilityFromDeezerID(deezerTrackId, &error) - if let error = error { throw error } - return response - - case "checkAvailabilityByPlatformID": - let args = call.arguments as! [String: Any] - let platform = args["platform"] as! String - let entityType = args["entity_type"] as! String - let entityId = args["entity_id"] as! String - let response = GobackendCheckAvailabilityByPlatformID(platform, entityType, entityId, &error) - if let error = error { throw error } - return response - case "getSpotifyIDFromDeezerTrack": let args = call.arguments as! [String: Any] let deezerTrackId = args["deezer_track_id"] as! String @@ -736,19 +712,12 @@ import Gobackend if let error = error { throw error } return response - case "preWarmTrackCache": - let args = call.arguments as! [String: Any] - let tracksJson = args["tracks"] as! String - let _ = GobackendPreWarmTrackCacheJSON(tracksJson, &error) - if let error = error { throw error } - return nil - case "getTrackCacheSize": let response = GobackendGetTrackCacheSize() return response case "clearTrackCache": - GobackendClearTrackCache() + GobackendClearTrackIDCache() return nil case "getLogs": diff --git a/lib/providers/track_provider.dart b/lib/providers/track_provider.dart index d93f9a13..16a670d8 100644 --- a/lib/providers/track_provider.dart +++ b/lib/providers/track_provider.dart @@ -674,34 +674,6 @@ class TrackNotifier extends Notifier { } } - Future checkAvailability(int index) async { - if (index < 0 || index >= state.tracks.length) return; - - final track = state.tracks[index]; - if (track.isrc == null || track.isrc!.isEmpty) return; - - try { - final availability = await PlatformBridge.checkAvailability( - track.id, - track.isrc!, - ); - final updatedTrack = track.copyWith( - availability: ServiceAvailability( - tidal: availability['tidal'] as bool? ?? false, - qobuz: availability['qobuz'] as bool? ?? false, - amazon: availability['amazon'] as bool? ?? false, - tidalUrl: availability['tidal_url'] as String?, - qobuzUrl: availability['qobuz_url'] as String?, - amazonUrl: availability['amazon_url'] as String?, - ), - ); - - final tracks = List.from(state.tracks); - tracks[index] = updatedTrack; - state = state.copyWith(tracks: tracks); - } catch (_) {} - } - void clear() { state = const TrackState(); } diff --git a/lib/services/platform_bridge.dart b/lib/services/platform_bridge.dart index 110d72ac..c512311c 100644 --- a/lib/services/platform_bridge.dart +++ b/lib/services/platform_bridge.dart @@ -66,13 +66,10 @@ class PlatformBridge { static const _jsonResultFileKey = '__json_file'; static const _backgroundJsonDecodeThresholdBytes = 128 * 1024; static const _metadataCacheTtl = Duration(minutes: 20); - static const _availabilityCacheTtl = Duration(minutes: 15); static const _urlHandleCacheTtl = Duration(minutes: 5); static const _customSearchCacheTtl = Duration(minutes: 2); static const _bridgeCacheMaxEntries = 256; static const _metadataPersistentCacheKey = 'bridge_metadata_lookup_cache_v1'; - static const _availabilityPersistentCacheKey = - 'bridge_availability_lookup_cache_v1'; static const _downloadProgressEvents = EventChannel( 'com.zarz.spotiflac/download_progress_stream', ); @@ -80,12 +77,9 @@ class PlatformBridge { 'com.zarz.spotiflac/library_scan_progress_stream', ); static final Map _metadataCache = {}; - static final Map _availabilityCache = {}; static final Map _urlHandleCache = {}; static final Map _customSearchCache = {}; static final Map>> _metadataInFlight = {}; - static final Map>> _availabilityInFlight = - {}; static final Map?>> _urlHandleInFlight = {}; static final Map>>> @@ -135,30 +129,6 @@ class PlatformBridge { }); } - static Future> checkAvailability( - String spotifyId, - String isrc, - ) { - Future> load() { - _log.d('checkAvailability: $spotifyId (ISRC: $isrc)'); - return _invokeMap('checkAvailability', { - 'spotify_id': spotifyId, - 'isrc': isrc, - }); - } - - final cacheKey = _availabilityCacheKey(spotifyId, isrc); - if (cacheKey.isEmpty) return load(); - return _cachedInvoke( - cacheKey, - _availabilityCache, - _availabilityInFlight, - _availabilityCacheTtl, - _availabilityPersistentCacheKey, - load, - ); - } - static Future> _invokeMap( String method, [ dynamic args, @@ -198,16 +168,6 @@ class PlatformBridge { } } - static String _availabilityCacheKey(String spotifyId, String isrc) { - final normalizedIsrc = isrc.trim().toUpperCase(); - if (normalizedIsrc.isNotEmpty) { - return 'isrc:$normalizedIsrc'; - } - final normalizedSpotifyId = spotifyId.trim(); - if (normalizedSpotifyId.isEmpty) return ''; - return 'spotify:$normalizedSpotifyId'; - } - static String _providerMetadataCacheKey( String providerId, String resourceType, @@ -322,11 +282,6 @@ class PlatformBridge { _metadataPersistentCacheKey, _metadataCache, ); - _restorePersistentCache( - prefs, - _availabilityPersistentCacheKey, - _availabilityCache, - ); } catch (e) { _log.w('Failed to load bridge lookup cache: $e'); } @@ -390,7 +345,8 @@ class PlatformBridge { try { final prefs = await SharedPreferences.getInstance(); await prefs.remove(_metadataPersistentCacheKey); - await prefs.remove(_availabilityPersistentCacheKey); + // Legacy key from the removed availability-check feature. + await prefs.remove('bridge_availability_lookup_cache_v1'); } catch (e) { _log.w('Failed to clear bridge lookup cache: $e'); } @@ -400,11 +356,9 @@ class PlatformBridge { _lookupCacheGeneration++; _persistentLookupCacheLoadFuture = null; _metadataCache.clear(); - _availabilityCache.clear(); _urlHandleCache.clear(); _customSearchCache.clear(); _metadataInFlight.clear(); - _availabilityInFlight.clear(); _urlHandleInFlight.clear(); for (final inFlight in _customSearchInFlight.values) { _cancelExtensionRequestUnawaited(inFlight.requestId); @@ -462,8 +416,7 @@ class PlatformBridge { static int _lookupCacheSize() { _pruneExpiredBridgeCache(_metadataCache); - _pruneExpiredBridgeCache(_availabilityCache); - return _metadataCache.length + _availabilityCache.length; + return _metadataCache.length; } static Future> _invokeDownloadMethod( @@ -1081,13 +1034,6 @@ class PlatformBridge { return _decodeMapResult(result); } - static Future preWarmTrackCache( - List> tracks, - ) async { - final tracksJson = jsonEncode(tracks); - await _channel.invokeMethod('preWarmTrackCache', {'tracks': tracksJson}); - } - static Future getTrackCacheSize() async { await _ensurePersistentLookupCachesLoaded(); final result = await _channel.invokeMethod('getTrackCacheSize');