diff --git a/lib/download.go b/lib/download.go index 08d6084..168bc9a 100644 --- a/lib/download.go +++ b/lib/download.go @@ -30,7 +30,12 @@ func (app *App) Download(url string, outputFolder string, serviceString string) serviceString = DEFAULT_DOWNLOAD_SERVICE } - _ = GetUrlType(url) + _, err := app.GetMetadata(url) + if err != nil { + return err + } + + _ = ParseUrlType(url) return errors.New("Invalid URL.") } diff --git a/lib/metadata.go b/lib/metadata.go index d91ce0b..751c271 100644 --- a/lib/metadata.go +++ b/lib/metadata.go @@ -1,6 +1,7 @@ package lib import ( + "encoding/json" "errors" ) @@ -25,9 +26,41 @@ type Metadata struct { } func (app *App) GetMetadata(url string) (Metadata, error) { - return Metadata{}, nil + urlType := ParseUrlType(url) + + switch urlType { + case UrlTypeTrack: + app.GetTrackMetadata(url) + } + + return Metadata{}, errors.New("Invalid URL.") +} + +func (app *App) GetTrackMetadata(url string) error { + client := NewSpotifyClient() + + err := client.Initialize() + if err != nil { + return errors.New("Unable to fetch Spotify metadata.") + } + + trackId, err := ParseTrackId(url) + if err != nil { + return err + } + + payload := BuildSpotifyReqPayloadTrack(trackId) + + rawMetadata, err := client.Query(payload) + if err != nil { + return err + } + a, err := json.Marshal(rawMetadata) + println(string(a)) + + return nil } func (app *App) PrintMetadata(url string) error { - return errors.New("Invalid URL.") + return errors.New("Unimplemented.") } diff --git a/lib/spotfetch.go b/lib/spotfetch.go new file mode 100644 index 0000000..f4a5fd2 --- /dev/null +++ b/lib/spotfetch.go @@ -0,0 +1,1837 @@ +package lib + +import ( + "bytes" + "encoding/base32" + "encoding/base64" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "html" + "io" + "net/http" + "regexp" + "strconv" + "strings" + "time" + + "sort" + + "github.com/pquerna/otp" + "github.com/pquerna/otp/totp" +) + +var SpotifyError = errors.New("spotify error") + +type SpotifyClient struct { + client *http.Client + accessToken string + clientToken string + clientID string + deviceID string + clientVersion string + cookies map[string]string +} + +func NewSpotifyClient() *SpotifyClient { + return &SpotifyClient{ + client: &http.Client{Timeout: 30 * time.Second}, + cookies: make(map[string]string), + } +} + +func (c *SpotifyClient) getTOTPSecret() (int, []byte) { + secrets := map[int][]byte{ + 59: {123, 105, 79, 70, 110, 59, 52, 125, 60, 49, 80, 70, 89, 75, 80, 86, 63, 53, 123, 37, 117, 49, 52, 93, 77, 62, 47, 86, 48, 104, 68, 72}, + 60: {79, 109, 69, 123, 90, 65, 46, 74, 94, 34, 58, 48, 70, 71, 92, 85, 122, 63, 91, 64, 87, 87}, + 61: {44, 55, 47, 42, 70, 40, 34, 114, 76, 74, 50, 111, 120, 97, 75, 76, 94, 102, 43, 69, 49, 120, 118, 80, 64, 78}, + } + + version := 61 + secretList := secrets[version] + return version, secretList +} + +func (c *SpotifyClient) generateTOTP() (string, int, error) { + version, secretList := c.getTOTPSecret() + + transformed := make([]byte, len(secretList)) + for i, b := range secretList { + transformed[i] = b ^ byte((i%33)+9) + } + + var joined strings.Builder + for _, b := range transformed { + joined.WriteString(strconv.Itoa(int(b))) + } + + hexStr := hex.EncodeToString([]byte(joined.String())) + hexBytes, err := hex.DecodeString(hexStr) + if err != nil { + return "", 0, err + } + + secret := base32Encode(hexBytes) + secret = strings.TrimRight(secret, "=") + + key, err := otp.NewKeyFromURL(fmt.Sprintf("otpauth://totp/secret?secret=%s", secret)) + if err != nil { + return "", 0, err + } + + totpCode, err := totp.GenerateCode(key.Secret(), time.Now()) + if err != nil { + return "", 0, err + } + + return totpCode, version, nil +} + +func base32Encode(data []byte) string { + b32 := base32.StdEncoding.WithPadding(base32.NoPadding) + return b32.EncodeToString(data) +} + +func (c *SpotifyClient) getAccessToken() error { + totpCode, version, err := c.generateTOTP() + if err != nil { + return err + } + + req, err := http.NewRequest("GET", "https://open.spotify.com/api/token", nil) + if err != nil { + return err + } + + q := req.URL.Query() + q.Add("reason", "init") + q.Add("productType", "web-player") + q.Add("totp", totpCode) + q.Add("totpVer", strconv.Itoa(version)) + q.Add("totpServer", totpCode) + req.URL.RawQuery = q.Encode() + + req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36") + req.Header.Set("Content-Type", "application/json;charset=UTF-8") + + resp, err := c.client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + return fmt.Errorf("%w: access token request failed: HTTP %d", SpotifyError, resp.StatusCode) + } + + var data map[string]interface{} + if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { + return err + } + + c.accessToken = getString(data, "accessToken") + c.clientID = getString(data, "clientId") + + for _, cookie := range resp.Cookies() { + if cookie.Name == "sp_t" { + c.deviceID = cookie.Value + } + c.cookies[cookie.Name] = cookie.Value + } + + return nil +} + +func (c *SpotifyClient) getSessionInfo() error { + req, err := http.NewRequest("GET", "https://open.spotify.com", nil) + if err != nil { + return err + } + + req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36") + + for name, value := range c.cookies { + req.AddCookie(&http.Cookie{Name: name, Value: value}) + } + + resp, err := c.client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + return fmt.Errorf("%w: session initialization failed: HTTP %d", SpotifyError, resp.StatusCode) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + + re := regexp.MustCompile(``) + matches := re.FindStringSubmatch(string(body)) + if len(matches) > 1 { + decoded, err := base64.StdEncoding.DecodeString(matches[1]) + if err == nil { + var cfg map[string]interface{} + if json.Unmarshal(decoded, &cfg) == nil { + c.clientVersion = getString(cfg, "clientVersion") + } + } + } + + for _, cookie := range resp.Cookies() { + if cookie.Name == "sp_t" { + c.deviceID = cookie.Value + } + c.cookies[cookie.Name] = cookie.Value + } + + return nil +} + +func (c *SpotifyClient) getClientToken() error { + if c.clientID == "" || c.deviceID == "" || c.clientVersion == "" { + if err := c.getSessionInfo(); err != nil { + return err + } + if err := c.getAccessToken(); err != nil { + return err + } + } + + payload := map[string]interface{}{ + "client_data": map[string]interface{}{ + "client_version": c.clientVersion, + "client_id": c.clientID, + "js_sdk_data": map[string]interface{}{ + "device_brand": "unknown", + "device_model": "unknown", + "os": "windows", + "os_version": "NT 10.0", + "device_id": c.deviceID, + "device_type": "computer", + }, + }, + } + + jsonData, err := json.Marshal(payload) + if err != nil { + return err + } + + req, err := http.NewRequest("POST", "https://clienttoken.spotify.com/v1/clienttoken", bytes.NewBuffer(jsonData)) + if err != nil { + return err + } + + req.Header.Set("Authority", "clienttoken.spotify.com") + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Accept", "application/json") + req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36") + + resp, err := c.client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + return fmt.Errorf("%w: client token request failed: HTTP %d", SpotifyError, resp.StatusCode) + } + + var data map[string]interface{} + if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { + return err + } + + if getString(data, "response_type") != "RESPONSE_GRANTED_TOKEN_RESPONSE" { + return fmt.Errorf("%w: invalid client token response type", SpotifyError) + } + + grantedToken := getMap(data, "granted_token") + c.clientToken = getString(grantedToken, "token") + + return nil +} + +func (c *SpotifyClient) Initialize() error { + if err := c.getSessionInfo(); err != nil { + return err + } + if err := c.getAccessToken(); err != nil { + return err + } + return c.getClientToken() +} + +func (c *SpotifyClient) Query(payload SpotifyPayload) (map[string]interface{}, error) { + if c.accessToken == "" || c.clientToken == "" { + if err := c.Initialize(); err != nil { + return nil, err + } + } + + jsonData, err := json.Marshal(payload) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", "https://api-partner.spotify.com/pathfinder/v2/query", bytes.NewBuffer(jsonData)) + if err != nil { + return nil, err + } + + req.Header.Set("Authorization", "Bearer "+c.accessToken) + req.Header.Set("Client-Token", c.clientToken) + req.Header.Set("Spotify-App-Version", c.clientVersion) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36") + + resp, err := c.client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + if resp.StatusCode != 200 { + errorText := string(body) + if len(errorText) > 200 { + errorText = errorText[:200] + } + return nil, fmt.Errorf("%w: API query failed: HTTP %d | %s", SpotifyError, resp.StatusCode, errorText) + } + + var result map[string]interface{} + if err := json.Unmarshal(body, &result); err != nil { + return nil, err + } + + return result, nil +} + +func getString(m map[string]interface{}, key string) string { + if val, ok := m[key].(string); ok { + return val + } + return "" +} + +func getMap(m map[string]interface{}, key string) map[string]interface{} { + if val, ok := m[key].(map[string]interface{}); ok { + return val + } + return make(map[string]interface{}) +} + +func getSlice(m map[string]interface{}, key string) []interface{} { + if val, ok := m[key].([]interface{}); ok { + return val + } + return nil +} + +func getFloat64(m map[string]interface{}, key string) float64 { + if val, ok := m[key].(float64); ok { + return val + } + return 0 +} + +func getInt(m map[string]interface{}, key string) int { + if val, ok := m[key].(int); ok { + return val + } + if val, ok := m[key].(float64); ok { + return int(val) + } + return 0 +} + +func getBool(m map[string]interface{}, key string) bool { + if val, ok := m[key].(bool); ok { + return val + } + return false +} + +func extractArtists(artistsData map[string]interface{}) []map[string]interface{} { + items := getSlice(artistsData, "items") + + artists := []map[string]interface{}{} + for _, item := range items { + itemMap, ok := item.(map[string]interface{}) + if !ok { + continue + } + profile := getMap(itemMap, "profile") + artistInfo := map[string]interface{}{ + "name": getString(profile, "name"), + } + artists = append(artists, artistInfo) + } + return artists +} + +func extractCoverImage(coverData map[string]interface{}) map[string]interface{} { + if len(coverData) == 0 { + return nil + } + + var sources []interface{} + if srcs, ok := coverData["sources"].([]interface{}); ok { + sources = srcs + } else if squareImg, ok := coverData["squareCoverImage"].(map[string]interface{}); ok { + if img, ok := squareImg["image"].(map[string]interface{}); ok { + if data, ok := img["data"].(map[string]interface{}); ok { + if srcs, ok := data["sources"].([]interface{}); ok { + sources = srcs + } + } + } + } + + if len(sources) == 0 { + return nil + } + + type sourceInfo struct { + url string + width float64 + height float64 + } + + filteredSources := []sourceInfo{} + for _, s := range sources { + sMap, ok := s.(map[string]interface{}) + if !ok { + continue + } + url := getString(sMap, "url") + if url == "" { + continue + } + + width := getFloat64(sMap, "width") + if width == 0 { + width = getFloat64(sMap, "maxWidth") + } + height := getFloat64(sMap, "height") + if height == 0 { + height = getFloat64(sMap, "maxHeight") + } + + if (width > 64 && height > 64) || (width == 0 && height == 0 && url != "") { + filteredSources = append(filteredSources, sourceInfo{url: url, width: width, height: height}) + } + } + + if len(filteredSources) == 0 { + return nil + } + + sort.Slice(filteredSources, func(i, j int) bool { + return filteredSources[i].width < filteredSources[j].width + }) + + var smallURL, mediumURL, imageID, fallbackURL string + + for _, source := range filteredSources { + if source.width == 300 { + smallURL = source.url + } else if source.width == 640 { + mediumURL = source.url + } else if source.width == 0 { + fallbackURL = source.url + } + + if imageID == "" && source.url != "" { + if strings.Contains(source.url, "ab67616d0000b273") { + parts := strings.Split(source.url, "ab67616d0000b273") + if len(parts) > 1 { + imageID = parts[len(parts)-1] + } + } else if strings.Contains(source.url, "ab67616d00001e02") { + parts := strings.Split(source.url, "ab67616d00001e02") + if len(parts) > 1 { + imageID = parts[len(parts)-1] + } + } else if strings.Contains(source.url, "/image/") { + parts := strings.Split(source.url, "/image/") + if len(parts) > 1 { + imagePart := strings.Split(parts[len(parts)-1], "?")[0] + if len(imagePart) > 20 { + prefixes := []string{"ab67616d0000b273", "ab67616d00001e02", "ab67616d00004851"} + for _, prefix := range prefixes { + if strings.Contains(imagePart, prefix) { + subParts := strings.Split(imagePart, prefix) + if len(subParts) > 1 { + imageID = subParts[len(subParts)-1] + break + } + } + } + } + } + } + } + } + + largeURL := "" + if imageID != "" { + largeURL = "https://i.scdn.co/image/ab67616d000082c1" + imageID + } + + result := map[string]interface{}{} + if smallURL != "" { + result["small"] = smallURL + } + if mediumURL != "" { + result["medium"] = mediumURL + } + if largeURL != "" { + result["large"] = largeURL + } + + if len(result) == 0 && fallbackURL != "" { + result["small"] = fallbackURL + result["medium"] = fallbackURL + result["large"] = fallbackURL + } + + if len(result) == 0 { + return nil + } + return result +} + +func extractDuration(ms float64) map[string]interface{} { + totalSeconds := int(ms) / 1000 + minutes := totalSeconds / 60 + seconds := totalSeconds % 60 + return map[string]interface{}{ + "formatted": fmt.Sprintf("%d:%02d", minutes, seconds), + } +} + +func FilterTrack(data map[string]interface{}, albumFetchData ...map[string]interface{}) map[string]interface{} { + dataMap := getMap(data, "data") + trackData := getMap(dataMap, "trackUnion") + if len(trackData) == 0 { + return make(map[string]interface{}) + } + + var albumFetchDataMap map[string]interface{} + if len(albumFetchData) > 0 { + albumFetchDataMap = albumFetchData[0] + } + + artists := extractArtists(getMap(trackData, "artists")) + + if len(artists) == 0 { + artists = []map[string]interface{}{} + firstArtistItems := getSlice(getMap(trackData, "firstArtist"), "items") + for _, item := range firstArtistItems { + itemMap, ok := item.(map[string]interface{}) + if !ok { + continue + } + if profile, exists := itemMap["profile"]; exists { + profileMap, ok := profile.(map[string]interface{}) + if ok { + artistInfo := map[string]interface{}{ + "name": getString(profileMap, "name"), + } + artists = append(artists, artistInfo) + } + } + } + + otherArtistItems := getSlice(getMap(trackData, "otherArtists"), "items") + for _, item := range otherArtistItems { + itemMap, ok := item.(map[string]interface{}) + if !ok { + continue + } + if profile, exists := itemMap["profile"]; exists { + profileMap, ok := profile.(map[string]interface{}) + if ok { + artistInfo := map[string]interface{}{ + "name": getString(profileMap, "name"), + } + artists = append(artists, artistInfo) + } + } + } + } + + if len(artists) == 0 { + albumData := getMap(trackData, "albumOfTrack") + if len(albumData) > 0 { + artists = extractArtists(getMap(albumData, "artists")) + } + } + + albumData := getMap(trackData, "albumOfTrack") + var albumInfo map[string]interface{} + copyrightInfo := []map[string]interface{}{} + discInfo := map[string]interface{}{ + "discNumber": getFloat64(trackData, "discNumber"), + "totalDiscs": nil, + } + + if len(albumData) > 0 { + copyrightData := getMap(albumData, "copyright") + if len(copyrightData) > 0 { + copyrightItems := getSlice(copyrightData, "items") + if copyrightItems != nil { + for _, item := range copyrightItems { + itemMap, ok := item.(map[string]interface{}) + if !ok { + continue + } + if getString(itemMap, "type") != "P" { + copyrightInfo = append(copyrightInfo, map[string]interface{}{ + "text": getString(itemMap, "text"), + }) + } + } + } + } + + tracksData := getMap(albumData, "tracks") + if len(tracksData) > 0 { + discNumbers := make(map[int]bool) + trackItems := getSlice(tracksData, "items") + if trackItems != nil { + for _, item := range trackItems { + itemMap, ok := item.(map[string]interface{}) + if !ok { + continue + } + trackItem := getMap(itemMap, "track") + if len(trackItem) > 0 { + discNum := int(getFloat64(trackItem, "discNumber")) + if discNum == 0 { + discNum = 1 + } + discNumbers[discNum] = true + } + } + } + if len(discNumbers) > 0 { + maxDisc := 1 + for discNum := range discNumbers { + if discNum > maxDisc { + maxDisc = discNum + } + } + discInfo["totalDiscs"] = maxDisc + } + } + + dateInfo := getMap(albumData, "date") + releaseDate := getString(dateInfo, "isoString") + var releaseYear interface{} + if releaseDate == "" && len(dateInfo) > 0 { + yearStr := getString(dateInfo, "year") + monthStr := getString(dateInfo, "month") + dayStr := getString(dateInfo, "day") + if yearStr != "" { + year, err := strconv.Atoi(yearStr) + if err == nil { + releaseYear = year + if monthStr != "" && dayStr != "" { + month, _ := strconv.Atoi(monthStr) + day, _ := strconv.Atoi(dayStr) + releaseDate = fmt.Sprintf("%s-%02d-%02d", yearStr, month, day) + } else { + releaseDate = yearStr + } + } + } + } else if releaseDate != "" { + parts := strings.Split(releaseDate, "T") + if len(parts) > 0 { + releaseDate = parts[0] + } else { + parts = strings.Split(releaseDate, " ") + if len(parts) > 0 { + releaseDate = parts[0] + } + } + dateParts := strings.Split(releaseDate, "-") + if len(dateParts) > 0 && dateParts[0] != "" { + year, err := strconv.Atoi(dateParts[0]) + if err == nil { + releaseYear = year + } + } + } + + tracksTotalCount := float64(0) + if len(tracksData) > 0 { + tracksTotalCount = getFloat64(tracksData, "totalCount") + } + + albumID := getString(albumData, "id") + if albumID == "" { + albumURI := getString(albumData, "uri") + if strings.Contains(albumURI, ":") { + parts := strings.Split(albumURI, ":") + albumID = parts[len(parts)-1] + } + } + + albumArtistsString := "" + albumLabel := "" + if albumFetchDataMap != nil && len(albumFetchDataMap) > 0 { + albumUnionData := getMap(getMap(albumFetchDataMap, "data"), "albumUnion") + if len(albumUnionData) > 0 { + albumArtists := extractArtists(getMap(albumUnionData, "artists")) + if len(albumArtists) > 0 { + albumArtistNames := []string{} + for _, artist := range albumArtists { + albumArtistNames = append(albumArtistNames, getString(artist, "name")) + } + albumArtistsString = strings.Join(albumArtistNames, ", ") + } + if albumArtistsString == "" { + albumArtistsString = getString(albumUnionData, "artists") + } + albumLabel = getString(albumUnionData, "label") + } + } + + if albumArtistsString == "" { + albumArtists := extractArtists(getMap(albumData, "artists")) + if len(albumArtists) > 0 { + albumArtistNames := []string{} + for _, artist := range albumArtists { + albumArtistNames = append(albumArtistNames, getString(artist, "name")) + } + albumArtistsString = strings.Join(albumArtistNames, ", ") + } + } + + albumInfo = map[string]interface{}{ + "id": albumID, + "name": getString(albumData, "name"), + "released": releaseDate, + "year": releaseYear, + "tracks": int(tracksTotalCount), + } + + if albumArtistsString != "" { + albumInfo["artists"] = albumArtistsString + } + + if albumLabel != "" { + albumInfo["label"] = albumLabel + } + } + + cover := extractCoverImage(getMap(trackData, "visualIdentity")) + if cover == nil && len(albumData) > 0 { + cover = extractCoverImage(getMap(albumData, "coverArt")) + } + + durationMs := getFloat64(getMap(trackData, "duration"), "totalMilliseconds") + durationObj := extractDuration(durationMs) + durationString := getString(durationObj, "formatted") + + artistNames := []string{} + for _, artist := range artists { + artistNames = append(artistNames, getString(artist, "name")) + } + artistsString := strings.Join(artistNames, ", ") + + copyrightTexts := []string{} + for _, item := range copyrightInfo { + copyrightTexts = append(copyrightTexts, getString(item, "text")) + } + copyrightString := strings.Join(copyrightTexts, ", ") + + discNumber := int(getFloat64(trackData, "discNumber")) + if discNumber == 0 { + discNumber = 1 + } + + maxDiscFromAlbum := 0 + totalDiscsFromAlbum := 0 + + if len(albumFetchData) > 0 && albumFetchData[0] != nil { + albumUnion := getMap(getMap(albumFetchData[0], "data"), "albumUnion") + if len(albumUnion) > 0 { + discsData := getMap(albumUnion, "discs") + if len(discsData) > 0 { + totalDiscsFromAlbum = int(getFloat64(discsData, "totalCount")) + } + + albumTracks := getMap(albumUnion, "tracks") + if len(albumTracks) > 0 { + albumTrackItems := getSlice(albumTracks, "items") + currentTrackID := getString(trackData, "id") + for idx, item := range albumTrackItems { + itemMap, ok := item.(map[string]interface{}) + if !ok { + continue + } + trackItem := getMap(itemMap, "track") + if len(trackItem) > 0 { + dNum := int(getFloat64(trackItem, "discNumber")) + if dNum > maxDiscFromAlbum { + maxDiscFromAlbum = dNum + } + + trackURI := getString(trackItem, "uri") + if strings.Contains(trackURI, currentTrackID) || getString(trackItem, "id") == currentTrackID { + if dNum > 0 { + discNumber = dNum + } + } + + trackNum := int(getFloat64(trackData, "trackNumber")) + itemTrackNum := idx + 1 + if trackNum == itemTrackNum && dNum > 0 { + } + } + } + } + } + } + + totalDiscs := 1 + if totalDiscsFromAlbum > 0 { + totalDiscs = totalDiscsFromAlbum + } else if maxDiscFromAlbum > 0 { + totalDiscs = maxDiscFromAlbum + } else if discInfo["totalDiscs"] != nil { + totalDiscs = discInfo["totalDiscs"].(int) + } + + contentRating := getMap(trackData, "contentRating") + isExplicit := getString(contentRating, "label") == "EXPLICIT" + + filtered := map[string]interface{}{ + "id": getString(trackData, "id"), + "name": getString(trackData, "name"), + "artists": artistsString, + "album": albumInfo, + "duration": durationString, + "track": int(getFloat64(trackData, "trackNumber")), + "disc": discNumber, + "discs": totalDiscs, + "copyright": copyrightString, + "plays": getString(trackData, "playcount"), + "cover": cover, + "is_explicit": isExplicit, + } + + return filtered +} + +func FilterAlbum(data map[string]interface{}) map[string]interface{} { + dataMap := getMap(data, "data") + albumData := getMap(dataMap, "albumUnion") + if len(albumData) == 0 { + return make(map[string]interface{}) + } + + artists := extractArtists(getMap(albumData, "artists")) + artistNames := []string{} + for _, artist := range artists { + artistNames = append(artistNames, getString(artist, "name")) + } + albumArtistsString := strings.Join(artistNames, ", ") + + coverObj := extractCoverImage(getMap(albumData, "coverArt")) + var cover interface{} + if coverObj != nil { + + cover = getString(coverObj, "small") + if cover == "" { + cover = getString(coverObj, "medium") + } + if cover == "" { + cover = getString(coverObj, "large") + } + } + + tracks := []map[string]interface{}{} + tracksData := getMap(albumData, "tracksV2") + trackItems := getSlice(tracksData, "items") + if trackItems != nil { + for _, item := range trackItems { + itemMap, ok := item.(map[string]interface{}) + if !ok { + continue + } + track := getMap(itemMap, "track") + if len(track) == 0 { + continue + } + + artistsData := getMap(track, "artists") + trackArtists := extractArtists(artistsData) + trackDurationMs := getFloat64(getMap(track, "duration"), "totalMilliseconds") + durationObj := extractDuration(trackDurationMs) + durationString := getString(durationObj, "formatted") + + trackArtistNames := []string{} + artistIDs := []string{} + + artistItems := getSlice(artistsData, "items") + if artistItems != nil { + for _, artistItem := range artistItems { + artistItemMap, ok := artistItem.(map[string]interface{}) + if !ok { + continue + } + artistURI := getString(artistItemMap, "uri") + if artistURI != "" && strings.Contains(artistURI, ":") { + parts := strings.Split(artistURI, ":") + if len(parts) > 0 { + artistID := parts[len(parts)-1] + if artistID != "" { + artistIDs = append(artistIDs, artistID) + } + } + } + } + } + + for _, artist := range trackArtists { + trackArtistNames = append(trackArtistNames, getString(artist, "name")) + } + trackArtistsString := strings.Join(trackArtistNames, ", ") + + trackURI := getString(track, "uri") + trackID := "" + if strings.Contains(trackURI, ":") { + parts := strings.Split(trackURI, ":") + trackID = parts[len(parts)-1] + } + + contentRating := getMap(track, "contentRating") + isExplicit := getString(contentRating, "label") == "EXPLICIT" + + discNumber := int(getFloat64(track, "discNumber")) + if discNumber == 0 { + discNumber = 1 + } + + trackInfo := map[string]interface{}{ + "id": trackID, + "name": getString(track, "name"), + "artists": trackArtistsString, + "artistIds": artistIDs, + "duration": durationString, + "plays": getString(track, "playcount"), + "is_explicit": isExplicit, + "disc_number": discNumber, + } + tracks = append(tracks, trackInfo) + } + } + + dateInfo := getMap(albumData, "date") + releaseDate := getString(dateInfo, "isoString") + if releaseDate != "" && strings.Contains(releaseDate, "T") { + parts := strings.Split(releaseDate, "T") + releaseDate = parts[0] + } + + albumURI := getString(albumData, "uri") + albumID := "" + if strings.Contains(albumURI, ":") { + parts := strings.Split(albumURI, ":") + albumID = parts[len(parts)-1] + } + + totalDiscs := 1 + discsData := getMap(albumData, "discs") + if len(discsData) > 0 { + totalDiscs = int(getFloat64(discsData, "totalCount")) + } + + filtered := map[string]interface{}{ + "id": albumID, + "name": getString(albumData, "name"), + "artists": albumArtistsString, + "cover": cover, + "releaseDate": releaseDate, + "count": len(tracks), + "tracks": tracks, + "discs": map[string]interface{}{ + "totalCount": totalDiscs, + }, + "label": getString(albumData, "label"), + } + + return filtered +} + +func FilterPlaylist(data map[string]interface{}) map[string]interface{} { + dataMap := getMap(data, "data") + playlistData := getMap(dataMap, "playlistV2") + if len(playlistData) == 0 { + return make(map[string]interface{}) + } + + ownerData := getMap(getMap(playlistData, "ownerV2"), "data") + var ownerInfo map[string]interface{} + if len(ownerData) > 0 { + var avatarURL interface{} + avatarData := getMap(ownerData, "avatar") + if len(avatarData) > 0 { + sources := getSlice(avatarData, "sources") + if sources != nil { + for _, source := range sources { + sourceMap, ok := source.(map[string]interface{}) + if !ok { + continue + } + if getFloat64(sourceMap, "width") == 300 { + avatarURL = getString(sourceMap, "url") + break + } + } + if avatarURL == nil && len(sources) > 0 { + if firstSource, ok := sources[0].(map[string]interface{}); ok { + avatarURL = getString(firstSource, "url") + } + } + } + } + + ownerInfo = map[string]interface{}{ + "name": getString(ownerData, "name"), + "avatar": avatarURL, + } + } + + imagesData := getMap(playlistData, "images") + if len(imagesData) == 0 { + imagesData = getMap(playlistData, "imagesV2") + } + var cover interface{} + if len(imagesData) > 0 { + imageItems := getSlice(imagesData, "items") + if imageItems != nil && len(imageItems) > 0 { + if firstImage, ok := imageItems[0].(map[string]interface{}); ok { + firstSources := getSlice(firstImage, "sources") + if firstSources != nil && len(firstSources) > 0 { + if firstSource, ok := firstSources[0].(map[string]interface{}); ok { + sourceURL := getString(firstSource, "url") + if sourceURL != "" { + cover = sourceURL + } + } + } + } + } + if cover == nil { + imageSources := getSlice(imagesData, "sources") + if imageSources != nil && len(imageSources) > 0 { + if firstSource, ok := imageSources[0].(map[string]interface{}); ok { + sourceURL := getString(firstSource, "url") + if sourceURL != "" { + cover = sourceURL + } + } + } + } + } + + tracks := []map[string]interface{}{} + content := getMap(playlistData, "content") + contentItems := getSlice(content, "items") + if contentItems != nil { + for _, item := range contentItems { + itemMap, ok := item.(map[string]interface{}) + if !ok { + continue + } + trackData := getMap(getMap(itemMap, "itemV2"), "data") + if len(trackData) == 0 { + continue + } + + var rank interface{} + var status interface{} + attributes := getSlice(itemMap, "attributes") + if attributes != nil { + for _, attr := range attributes { + attrMap, ok := attr.(map[string]interface{}) + if !ok { + continue + } + key := getString(attrMap, "key") + if key == "rank" { + rank = getString(attrMap, "value") + } else if key == "status" { + status = getString(attrMap, "value") + } + } + } + + artistsData := getMap(trackData, "artists") + trackArtists := extractArtists(artistsData) + trackArtistNames := []string{} + artistIDs := []string{} + + artistItems := getSlice(artistsData, "items") + if artistItems != nil { + for _, artistItem := range artistItems { + artistItemMap, ok := artistItem.(map[string]interface{}) + if !ok { + continue + } + artistURI := getString(artistItemMap, "uri") + if artistURI != "" && strings.Contains(artistURI, ":") { + parts := strings.Split(artistURI, ":") + if len(parts) > 0 { + artistID := parts[len(parts)-1] + if artistID != "" { + artistIDs = append(artistIDs, artistID) + } + } + } + } + } + + for _, artist := range trackArtists { + trackArtistNames = append(trackArtistNames, getString(artist, "name")) + } + artistsString := strings.Join(trackArtistNames, ", ") + + trackDurationMs := getFloat64(getMap(trackData, "trackDuration"), "totalMilliseconds") + durationObj := extractDuration(trackDurationMs) + durationString := getString(durationObj, "formatted") + + trackURI := getString(trackData, "uri") + trackID := getString(trackData, "id") + if trackID == "" { + if strings.Contains(trackURI, ":") { + parts := strings.Split(trackURI, ":") + trackID = parts[len(parts)-1] + } + } + + albumData := getMap(trackData, "albumOfTrack") + albumName := "" + albumID := "" + albumArtistsString := "" + var trackCover interface{} + + if len(albumData) > 0 { + albumName = getString(albumData, "name") + albumURI := getString(albumData, "uri") + if strings.Contains(albumURI, ":") { + parts := strings.Split(albumURI, ":") + albumID = parts[len(parts)-1] + } + coverObj := extractCoverImage(getMap(albumData, "coverArt")) + if coverObj != nil { + + trackCover = getString(coverObj, "small") + if trackCover == "" { + trackCover = getString(coverObj, "medium") + } + if trackCover == "" { + trackCover = getString(coverObj, "large") + } + } + + albumArtists := extractArtists(getMap(albumData, "artists")) + if len(albumArtists) > 0 { + albumArtistNames := []string{} + for _, artist := range albumArtists { + albumArtistNames = append(albumArtistNames, getString(artist, "name")) + } + albumArtistsString = strings.Join(albumArtistNames, ", ") + } + } + + contentRating := getMap(trackData, "contentRating") + isExplicit := getString(contentRating, "label") == "EXPLICIT" + + trackName := getString(trackData, "name") + if trackName == "" { + continue + } + + trackInfo := map[string]interface{}{ + "id": trackID, + "cover": trackCover, + "title": trackName, + "artist": artistsString, + "artistIds": artistIDs, + "plays": rank, + "status": status, + "album": albumName, + "albumArtist": albumArtistsString, + "albumId": albumID, + "duration": durationString, + "is_explicit": isExplicit, + "disc_number": int(getFloat64(trackData, "discNumber")), + } + tracks = append(tracks, trackInfo) + } + } + + followersData, exists := playlistData["followers"] + var followersCount interface{} + if exists { + if followersMap, ok := followersData.(map[string]interface{}); ok { + followersCount = getFloat64(followersMap, "totalCount") + } else if count, ok := followersData.(float64); ok { + followersCount = count + } else if count, ok := followersData.(int); ok { + followersCount = float64(count) + } else { + followersCount = float64(0) + } + } else { + followersCount = float64(0) + } + + playlistURI := getString(playlistData, "uri") + playlistID := "" + if strings.Contains(playlistURI, ":") { + parts := strings.Split(playlistURI, ":") + playlistID = parts[len(parts)-1] + } + + totalCount := getFloat64(content, "totalCount") + count := len(tracks) + if totalCount > 0 { + count = int(totalCount) + } + + filtered := map[string]interface{}{ + "id": playlistID, + "name": getString(playlistData, "name"), + "description": getString(playlistData, "description"), + "owner": ownerInfo, + "cover": cover, + "count": count, + "tracks": tracks, + "followers": followersCount, + } + + return filtered +} + +func extractRelease(release map[string]interface{}) map[string]interface{} { + if len(release) == 0 { + return nil + } + + dateInfo := getMap(release, "date") + releaseDate := getString(dateInfo, "isoString") + if releaseDate == "" && len(dateInfo) > 0 { + yearStr := getString(dateInfo, "year") + monthStr := getString(dateInfo, "month") + dayStr := getString(dateInfo, "day") + if yearStr != "" { + if monthStr != "" && dayStr != "" { + month, _ := strconv.Atoi(monthStr) + day, _ := strconv.Atoi(dayStr) + releaseDate = fmt.Sprintf("%s-%02d-%02d", yearStr, month, day) + } else { + releaseDate = yearStr + } + } + } else if releaseDate != "" && strings.Contains(releaseDate, "T") { + parts := strings.Split(releaseDate, "T") + releaseDate = parts[0] + } + + coverObj := extractCoverImage(getMap(release, "coverArt")) + var cover interface{} + if coverObj != nil { + cover = getString(coverObj, "medium") + } + + releaseID := getString(release, "id") + if releaseID == "" { + releaseURI := getString(release, "uri") + if strings.Contains(releaseURI, ":") { + parts := strings.Split(releaseURI, ":") + releaseID = parts[len(parts)-1] + } + } + + var year interface{} + if yearVal, exists := dateInfo["year"]; exists { + year = yearVal + } + + var totalTracks int + tracksInfo := getMap(release, "tracks") + if tracksInfo != nil { + totalTracks = int(getFloat64(tracksInfo, "totalCount")) + } + + return map[string]interface{}{ + "id": releaseID, + "name": getString(release, "name"), + "cover": cover, + "date": releaseDate, + "year": year, + "total_tracks": totalTracks, + "type": getString(release, "type"), + } +} + +func extractDiscographyItems(itemsData map[string]interface{}) []map[string]interface{} { + items := []map[string]interface{}{} + dataItems := getSlice(itemsData, "items") + if dataItems != nil { + for _, item := range dataItems { + itemMap, ok := item.(map[string]interface{}) + if !ok { + continue + } + releases := getMap(itemMap, "releases") + var release map[string]interface{} + if len(releases) > 0 { + releaseItems := getSlice(releases, "items") + if releaseItems != nil && len(releaseItems) > 0 { + if releaseMap, ok := releaseItems[0].(map[string]interface{}); ok { + release = releaseMap + } + } + } else { + release = getMap(itemMap, "album") + } + + if len(release) > 0 { + extracted := extractRelease(release) + if extracted != nil { + items = append(items, extracted) + } + } + } + } + return items +} + +func stripHTMLTags(s string) string { + re := regexp.MustCompile(`<[^>]*>`) + return re.ReplaceAllString(s, "") +} + +func FilterArtist(data map[string]interface{}) map[string]interface{} { + dataMap := getMap(data, "data") + artistData := getMap(dataMap, "artistUnion") + if len(artistData) == 0 { + return make(map[string]interface{}) + } + + profileRaw := getMap(artistData, "profile") + profile := make(map[string]interface{}) + if len(profileRaw) > 0 { + if biography, exists := profileRaw["biography"]; exists { + biographyMap, ok := biography.(map[string]interface{}) + if ok { + biographyText := getString(biographyMap, "text") + if biographyText != "" { + profile["biography"] = html.UnescapeString(stripHTMLTags(biographyText)) + } + } + } + if _, exists := profileRaw["name"]; exists { + profile["name"] = getString(profileRaw, "name") + } + if _, exists := profileRaw["verified"]; exists { + profile["verified"] = getBool(profileRaw, "verified") + } + } + + headerImageData := getMap(artistData, "headerImage") + var headerImage interface{} + if len(headerImageData) > 0 { + headerData := getMap(headerImageData, "data") + if len(headerData) > 0 { + sources := getSlice(headerData, "sources") + if sources != nil && len(sources) > 0 { + if firstSource, ok := sources[0].(map[string]interface{}); ok { + headerImage = getString(firstSource, "url") + } + } + } + } + + statsRaw := getMap(artistData, "stats") + stats := make(map[string]interface{}) + if len(statsRaw) > 0 { + if _, exists := statsRaw["followers"]; exists { + stats["followers"] = getFloat64(statsRaw, "followers") + } + if _, exists := statsRaw["monthlyListeners"]; exists { + stats["listeners"] = getFloat64(statsRaw, "monthlyListeners") + } + if _, exists := statsRaw["worldRank"]; exists { + stats["rank"] = getFloat64(statsRaw, "worldRank") + } + } + + discography := getMap(artistData, "discography") + discographyResult := make(map[string]interface{}) + + allData := getMap(discography, "all") + if len(allData) > 0 { + discographyResult["all"] = extractDiscographyItems(allData) + if totalCount, exists := allData["totalCount"]; exists { + var total float64 + if tc, ok := totalCount.(float64); ok { + total = tc + } else if tc, ok := totalCount.(int); ok { + total = float64(tc) + } else if tc, ok := totalCount.(int64); ok { + total = float64(tc) + } + discographyResult["total"] = total + } + } + + visualsData := getMap(artistData, "visuals") + galleryData := getMap(visualsData, "gallery") + gallery := []interface{}{} + if len(galleryData) > 0 { + galleryItems := getSlice(galleryData, "items") + if galleryItems != nil { + for _, item := range galleryItems { + itemMap, ok := item.(map[string]interface{}) + if !ok { + continue + } + sources := getSlice(itemMap, "sources") + if sources != nil && len(sources) > 0 { + if firstSource, ok := sources[0].(map[string]interface{}); ok { + galleryURL := getString(firstSource, "url") + if galleryURL != "" { + gallery = append(gallery, galleryURL) + } + } + } + } + } + } + + avatarObj := extractCoverImage(getMap(visualsData, "avatarImage")) + var avatar interface{} + if avatarObj != nil { + if mediumURL, ok := avatarObj["medium"].(string); ok && mediumURL != "" { + avatar = mediumURL + } else if smallURL, ok := avatarObj["small"].(string); ok && smallURL != "" { + avatar = smallURL + } + } + + artistURI := getString(artistData, "uri") + artistID := "" + if strings.Contains(artistURI, ":") { + parts := strings.Split(artistURI, ":") + artistID = parts[len(parts)-1] + } + + filtered := map[string]interface{}{ + "id": artistID, + "name": getString(profile, "name"), + "profile": profile, + "avatar": avatar, + "header": headerImage, + "stats": stats, + "gallery": gallery, + "discography": discographyResult, + } + + return filtered +} + +func FilterSearch(data map[string]interface{}) map[string]interface{} { + dataMap := getMap(data, "data") + searchData := getMap(dataMap, "searchV2") + if len(searchData) == 0 { + return make(map[string]interface{}) + } + + results := map[string]interface{}{ + "tracks": []map[string]interface{}{}, + "albums": []map[string]interface{}{}, + "artists": []map[string]interface{}{}, + "playlists": []map[string]interface{}{}, + } + + tracksData := getMap(searchData, "tracksV2") + if len(tracksData) == 0 { + tracksData = getMap(searchData, "tracks") + } + trackItems := getSlice(tracksData, "items") + if trackItems != nil { + for _, item := range trackItems { + itemMap, ok := item.(map[string]interface{}) + if !ok { + continue + } + var track map[string]interface{} + if itemData, exists := itemMap["item"]; exists { + itemDataMap, ok := itemData.(map[string]interface{}) + if ok { + track = getMap(itemDataMap, "data") + } + } else if trackData, exists := itemMap["track"]; exists { + if trackMap, ok := trackData.(map[string]interface{}); ok { + track = trackMap + } + } + + if len(track) == 0 { + continue + } + + trackArtists := extractArtists(getMap(track, "artists")) + trackDurationMs := getFloat64(getMap(track, "duration"), "totalMilliseconds") + if trackDurationMs == 0 { + trackDurationMs = getFloat64(getMap(track, "trackDuration"), "totalMilliseconds") + } + trackDuration := extractDuration(trackDurationMs) + + albumData := getMap(track, "albumOfTrack") + var albumInfo map[string]interface{} + if len(albumData) > 0 { + albumURI := getString(albumData, "uri") + albumID := getString(albumData, "id") + if albumID == "" { + if strings.Contains(albumURI, ":") { + parts := strings.Split(albumURI, ":") + albumID = parts[len(parts)-1] + } + } + albumInfo = map[string]interface{}{ + "name": getString(albumData, "name"), + "uri": albumURI, + "id": albumID, + } + } + + trackURI := getString(track, "uri") + trackID := getString(track, "id") + if trackID == "" { + if strings.Contains(trackURI, ":") { + parts := strings.Split(trackURI, ":") + trackID = parts[len(parts)-1] + } + } + + coverObj := extractCoverImage(getMap(albumData, "coverArt")) + var cover interface{} + if coverObj != nil { + cover = getString(coverObj, "medium") + } + + trackName := getString(track, "name") + if trackName == "" { + continue + } + + trackArtistNames := []string{} + for _, artist := range trackArtists { + trackArtistNames = append(trackArtistNames, getString(artist, "name")) + } + trackArtistsString := strings.Join(trackArtistNames, ", ") + + durationString := getString(trackDuration, "formatted") + + albumName := "" + if albumInfo != nil { + albumName = getString(albumInfo, "name") + } + + contentRating := getMap(track, "contentRating") + isExplicit := getString(contentRating, "label") == "EXPLICIT" + + trackResults := results["tracks"].([]map[string]interface{}) + trackResults = append(trackResults, map[string]interface{}{ + "id": trackID, + "name": trackName, + "artists": trackArtistsString, + "album": albumName, + "duration": durationString, + "cover": cover, + "is_explicit": isExplicit, + }) + results["tracks"] = trackResults + } + } + + albumsData := getMap(searchData, "albumsV2") + if len(albumsData) == 0 { + albumsData = getMap(searchData, "albums") + } + albumItems := getSlice(albumsData, "items") + if albumItems != nil { + for _, item := range albumItems { + itemMap, ok := item.(map[string]interface{}) + if !ok { + continue + } + var album map[string]interface{} + if itemData, exists := itemMap["data"]; exists { + if albumMap, ok := itemData.(map[string]interface{}); ok { + album = albumMap + } + } else if albumData, exists := itemMap["album"]; exists { + if albumMap, ok := albumData.(map[string]interface{}); ok { + album = albumMap + } + } + + if len(album) == 0 { + continue + } + + albumArtists := extractArtists(getMap(album, "artists")) + albumURI := getString(album, "uri") + albumID := getString(album, "id") + if albumID == "" { + if strings.Contains(albumURI, ":") { + parts := strings.Split(albumURI, ":") + albumID = parts[len(parts)-1] + } + } + + coverObj := extractCoverImage(getMap(album, "coverArt")) + var cover interface{} + if coverObj != nil { + cover = getString(coverObj, "medium") + } + + albumArtistNames := []string{} + for _, artist := range albumArtists { + albumArtistNames = append(albumArtistNames, getString(artist, "name")) + } + albumArtistsString := strings.Join(albumArtistNames, ", ") + + dateInfo := getMap(album, "date") + var year interface{} + if len(dateInfo) > 0 { + if yearVal, exists := dateInfo["year"]; exists { + year = yearVal + } + } + + albumName := getString(album, "name") + if albumName == "" || albumArtistsString == "" { + continue + } + + albumResult := map[string]interface{}{ + "id": albumID, + "name": albumName, + "artists": albumArtistsString, + "cover": cover, + } + + if year != nil { + albumResult["year"] = year + } + + albumResults := results["albums"].([]map[string]interface{}) + albumResults = append(albumResults, albumResult) + results["albums"] = albumResults + } + } + + artistsData := getMap(searchData, "artistsV2") + if len(artistsData) == 0 { + artistsData = getMap(searchData, "artists") + } + artistItems := getSlice(artistsData, "items") + if artistItems != nil { + for _, item := range artistItems { + itemMap, ok := item.(map[string]interface{}) + if !ok { + continue + } + var artist map[string]interface{} + if itemData, exists := itemMap["data"]; exists { + if artistMap, ok := itemData.(map[string]interface{}); ok { + artist = artistMap + } + } else if artistData, exists := itemMap["artist"]; exists { + if artistMap, ok := artistData.(map[string]interface{}); ok { + artist = artistMap + } + } + + if len(artist) == 0 { + continue + } + + artistURI := getString(artist, "uri") + artistID := "" + if strings.Contains(artistURI, ":") { + parts := strings.Split(artistURI, ":") + artistID = parts[len(parts)-1] + } + + coverObj := extractCoverImage(getMap(artist, "visualIdentity")) + if coverObj == nil { + visuals := getMap(artist, "visuals") + if len(visuals) > 0 { + coverObj = extractCoverImage(getMap(visuals, "avatarImage")) + } + } + + var cover interface{} + if coverObj != nil { + cover = getString(coverObj, "medium") + } + + artistName := getString(getMap(artist, "profile"), "name") + if artistName == "" { + artistName = getString(artist, "name") + } + + if artistName == "" { + continue + } + + artistResults := results["artists"].([]map[string]interface{}) + artistResults = append(artistResults, map[string]interface{}{ + "id": artistID, + "name": artistName, + "cover": cover, + }) + results["artists"] = artistResults + } + } + + playlistsData := getMap(searchData, "playlistsV2") + if len(playlistsData) == 0 { + playlistsData = getMap(searchData, "playlists") + } + playlistItems := getSlice(playlistsData, "items") + if playlistItems != nil { + for _, item := range playlistItems { + itemMap, ok := item.(map[string]interface{}) + if !ok { + continue + } + var playlist map[string]interface{} + if itemData, exists := itemMap["data"]; exists { + if playlistMap, ok := itemData.(map[string]interface{}); ok { + playlist = playlistMap + } + } else if playlistData, exists := itemMap["playlist"]; exists { + if playlistMap, ok := playlistData.(map[string]interface{}); ok { + playlist = playlistMap + } + } + + if len(playlist) == 0 { + continue + } + + playlistURI := getString(playlist, "uri") + playlistID := "" + if strings.Contains(playlistURI, ":") { + parts := strings.Split(playlistURI, ":") + playlistID = parts[len(parts)-1] + } + + playlistImages := getMap(playlist, "images") + if len(playlistImages) == 0 { + playlistImages = getMap(playlist, "imagesV2") + } + var playlistCoverObj map[string]interface{} + if len(playlistImages) > 0 { + imageItems := getSlice(playlistImages, "items") + if imageItems != nil && len(imageItems) > 0 { + if firstImage, ok := imageItems[0].(map[string]interface{}); ok { + firstSources := getSlice(firstImage, "sources") + if firstSources != nil { + playlistCoverObj = extractCoverImage(map[string]interface{}{"sources": firstSources}) + } + } + } + if playlistCoverObj == nil { + playlistCoverObj = extractCoverImage(playlistImages) + } + } + + var playlistCover interface{} + if playlistCoverObj != nil { + playlistCover = getString(playlistCoverObj, "medium") + } + + ownerData := getMap(getMap(playlist, "ownerV2"), "data") + ownerName := getString(ownerData, "name") + + playlistName := getString(playlist, "name") + if playlistName == "" { + continue + } + + playlistResult := map[string]interface{}{ + "id": playlistID, + "name": playlistName, + "cover": playlistCover, + } + + if ownerName != "" { + playlistResult["owner"] = ownerName + } + + playlistResults := results["playlists"].([]map[string]interface{}) + playlistResults = append(playlistResults, playlistResult) + results["playlists"] = playlistResults + } + } + + tracks := results["tracks"].([]map[string]interface{}) + albums := results["albums"].([]map[string]interface{}) + artists := results["artists"].([]map[string]interface{}) + playlists := results["playlists"].([]map[string]interface{}) + + return map[string]interface{}{ + "results": results, + "totalResults": map[string]interface{}{ + "tracks": len(tracks), + "albums": len(albums), + "artists": len(artists), + "playlists": len(playlists), + }, + } +} + +type SpotifyPayload map[string]interface{} + +func BuildSpotifyReqPayloadTrack(trackId string) SpotifyPayload { + payload := map[string]interface{}{ + "variables": map[string]interface{}{ + "uri": fmt.Sprintf("spotify:track:%s", trackId), + }, + "operationName": "getTrack", + "extensions": map[string]interface{}{ + "persistedQuery": map[string]interface{}{ + "version": 1, + "sha256Hash": "612585ae06ba435ad26369870deaae23b5c8800a256cd8a57e08eddc25a37294", + }, + }, + } + + return payload +} diff --git a/lib/utils.go b/lib/utils.go index 64c243b..5deac84 100644 --- a/lib/utils.go +++ b/lib/utils.go @@ -1,6 +1,7 @@ package lib import ( + "errors" "strings" ) @@ -12,7 +13,7 @@ const ( UrlTypeInvalid ) -func GetUrlType(url string) UrlType { +func ParseUrlType(url string) UrlType { if strings.Contains(url, "https://open.spotify.com/track") { return UrlTypeTrack } @@ -23,3 +24,13 @@ func GetUrlType(url string) UrlType { return UrlTypeInvalid } + +func ParseTrackId(url string) (string, error) { + tmp := strings.Split(url, "/") + + if len(tmp) == 0 { + return "", errors.New("Invalid URL.") + } + + return tmp[len(tmp)-1], nil +}