Files
SpotiFLAC/backend/spotify_metadata.go
T
afkarxyz 9260adc2d2 v7.0.1
2026-01-11 08:39:14 +07:00

987 lines
28 KiB
Go

package backend
import (
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"strings"
"time"
)
const (
apiBaseURL = "https://afkarxyz.web.id"
apiKey = "NDAwNDAxNDAzNDA0NTAwNTAyNTAz"
)
var (
errInvalidSpotifyURL = errors.New("invalid or unsupported Spotify URL")
)
type SpotifyMetadataClient struct {
httpClient *http.Client
}
func NewSpotifyMetadataClient() *SpotifyMetadataClient {
return &SpotifyMetadataClient{
httpClient: &http.Client{Timeout: 30 * time.Second},
}
}
type TrackMetadata struct {
SpotifyID string `json:"spotify_id,omitempty"`
Artists string `json:"artists"`
Name string `json:"name"`
AlbumName string `json:"album_name"`
AlbumArtist string `json:"album_artist,omitempty"`
DurationMS int `json:"duration_ms"`
Images string `json:"images"`
ReleaseDate string `json:"release_date"`
TrackNumber int `json:"track_number"`
TotalTracks int `json:"total_tracks,omitempty"`
DiscNumber int `json:"disc_number,omitempty"`
TotalDiscs int `json:"total_discs,omitempty"`
ExternalURL string `json:"external_urls"`
ISRC string `json:"isrc"`
Copyright string `json:"copyright,omitempty"`
Publisher string `json:"publisher,omitempty"`
Plays string `json:"plays,omitempty"`
}
type ArtistSimple struct {
ID string `json:"id"`
Name string `json:"name"`
ExternalURL string `json:"external_urls"`
}
type AlbumTrackMetadata struct {
SpotifyID string `json:"spotify_id,omitempty"`
Artists string `json:"artists"`
Name string `json:"name"`
AlbumName string `json:"album_name"`
AlbumArtist string `json:"album_artist,omitempty"`
DurationMS int `json:"duration_ms"`
Images string `json:"images"`
ReleaseDate string `json:"release_date"`
TrackNumber int `json:"track_number"`
TotalTracks int `json:"total_tracks,omitempty"`
DiscNumber int `json:"disc_number,omitempty"`
TotalDiscs int `json:"total_discs,omitempty"`
ExternalURL string `json:"external_urls"`
ISRC string `json:"isrc"`
AlbumType string `json:"album_type,omitempty"`
AlbumID string `json:"album_id,omitempty"`
AlbumURL string `json:"album_url,omitempty"`
ArtistID string `json:"artist_id,omitempty"`
ArtistURL string `json:"artist_url,omitempty"`
ArtistsData []ArtistSimple `json:"artists_data,omitempty"`
Plays string `json:"plays,omitempty"`
Status string `json:"status,omitempty"`
}
type TrackResponse struct {
Track TrackMetadata `json:"track"`
}
type AlbumInfoMetadata struct {
TotalTracks int `json:"total_tracks"`
Name string `json:"name"`
ReleaseDate string `json:"release_date"`
Artists string `json:"artists"`
Images string `json:"images"`
Batch string `json:"batch,omitempty"`
ArtistID string `json:"artist_id,omitempty"`
ArtistURL string `json:"artist_url,omitempty"`
}
type AlbumResponsePayload struct {
AlbumInfo AlbumInfoMetadata `json:"album_info"`
TrackList []AlbumTrackMetadata `json:"track_list"`
}
type PlaylistInfoMetadata struct {
Tracks struct {
Total int `json:"total"`
} `json:"tracks"`
Followers struct {
Total int `json:"total"`
} `json:"followers"`
Owner struct {
DisplayName string `json:"display_name"`
Name string `json:"name"`
Images string `json:"images"`
} `json:"owner"`
Cover string `json:"cover,omitempty"`
Description string `json:"description,omitempty"`
Batch string `json:"batch,omitempty"`
}
type PlaylistResponsePayload struct {
PlaylistInfo PlaylistInfoMetadata `json:"playlist_info"`
TrackList []AlbumTrackMetadata `json:"track_list"`
}
type ArtistInfoMetadata struct {
Name string `json:"name"`
Followers int `json:"followers"`
Genres []string `json:"genres"`
Images string `json:"images"`
Header string `json:"header,omitempty"`
Gallery []string `json:"gallery,omitempty"`
ExternalURL string `json:"external_urls"`
DiscographyType string `json:"discography_type"`
TotalAlbums int `json:"total_albums"`
Biography string `json:"biography,omitempty"`
Verified bool `json:"verified,omitempty"`
Listeners int `json:"listeners,omitempty"`
Rank int `json:"rank,omitempty"`
Batch string `json:"batch,omitempty"`
}
type DiscographyAlbumMetadata struct {
ID string `json:"id"`
Name string `json:"name"`
AlbumType string `json:"album_type"`
ReleaseDate string `json:"release_date"`
TotalTracks int `json:"total_tracks"`
Artists string `json:"artists"`
Images string `json:"images"`
ExternalURL string `json:"external_urls"`
}
type ArtistDiscographyPayload struct {
ArtistInfo ArtistInfoMetadata `json:"artist_info"`
AlbumList []DiscographyAlbumMetadata `json:"album_list"`
TrackList []AlbumTrackMetadata `json:"track_list"`
}
type ArtistResponsePayload struct {
Artist struct {
Name string `json:"name"`
Followers int `json:"followers"`
Genres []string `json:"genres"`
Images string `json:"images"`
ExternalURL string `json:"external_urls"`
Popularity int `json:"popularity"`
} `json:"artist"`
}
type spotifyURI struct {
Type string
ID string
DiscographyGroup string
}
type apiTrackResponse struct {
ID string `json:"id"`
Name string `json:"name"`
Artists string `json:"artists"`
Duration string `json:"duration"`
Track int `json:"track"`
Disc int `json:"disc"`
Discs int `json:"discs"`
Copyright string `json:"copyright"`
Label string `json:"label"`
Plays string `json:"plays"`
Album struct {
ID string `json:"id"`
Name string `json:"name"`
Released string `json:"released"`
Year int `json:"year"`
Tracks int `json:"tracks"`
Artists string `json:"artists"`
Label string `json:"label"`
} `json:"album"`
Cover struct {
Small string `json:"small"`
Medium string `json:"medium"`
Large string `json:"large"`
} `json:"cover"`
}
type apiAlbumResponse struct {
ID string `json:"id"`
Name string `json:"name"`
Artists string `json:"artists"`
Cover string `json:"cover"`
ReleaseDate string `json:"releaseDate"`
Count int `json:"count"`
Tracks []struct {
ID string `json:"id"`
Name string `json:"name"`
Artists string `json:"artists"`
ArtistIds []string `json:"artistIds"`
Duration string `json:"duration"`
Plays string `json:"plays"`
} `json:"tracks"`
}
type apiPlaylistResponse struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Owner struct {
Name string `json:"name"`
Avatar string `json:"avatar"`
} `json:"owner"`
Cover string `json:"cover"`
Count int `json:"count"`
Followers int `json:"followers"`
Tracks []struct {
ID string `json:"id"`
Cover string `json:"cover"`
Title string `json:"title"`
Artist string `json:"artist"`
ArtistIds []string `json:"artistIds"`
Plays string `json:"plays"`
Status string `json:"status"`
Album string `json:"album"`
AlbumID string `json:"albumId"`
Duration string `json:"duration"`
} `json:"tracks"`
}
type apiArtistResponse struct {
ID string `json:"id"`
Name string `json:"name"`
Profile struct {
Biography string `json:"biography"`
Name string `json:"name"`
Verified bool `json:"verified"`
} `json:"profile"`
Avatar string `json:"avatar"`
Header string `json:"header"`
Stats struct {
Followers int `json:"followers"`
Listeners int `json:"listeners"`
Rank int `json:"rank"`
} `json:"stats"`
Gallery []string `json:"gallery"`
Discography struct {
All []struct {
ID string `json:"id"`
Name string `json:"name"`
Cover string `json:"cover"`
Date string `json:"date"`
Year int `json:"year"`
} `json:"all"`
Total int `json:"total"`
} `json:"discography"`
}
type apiSearchResponse struct {
Results struct {
Tracks []struct {
ID string `json:"id"`
Name string `json:"name"`
Artists string `json:"artists"`
Album string `json:"album"`
Duration string `json:"duration"`
Cover string `json:"cover"`
} `json:"tracks"`
Albums []struct {
ID string `json:"id"`
Name string `json:"name"`
Artists string `json:"artists"`
Cover string `json:"cover"`
Year int `json:"year"`
} `json:"albums"`
Artists []struct {
ID string `json:"id"`
Name string `json:"name"`
Cover string `json:"cover"`
} `json:"artists"`
Playlists []struct {
ID string `json:"id"`
Name string `json:"name"`
Cover string `json:"cover"`
Owner string `json:"owner"`
} `json:"playlists"`
} `json:"results"`
TotalResults struct {
Tracks int `json:"tracks"`
Albums int `json:"albums"`
Artists int `json:"artists"`
Playlists int `json:"playlists"`
} `json:"totalResults"`
}
type SearchResult struct {
ID string `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
Artists string `json:"artists,omitempty"`
AlbumName string `json:"album_name,omitempty"`
Images string `json:"images"`
ReleaseDate string `json:"release_date,omitempty"`
ExternalURL string `json:"external_urls"`
Duration int `json:"duration_ms,omitempty"`
TotalTracks int `json:"total_tracks,omitempty"`
Owner string `json:"owner,omitempty"`
}
type SearchResponse struct {
Tracks []SearchResult `json:"tracks"`
Albums []SearchResult `json:"albums"`
Artists []SearchResult `json:"artists"`
Playlists []SearchResult `json:"playlists"`
}
func GetFilteredSpotifyData(ctx context.Context, spotifyURL string, batch bool, delay time.Duration) (interface{}, error) {
client := NewSpotifyMetadataClient()
return client.GetFilteredData(ctx, spotifyURL, batch, delay)
}
func (c *SpotifyMetadataClient) GetFilteredData(ctx context.Context, spotifyURL string, batch bool, delay time.Duration) (interface{}, error) {
parsed, err := parseSpotifyURI(spotifyURL)
if err != nil {
return nil, err
}
raw, err := c.getRawSpotifyData(ctx, parsed, batch, delay)
if err != nil {
return nil, err
}
return c.processSpotifyData(ctx, raw)
}
func (c *SpotifyMetadataClient) getRawSpotifyData(ctx context.Context, parsed spotifyURI, batch bool, delay time.Duration) (interface{}, error) {
switch parsed.Type {
case "playlist":
return c.fetchPlaylist(ctx, parsed.ID)
case "album":
return c.fetchAlbum(ctx, parsed.ID)
case "track":
return c.fetchTrack(ctx, parsed.ID)
case "artist_discography":
return c.fetchArtistDiscography(ctx, parsed)
case "artist":
discographyParsed := spotifyURI{Type: "artist_discography", ID: parsed.ID, DiscographyGroup: "all"}
return c.fetchArtistDiscography(ctx, discographyParsed)
default:
return nil, fmt.Errorf("unsupported Spotify type: %s", parsed.Type)
}
}
func (c *SpotifyMetadataClient) processSpotifyData(ctx context.Context, raw interface{}) (interface{}, error) {
switch payload := raw.(type) {
case *apiPlaylistResponse:
return c.formatPlaylistData(payload), nil
case *apiAlbumResponse:
return c.formatAlbumData(payload)
case *apiTrackResponse:
return c.formatTrackData(payload), nil
case *apiArtistResponse:
return c.formatArtistDiscographyData(ctx, payload)
default:
return nil, errors.New("unknown raw payload type")
}
}
func (c *SpotifyMetadataClient) fetchTrack(ctx context.Context, trackID string) (*apiTrackResponse, error) {
url := fmt.Sprintf("%s/track/%s", apiBaseURL, trackID)
var data apiTrackResponse
if err := c.getJSON(ctx, url, &data); err != nil {
return nil, err
}
return &data, nil
}
func (c *SpotifyMetadataClient) fetchAlbum(ctx context.Context, albumID string) (*apiAlbumResponse, error) {
url := fmt.Sprintf("%s/album/%s", apiBaseURL, albumID)
var data apiAlbumResponse
if err := c.getJSON(ctx, url, &data); err != nil {
return nil, err
}
return &data, nil
}
func (c *SpotifyMetadataClient) fetchPlaylist(ctx context.Context, playlistID string) (*apiPlaylistResponse, error) {
url := fmt.Sprintf("%s/playlist/%s", apiBaseURL, playlistID)
var data apiPlaylistResponse
if err := c.getJSON(ctx, url, &data); err != nil {
return nil, err
}
return &data, nil
}
func (c *SpotifyMetadataClient) fetchArtistDiscography(ctx context.Context, parsed spotifyURI) (*apiArtistResponse, error) {
url := fmt.Sprintf("%s/artist/%s", apiBaseURL, parsed.ID)
var data apiArtistResponse
if err := c.getJSON(ctx, url, &data); err != nil {
return nil, err
}
return &data, nil
}
func (c *SpotifyMetadataClient) formatTrackData(raw *apiTrackResponse) TrackResponse {
durationMS := parseDuration(raw.Duration)
externalURL := fmt.Sprintf("https://open.spotify.com/track/%s", raw.ID)
coverURL := raw.Cover.Medium
if coverURL == "" {
coverURL = raw.Cover.Large
}
if coverURL == "" {
coverURL = raw.Cover.Small
}
releaseDate := raw.Album.Released
if releaseDate == "" && raw.Album.Year > 0 {
releaseDate = fmt.Sprintf("%d", raw.Album.Year)
}
trackMetadata := TrackMetadata{
SpotifyID: raw.ID,
Artists: raw.Artists,
Name: raw.Name,
AlbumName: raw.Album.Name,
AlbumArtist: raw.Album.Artists,
DurationMS: durationMS,
Images: coverURL,
ReleaseDate: releaseDate,
TrackNumber: raw.Track,
TotalTracks: raw.Album.Tracks,
DiscNumber: raw.Disc,
TotalDiscs: raw.Discs,
ExternalURL: externalURL,
ISRC: raw.ID,
Copyright: raw.Copyright,
Publisher: raw.Album.Label,
Plays: raw.Plays,
}
return TrackResponse{
Track: trackMetadata,
}
}
func (c *SpotifyMetadataClient) formatAlbumData(raw *apiAlbumResponse) (*AlbumResponsePayload, error) {
var artistID, artistURL string
info := AlbumInfoMetadata{
TotalTracks: raw.Count,
Name: raw.Name,
ReleaseDate: raw.ReleaseDate,
Artists: raw.Artists,
Images: raw.Cover,
ArtistID: artistID,
ArtistURL: artistURL,
}
tracks := make([]AlbumTrackMetadata, 0, len(raw.Tracks))
for idx, item := range raw.Tracks {
durationMS := parseDuration(item.Duration)
trackNumber := idx + 1
var artistID, artistURL string
if len(item.ArtistIds) > 0 {
artistID = item.ArtistIds[0]
artistURL = fmt.Sprintf("https://open.spotify.com/artist/%s", artistID)
}
artistsData := make([]ArtistSimple, 0, len(item.ArtistIds))
for _, id := range item.ArtistIds {
artistsData = append(artistsData, ArtistSimple{
ID: id,
Name: "",
ExternalURL: fmt.Sprintf("https://open.spotify.com/artist/%s", id),
})
}
tracks = append(tracks, AlbumTrackMetadata{
SpotifyID: item.ID,
Artists: item.Artists,
Name: item.Name,
AlbumName: raw.Name,
AlbumArtist: raw.Artists,
DurationMS: durationMS,
Images: raw.Cover,
ReleaseDate: raw.ReleaseDate,
TrackNumber: trackNumber,
TotalTracks: raw.Count,
DiscNumber: 1,
TotalDiscs: 0,
ExternalURL: fmt.Sprintf("https://open.spotify.com/track/%s", item.ID),
ISRC: item.ID,
AlbumID: raw.ID,
AlbumURL: fmt.Sprintf("https://open.spotify.com/album/%s", raw.ID),
ArtistID: artistID,
ArtistURL: artistURL,
ArtistsData: artistsData,
Plays: item.Plays,
})
}
return &AlbumResponsePayload{
AlbumInfo: info,
TrackList: tracks,
}, nil
}
func (c *SpotifyMetadataClient) formatPlaylistData(raw *apiPlaylistResponse) PlaylistResponsePayload {
var info PlaylistInfoMetadata
info.Tracks.Total = raw.Count
info.Followers.Total = raw.Followers
info.Owner.DisplayName = raw.Owner.Name
info.Owner.Name = raw.Name
info.Owner.Images = raw.Owner.Avatar
info.Cover = raw.Cover
info.Description = raw.Description
tracks := make([]AlbumTrackMetadata, 0, len(raw.Tracks))
for _, item := range raw.Tracks {
durationMS := parseDuration(item.Duration)
var artistID, artistURL string
if len(item.ArtistIds) > 0 {
artistID = item.ArtistIds[0]
artistURL = fmt.Sprintf("https://open.spotify.com/artist/%s", artistID)
}
artistsData := make([]ArtistSimple, 0, len(item.ArtistIds))
for _, id := range item.ArtistIds {
artistsData = append(artistsData, ArtistSimple{
ID: id,
Name: "",
ExternalURL: fmt.Sprintf("https://open.spotify.com/artist/%s", id),
})
}
tracks = append(tracks, AlbumTrackMetadata{
SpotifyID: item.ID,
Artists: item.Artist,
Name: item.Title,
AlbumName: item.Album,
AlbumArtist: item.Artist,
DurationMS: durationMS,
Images: item.Cover,
ReleaseDate: "",
TrackNumber: 0,
TotalTracks: 0,
DiscNumber: 1,
TotalDiscs: 0,
ExternalURL: fmt.Sprintf("https://open.spotify.com/track/%s", item.ID),
ISRC: item.ID,
AlbumID: item.AlbumID,
AlbumURL: fmt.Sprintf("https://open.spotify.com/album/%s", item.AlbumID),
ArtistID: artistID,
ArtistURL: artistURL,
ArtistsData: artistsData,
Plays: item.Plays,
Status: item.Status,
})
}
return PlaylistResponsePayload{
PlaylistInfo: info,
TrackList: tracks,
}
}
func (c *SpotifyMetadataClient) formatArtistDiscographyData(ctx context.Context, raw *apiArtistResponse) (*ArtistDiscographyPayload, error) {
discType := "all"
info := ArtistInfoMetadata{
Name: raw.Name,
Followers: raw.Stats.Followers,
Genres: []string{},
Images: raw.Avatar,
Header: raw.Header,
Gallery: raw.Gallery,
ExternalURL: fmt.Sprintf("https://open.spotify.com/artist/%s", raw.ID),
DiscographyType: discType,
TotalAlbums: raw.Discography.Total,
Biography: raw.Profile.Biography,
Verified: raw.Profile.Verified,
Listeners: raw.Stats.Listeners,
Rank: raw.Stats.Rank,
}
albumList := make([]DiscographyAlbumMetadata, 0, len(raw.Discography.All))
allTracks := make([]AlbumTrackMetadata, 0)
for _, alb := range raw.Discography.All {
select {
case <-ctx.Done():
return &ArtistDiscographyPayload{
ArtistInfo: info,
AlbumList: albumList,
TrackList: allTracks,
}, ctx.Err()
default:
}
albumList = append(albumList, DiscographyAlbumMetadata{
ID: alb.ID,
Name: alb.Name,
AlbumType: "album",
ReleaseDate: alb.Date,
TotalTracks: 0,
Artists: raw.Name,
Images: alb.Cover,
ExternalURL: fmt.Sprintf("https://open.spotify.com/album/%s", alb.ID),
})
albumData, err := c.fetchAlbum(ctx, alb.ID)
if err != nil {
fmt.Printf("Error getting tracks for album %s: %v\n", alb.Name, err)
continue
}
for idx, tr := range albumData.Tracks {
durationMS := parseDuration(tr.Duration)
trackNumber := idx + 1
var artistID, artistURL string
if len(tr.ArtistIds) > 0 {
artistID = tr.ArtistIds[0]
artistURL = fmt.Sprintf("https://open.spotify.com/artist/%s", artistID)
}
artistsData := make([]ArtistSimple, 0, len(tr.ArtistIds))
for _, id := range tr.ArtistIds {
artistsData = append(artistsData, ArtistSimple{
ID: id,
Name: "",
ExternalURL: fmt.Sprintf("https://open.spotify.com/artist/%s", id),
})
}
allTracks = append(allTracks, AlbumTrackMetadata{
SpotifyID: tr.ID,
Artists: tr.Artists,
Name: tr.Name,
AlbumName: albumData.Name,
AlbumArtist: albumData.Artists,
AlbumType: "album",
DurationMS: durationMS,
Images: albumData.Cover,
ReleaseDate: albumData.ReleaseDate,
TrackNumber: trackNumber,
TotalTracks: albumData.Count,
DiscNumber: 1,
ExternalURL: fmt.Sprintf("https://open.spotify.com/track/%s", tr.ID),
ISRC: tr.ID,
AlbumID: alb.ID,
AlbumURL: fmt.Sprintf("https://open.spotify.com/album/%s", alb.ID),
ArtistID: artistID,
ArtistURL: artistURL,
ArtistsData: artistsData,
Plays: tr.Plays,
})
}
}
return &ArtistDiscographyPayload{
ArtistInfo: info,
AlbumList: albumList,
TrackList: allTracks,
}, nil
}
func (c *SpotifyMetadataClient) getJSON(ctx context.Context, endpoint string, dst interface{}) error {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
if err != nil {
return err
}
decodedKey, err := base64.StdEncoding.DecodeString(apiKey)
if err != nil {
return fmt.Errorf("failed to decode API key: %w", err)
}
req.Header.Set("Accept", "application/json")
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
req.Header.Set("X-API-Key", string(decodedKey))
resp, err := c.httpClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("API returned status %d for %s: %s", resp.StatusCode, endpoint, string(body))
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
return json.Unmarshal(body, dst)
}
func parseDuration(durationStr string) int {
if durationStr == "" {
return 0
}
parts := strings.Split(durationStr, ":")
if len(parts) != 2 {
return 0
}
minutes, err1 := strconv.Atoi(parts[0])
seconds, err2 := strconv.Atoi(parts[1])
if err1 != nil || err2 != nil {
return 0
}
return (minutes*60 + seconds) * 1000
}
func parseSpotifyURI(input string) (spotifyURI, error) {
trimmed := strings.TrimSpace(input)
if trimmed == "" {
return spotifyURI{}, errInvalidSpotifyURL
}
if strings.HasPrefix(trimmed, "spotify:") {
parts := strings.Split(trimmed, ":")
if len(parts) == 3 {
switch parts[1] {
case "album", "track", "playlist", "artist":
return spotifyURI{Type: parts[1], ID: parts[2]}, nil
}
}
}
parsed, err := url.Parse(trimmed)
if err != nil {
return spotifyURI{}, err
}
if parsed.Host != "open.spotify.com" && parsed.Host != "play.spotify.com" {
return spotifyURI{}, errInvalidSpotifyURL
}
parts := cleanPathParts(parsed.Path)
if len(parts) == 0 {
return spotifyURI{}, errInvalidSpotifyURL
}
if parts[0] == "embed" {
parts = parts[1:]
}
if len(parts) == 0 {
return spotifyURI{}, errInvalidSpotifyURL
}
if strings.HasPrefix(parts[0], "intl-") {
parts = parts[1:]
}
if len(parts) == 0 {
return spotifyURI{}, errInvalidSpotifyURL
}
if len(parts) == 2 {
switch parts[0] {
case "album", "track", "playlist", "artist":
return spotifyURI{Type: parts[0], ID: parts[1]}, nil
}
}
if len(parts) >= 3 && parts[0] == "artist" {
if len(parts) >= 3 && parts[2] == "discography" {
discType := "all"
if len(parts) >= 4 {
candidate := parts[3]
if candidate == "all" || candidate == "album" || candidate == "single" || candidate == "compilation" {
discType = candidate
}
}
return spotifyURI{Type: "artist_discography", ID: parts[1], DiscographyGroup: discType}, nil
}
return spotifyURI{Type: "artist", ID: parts[1]}, nil
}
return spotifyURI{}, errInvalidSpotifyURL
}
func cleanPathParts(path string) []string {
raw := strings.Split(path, "/")
parts := make([]string, 0, len(raw))
for _, part := range raw {
if part != "" {
parts = append(parts, part)
}
}
return parts
}
func parseArtistIDsFromString(artists string) []string {
return []string{}
}
func (c *SpotifyMetadataClient) Search(ctx context.Context, query string, limit int) (*SearchResponse, error) {
if query == "" {
return nil, errors.New("search query cannot be empty")
}
if limit <= 0 || limit > 50 {
limit = 50
}
encodedQuery := url.QueryEscape(query)
searchURL := fmt.Sprintf("%s/search?q=%s&limit=%d&offset=0", apiBaseURL, encodedQuery, limit)
var apiResp apiSearchResponse
if err := c.getJSON(ctx, searchURL, &apiResp); err != nil {
return nil, fmt.Errorf("search failed: %w", err)
}
response := &SearchResponse{
Tracks: make([]SearchResult, 0),
Albums: make([]SearchResult, 0),
Artists: make([]SearchResult, 0),
Playlists: make([]SearchResult, 0),
}
for _, item := range apiResp.Results.Tracks {
response.Tracks = append(response.Tracks, SearchResult{
ID: item.ID,
Name: item.Name,
Type: "track",
Artists: item.Artists,
AlbumName: item.Album,
Images: item.Cover,
ExternalURL: fmt.Sprintf("https://open.spotify.com/track/%s", item.ID),
Duration: parseDuration(item.Duration),
})
}
for _, item := range apiResp.Results.Albums {
response.Albums = append(response.Albums, SearchResult{
ID: item.ID,
Name: item.Name,
Type: "album",
Artists: item.Artists,
Images: item.Cover,
ReleaseDate: fmt.Sprintf("%d", item.Year),
ExternalURL: fmt.Sprintf("https://open.spotify.com/album/%s", item.ID),
})
}
for _, item := range apiResp.Results.Artists {
response.Artists = append(response.Artists, SearchResult{
ID: item.ID,
Name: item.Name,
Type: "artist",
Images: item.Cover,
ExternalURL: fmt.Sprintf("https://open.spotify.com/artist/%s", item.ID),
})
}
for _, item := range apiResp.Results.Playlists {
response.Playlists = append(response.Playlists, SearchResult{
ID: item.ID,
Name: item.Name,
Type: "playlist",
Images: item.Cover,
Owner: item.Owner,
ExternalURL: fmt.Sprintf("https://open.spotify.com/playlist/%s", item.ID),
})
}
return response, nil
}
func SearchSpotify(ctx context.Context, query string, limit int) (*SearchResponse, error) {
client := NewSpotifyMetadataClient()
return client.Search(ctx, query, limit)
}
func (c *SpotifyMetadataClient) SearchByType(ctx context.Context, query string, searchType string, limit int, offset int) ([]SearchResult, error) {
if query == "" {
return nil, errors.New("search query cannot be empty")
}
if limit <= 0 || limit > 50 {
limit = 50
}
if offset < 0 {
offset = 0
}
encodedQuery := url.QueryEscape(query)
searchURL := fmt.Sprintf("%s/search?q=%s&limit=%d&offset=%d", apiBaseURL, encodedQuery, limit, offset)
var apiResp apiSearchResponse
if err := c.getJSON(ctx, searchURL, &apiResp); err != nil {
return nil, fmt.Errorf("search failed: %w", err)
}
results := make([]SearchResult, 0)
switch searchType {
case "track":
for _, item := range apiResp.Results.Tracks {
results = append(results, SearchResult{
ID: item.ID,
Name: item.Name,
Type: "track",
Artists: item.Artists,
AlbumName: item.Album,
Images: item.Cover,
ExternalURL: fmt.Sprintf("https://open.spotify.com/track/%s", item.ID),
Duration: parseDuration(item.Duration),
})
}
case "album":
for _, item := range apiResp.Results.Albums {
results = append(results, SearchResult{
ID: item.ID,
Name: item.Name,
Type: "album",
Artists: item.Artists,
Images: item.Cover,
ReleaseDate: fmt.Sprintf("%d", item.Year),
ExternalURL: fmt.Sprintf("https://open.spotify.com/album/%s", item.ID),
})
}
case "artist":
for _, item := range apiResp.Results.Artists {
results = append(results, SearchResult{
ID: item.ID,
Name: item.Name,
Type: "artist",
Images: item.Cover,
ExternalURL: fmt.Sprintf("https://open.spotify.com/artist/%s", item.ID),
})
}
case "playlist":
for _, item := range apiResp.Results.Playlists {
results = append(results, SearchResult{
ID: item.ID,
Name: item.Name,
Type: "playlist",
Images: item.Cover,
Owner: item.Owner,
ExternalURL: fmt.Sprintf("https://open.spotify.com/playlist/%s", item.ID),
})
}
default:
return nil, fmt.Errorf("invalid search type: %s", searchType)
}
return results, nil
}
func SearchSpotifyByType(ctx context.Context, query string, searchType string, limit int, offset int) ([]SearchResult, error) {
client := NewSpotifyMetadataClient()
return client.SearchByType(ctx, query, searchType, limit, offset)
}