diff --git a/backend/isrc_finder.go b/backend/isrc_finder.go index eca31d7..cf077ea 100644 --- a/backend/isrc_finder.go +++ b/backend/isrc_finder.go @@ -87,14 +87,7 @@ func (s *SongLinkClient) lookupSpotifyISRC(spotifyTrackID string) (string, error isrc, resolvedTrackID, err := s.lookupSpotifyISRCViaSpotFetchAPI(normalizedTrackID, spotFetchAPIURL) if err == nil && isrc != "" { fmt.Printf("Found ISRC via SpotFetch API: %s\n", isrc) - if err := PutCachedISRC(normalizedTrackID, isrc); err != nil { - fmt.Printf("Warning: failed to write ISRC cache: %v\n", err) - } - if resolvedTrackID != "" && resolvedTrackID != normalizedTrackID { - if err := PutCachedISRC(resolvedTrackID, isrc); err != nil { - fmt.Printf("Warning: failed to write ISRC cache for resolved track ID: %v\n", err) - } - } + cacheResolvedSpotifyTrackISRC(normalizedTrackID, resolvedTrackID, isrc) return isrc, nil } if err != nil { @@ -102,21 +95,46 @@ func (s *SongLinkClient) lookupSpotifyISRC(spotifyTrackID string) (string, error } } - payload, err := fetchSpotifyTrackRawData(s.client, normalizedTrackID) - if err != nil { - return "", err + payload, metadataErr := fetchSpotifyTrackRawData(s.client, normalizedTrackID) + if metadataErr == nil { + isrc, extractErr := extractSpotifyTrackISRC(payload) + if extractErr == nil { + fmt.Printf("Found ISRC via Spotify metadata: %s\n", isrc) + cacheResolvedSpotifyTrackISRC(normalizedTrackID, "", isrc) + return isrc, nil + } + metadataErr = extractErr } - isrc, err := extractSpotifyTrackISRC(payload) - if err != nil { - return "", err + if metadataErr != nil { + fmt.Printf("Warning: Spotify metadata ISRC lookup failed, falling back to Soundplate: %v\n", metadataErr) } - fmt.Printf("Found ISRC via Spotify metadata: %s\n", isrc) - if err := PutCachedISRC(normalizedTrackID, isrc); err != nil { + isrc, resolvedTrackID, soundplateErr := s.lookupSpotifyISRCViaSoundplate(normalizedTrackID) + if soundplateErr == nil && isrc != "" { + fmt.Printf("Found ISRC via Soundplate: %s\n", isrc) + cacheResolvedSpotifyTrackISRC(normalizedTrackID, resolvedTrackID, isrc) + return isrc, nil + } + + if metadataErr != nil && soundplateErr != nil { + return "", fmt.Errorf("spotify metadata lookup failed: %v | soundplate lookup failed: %w", metadataErr, soundplateErr) + } + if soundplateErr != nil { + return "", soundplateErr + } + return "", metadataErr +} + +func cacheResolvedSpotifyTrackISRC(trackID string, resolvedTrackID string, isrc string) { + if err := PutCachedISRC(trackID, isrc); err != nil { fmt.Printf("Warning: failed to write ISRC cache: %v\n", err) } - return isrc, nil + if resolvedTrackID != "" && resolvedTrackID != trackID { + if err := PutCachedISRC(resolvedTrackID, isrc); err != nil { + fmt.Printf("Warning: failed to write ISRC cache for resolved track ID: %v\n", err) + } + } } func (s *SongLinkClient) lookupSpotifyISRCViaSpotFetchAPI(spotifyTrackID string, apiBaseURL string) (string, string, error) { diff --git a/backend/soundplate.go b/backend/soundplate.go new file mode 100644 index 0000000..bce9c94 --- /dev/null +++ b/backend/soundplate.go @@ -0,0 +1,95 @@ +package backend + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strings" +) + +const ( + soundplateSpotifyAPIURL = "https://phpstack-822472-6184058.cloudwaysapps.com/api/spotify.php" + soundplateRefererURL = "https://phpstack-822472-6184058.cloudwaysapps.com/?" + soundplateUserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36" +) + +type soundplateSpotifyResponse struct { + Name string `json:"name"` + Artist string `json:"artist"` + Album string `json:"album"` + AlbumType string `json:"album_type"` + ArtworkURL string `json:"artwork_url"` + ISRC string `json:"isrc"` + Year string `json:"year"` + SpotifyURL string `json:"spotify_url"` +} + +func (s *SongLinkClient) lookupSpotifyISRCViaSoundplate(spotifyTrackID string) (string, string, error) { + normalizedTrackID, err := extractSpotifyTrackID(spotifyTrackID) + if err != nil { + return "", "", err + } + + spotifyTrackURL := fmt.Sprintf("https://open.spotify.com/track/%s", normalizedTrackID) + query := url.Values{} + query.Set("q", spotifyTrackURL) + + req, err := http.NewRequest(http.MethodGet, soundplateSpotifyAPIURL+"?"+query.Encode(), nil) + if err != nil { + return "", "", fmt.Errorf("failed to create Soundplate ISRC request: %w", err) + } + req.Header.Set("User-Agent", soundplateUserAgent) + req.Header.Set("Accept", "*/*") + req.Header.Set("Referer", soundplateRefererURL) + req.Header.Set("Accept-Language", "en-US,en;q=0.9,id;q=0.8") + req.Header.Set("Sec-CH-UA", "\"Chromium\";v=\"146\", \"Not-A.Brand\";v=\"24\", \"Google Chrome\";v=\"146\"") + req.Header.Set("Sec-CH-UA-Mobile", "?0") + req.Header.Set("Sec-CH-UA-Platform", "\"Windows\"") + req.Header.Set("Sec-Fetch-Dest", "empty") + req.Header.Set("Sec-Fetch-Mode", "cors") + req.Header.Set("Sec-Fetch-Site", "same-origin") + req.Header.Set("Priority", "u=1, i") + + resp, err := s.client.Do(req) + if err != nil { + return "", "", fmt.Errorf("Soundplate ISRC request failed: %w", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", "", fmt.Errorf("failed to read Soundplate ISRC response: %w", err) + } + + if resp.StatusCode != http.StatusOK { + bodyPreview := strings.TrimSpace(string(body)) + if len(bodyPreview) > 256 { + bodyPreview = bodyPreview[:256] + } + return "", "", fmt.Errorf("Soundplate ISRC returned status %d (%s)", resp.StatusCode, bodyPreview) + } + + var payload soundplateSpotifyResponse + if err := json.Unmarshal(body, &payload); err != nil { + return "", "", fmt.Errorf("failed to decode Soundplate ISRC response: %w", err) + } + + isrc := firstISRCMatch(payload.ISRC) + if isrc == "" { + isrc = firstISRCMatch(string(body)) + } + if isrc == "" { + return "", "", fmt.Errorf("ISRC missing in Soundplate response") + } + + resolvedTrackID := "" + if payload.SpotifyURL != "" { + if trackID, err := extractSpotifyTrackID(payload.SpotifyURL); err == nil { + resolvedTrackID = trackID + } + } + + return isrc, resolvedTrackID, nil +}