package backend import ( "encoding/base64" "encoding/json" "fmt" "io" "net/http" "net/url" "os" "path/filepath" "regexp" "strings" "time" ) type AmazonDownloader struct { client *http.Client regions []string } type SongLinkResponse struct { LinksByPlatform map[string]struct { URL string `json:"url"` } `json:"linksByPlatform"` } type AfkarXYZResponse struct { Success bool `json:"success"` Data struct { DirectLink string `json:"direct_link"` FileName string `json:"file_name"` FileSize int64 `json:"file_size"` } `json:"data"` } func NewAmazonDownloader() *AmazonDownloader { return &AmazonDownloader{ client: &http.Client{ Timeout: 120 * time.Second, }, regions: []string{"us", "eu"}, } } func (a *AmazonDownloader) GetAmazonURLFromSpotify(spotifyTrackID string) (string, error) { spotifyBase := "https://open.spotify.com/track/" spotifyURL := fmt.Sprintf("%s%s", spotifyBase, spotifyTrackID) apiBase := "https://api.song.link/v1-alpha.1/links?url=" apiURL := fmt.Sprintf("%s%s", apiBase, url.QueryEscape(spotifyURL)) req, err := http.NewRequest("GET", apiURL, nil) if err != nil { return "", fmt.Errorf("failed to create request: %w", err) } fmt.Println("Getting Amazon URL...") resp, err := a.client.Do(req) if err != nil { return "", fmt.Errorf("failed to get Amazon URL: %w", err) } defer resp.Body.Close() if resp.StatusCode != 200 { return "", fmt.Errorf("API returned status %d", resp.StatusCode) } body, err := io.ReadAll(resp.Body) if err != nil { return "", fmt.Errorf("failed to read response body: %w", err) } if len(body) == 0 { return "", fmt.Errorf("API returned empty response") } var songLinkResp SongLinkResponse if err := json.Unmarshal(body, &songLinkResp); err != nil { bodyStr := string(body) if len(bodyStr) > 200 { bodyStr = bodyStr[:200] + "..." } return "", fmt.Errorf("failed to decode response: %w (response: %s)", err, bodyStr) } amazonLink, ok := songLinkResp.LinksByPlatform["amazonMusic"] if !ok || amazonLink.URL == "" { return "", fmt.Errorf("amazon Music link not found") } amazonURL := amazonLink.URL if strings.Contains(amazonURL, "trackAsin=") { parts := strings.Split(amazonURL, "trackAsin=") if len(parts) > 1 { trackAsin := strings.Split(parts[1], "&")[0] musicBase, _ := base64.StdEncoding.DecodeString("aHR0cHM6Ly9tdXNpYy5hbWF6b24uY29tL3RyYWNrcy8=") amazonURL = fmt.Sprintf("%s%s?musicTerritory=US", string(musicBase), trackAsin) } } fmt.Printf("Found Amazon URL: %s\n", amazonURL) return amazonURL, nil } func (a *AmazonDownloader) DownloadFromAfkarXYZ(amazonURL, outputDir, quality string) (string, error) { apiURL := "https://amazon.afkarxyz.fun/convert?url=" + url.QueryEscape(amazonURL) req, err := http.NewRequest("GET", apiURL, nil) if err != nil { return "", err } fmt.Printf("Fetching from AfkarXYZ...\n") resp, err := a.client.Do(req) if err != nil { return "", err } defer resp.Body.Close() if resp.StatusCode != 200 { return "", fmt.Errorf("AfkarXYZ API returned status %d", resp.StatusCode) } bodyBytes, _ := io.ReadAll(resp.Body) var apiResp AfkarXYZResponse if err := json.Unmarshal(bodyBytes, &apiResp); err != nil { return "", fmt.Errorf("failed to decode response: %w", err) } if !apiResp.Success || apiResp.Data.DirectLink == "" { return "", fmt.Errorf("AfkarXYZ failed or no link found") } downloadURL := apiResp.Data.DirectLink fileName := apiResp.Data.FileName if fileName == "" { fileName = "track.flac" } reg := regexp.MustCompile(`[<>:"/\\|?*]`) fileName = reg.ReplaceAllString(fileName, "") filePath := filepath.Join(outputDir, fileName) out, err := os.Create(filePath) if err != nil { return "", err } defer out.Close() dlReq, _ := http.NewRequest("GET", downloadURL, nil) dlResp, err := a.client.Do(dlReq) if err != nil { return "", err } defer dlResp.Body.Close() fmt.Printf("Downloading from AfkarXYZ: %s\n", fileName) pw := NewProgressWriter(out) _, err = io.Copy(pw, dlResp.Body) if err != nil { out.Close() os.Remove(filePath) return "", err } fmt.Printf("\rDownloaded: %.2f MB (Complete)\n", float64(pw.GetTotal())/(1024*1024)) return filePath, nil } func (a *AmazonDownloader) DownloadFromService(amazonURL, outputDir, quality string) (string, error) { return a.DownloadFromAfkarXYZ(amazonURL, outputDir, quality) } func (a *AmazonDownloader) DownloadByURL(amazonURL, outputDir, quality, filenameFormat, playlistName, playlistOwner string, includeTrackNumber bool, position int, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate, spotifyCoverURL string, spotifyTrackNumber, spotifyDiscNumber, spotifyTotalTracks int, embedMaxQualityCover bool, spotifyTotalDiscs int, spotifyCopyright, spotifyPublisher, spotifyURL string) (string, error) { if outputDir != "." { if err := os.MkdirAll(outputDir, 0755); err != nil { return "", fmt.Errorf("failed to create output directory: %w", err) } } if spotifyTrackName != "" && spotifyArtistName != "" { expectedFilename := BuildExpectedFilename(spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate, filenameFormat, playlistName, playlistOwner, includeTrackNumber, position, spotifyDiscNumber, false) expectedPath := filepath.Join(outputDir, expectedFilename) if fileInfo, err := os.Stat(expectedPath); err == nil && fileInfo.Size() > 0 { fmt.Printf("File already exists: %s (%.2f MB)\n", expectedPath, float64(fileInfo.Size())/(1024*1024)) return "EXISTS:" + expectedPath, nil } } fmt.Printf("Using Amazon URL: %s\n", amazonURL) filePath, err := a.DownloadFromService(amazonURL, outputDir, quality) if err != nil { return "", err } if spotifyTrackName != "" && spotifyArtistName != "" { safeArtist := sanitizeFilename(spotifyArtistName) safeTitle := sanitizeFilename(spotifyTrackName) safeAlbum := sanitizeFilename(spotifyAlbumName) safeAlbumArtist := sanitizeFilename(spotifyAlbumArtist) year := "" if len(spotifyReleaseDate) >= 4 { year = spotifyReleaseDate[:4] } var newFilename string if strings.Contains(filenameFormat, "{") { newFilename = filenameFormat newFilename = strings.ReplaceAll(newFilename, "{title}", safeTitle) newFilename = strings.ReplaceAll(newFilename, "{artist}", safeArtist) newFilename = strings.ReplaceAll(newFilename, "{album}", safeAlbum) newFilename = strings.ReplaceAll(newFilename, "{album_artist}", safeAlbumArtist) newFilename = strings.ReplaceAll(newFilename, "{year}", year) if spotifyDiscNumber > 0 { newFilename = strings.ReplaceAll(newFilename, "{disc}", fmt.Sprintf("%d", spotifyDiscNumber)) } else { newFilename = strings.ReplaceAll(newFilename, "{disc}", "") } if position > 0 { newFilename = strings.ReplaceAll(newFilename, "{track}", fmt.Sprintf("%02d", position)) } else { newFilename = regexp.MustCompile(`\{track\}\.\s*`).ReplaceAllString(newFilename, "") newFilename = regexp.MustCompile(`\{track\}\s*-\s*`).ReplaceAllString(newFilename, "") newFilename = regexp.MustCompile(`\{track\}\s*`).ReplaceAllString(newFilename, "") } } else { switch filenameFormat { case "artist-title": newFilename = fmt.Sprintf("%s - %s", safeArtist, safeTitle) case "title": newFilename = safeTitle default: newFilename = fmt.Sprintf("%s - %s", safeTitle, safeArtist) } if includeTrackNumber && position > 0 { newFilename = fmt.Sprintf("%02d. %s", position, newFilename) } } newFilename = newFilename + ".flac" newFilePath := filepath.Join(outputDir, newFilename) if err := os.Rename(filePath, newFilePath); err != nil { fmt.Printf("Warning: Failed to rename file: %v\n", err) } else { filePath = newFilePath fmt.Printf("Renamed to: %s\n", newFilename) } } fmt.Println("Embedding Spotify metadata...") coverPath := "" if spotifyCoverURL != "" { coverPath = filePath + ".cover.jpg" coverClient := NewCoverClient() if err := coverClient.DownloadCoverToPath(spotifyCoverURL, coverPath, embedMaxQualityCover); err != nil { fmt.Printf("Warning: Failed to download Spotify cover: %v\n", err) coverPath = "" } else { defer os.Remove(coverPath) fmt.Println("Spotify cover downloaded") } } trackNumberToEmbed := spotifyTrackNumber if trackNumberToEmbed == 0 { trackNumberToEmbed = 1 } metadata := Metadata{ Title: spotifyTrackName, Artist: spotifyArtistName, Album: spotifyAlbumName, AlbumArtist: spotifyAlbumArtist, Date: spotifyReleaseDate, TrackNumber: trackNumberToEmbed, TotalTracks: spotifyTotalTracks, DiscNumber: spotifyDiscNumber, TotalDiscs: spotifyTotalDiscs, URL: spotifyURL, Copyright: spotifyCopyright, Publisher: spotifyPublisher, Description: "https://github.com/afkarxyz/SpotiFLAC", } if err := EmbedMetadata(filePath, metadata, coverPath); err != nil { fmt.Printf("Warning: Failed to embed metadata: %v\n", err) } else { fmt.Println("Metadata embedded successfully") } fmt.Println("Done") fmt.Println("✓ Downloaded successfully from Amazon Music") return filePath, nil } func (a *AmazonDownloader) DownloadBySpotifyID(spotifyTrackID, outputDir, quality, filenameFormat, playlistName, playlistOwner string, includeTrackNumber bool, position int, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate, spotifyCoverURL string, spotifyTrackNumber, spotifyDiscNumber, spotifyTotalTracks int, embedMaxQualityCover bool, spotifyTotalDiscs int, spotifyCopyright, spotifyPublisher, spotifyURL string) (string, error) { amazonURL, err := a.GetAmazonURLFromSpotify(spotifyTrackID) if err != nil { return "", err } return a.DownloadByURL(amazonURL, outputDir, quality, filenameFormat, playlistName, playlistOwner, includeTrackNumber, position, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate, spotifyCoverURL, spotifyTrackNumber, spotifyDiscNumber, spotifyTotalTracks, embedMaxQualityCover, spotifyTotalDiscs, spotifyCopyright, spotifyPublisher, spotifyURL) }