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 }