This commit is contained in:
afkarxyz
2026-03-25 21:06:45 +07:00
parent ff14990bd8
commit f13359df7f
49 changed files with 4669 additions and 1477 deletions
+100 -30
View File
@@ -18,13 +18,17 @@ var (
errInvalidSpotifyURL = errors.New("invalid or unsupported Spotify URL")
)
type MetadataCallback func(data interface{})
type SpotifyMetadataClient struct {
httpClient *http.Client
Separator string
}
func NewSpotifyMetadataClient() *SpotifyMetadataClient {
return &SpotifyMetadataClient{
httpClient: &http.Client{Timeout: 30 * time.Second},
Separator: ", ",
}
}
@@ -342,54 +346,57 @@ type SearchResponse struct {
Playlists []SearchResult `json:"playlists"`
}
func GetFilteredSpotifyData(ctx context.Context, spotifyURL string, batch bool, delay time.Duration) (interface{}, error) {
func GetFilteredSpotifyData(ctx context.Context, spotifyURL string, batch bool, delay time.Duration, separator string, callback MetadataCallback) (interface{}, error) {
client := NewSpotifyMetadataClient()
return client.GetFilteredData(ctx, spotifyURL, batch, delay)
if separator != "" {
client.Separator = separator
}
return client.GetFilteredData(ctx, spotifyURL, batch, delay, callback)
}
func (c *SpotifyMetadataClient) GetFilteredData(ctx context.Context, spotifyURL string, batch bool, delay time.Duration) (interface{}, error) {
func (c *SpotifyMetadataClient) GetFilteredData(ctx context.Context, spotifyURL string, batch bool, delay time.Duration, callback MetadataCallback) (interface{}, error) {
parsed, err := parseSpotifyURI(spotifyURL)
if err != nil {
return nil, err
}
raw, err := c.getRawSpotifyData(ctx, parsed, batch, delay)
raw, err := c.getRawSpotifyData(ctx, parsed, batch, delay, callback)
if err != nil {
return nil, err
}
return c.processSpotifyData(ctx, raw)
return c.processSpotifyData(ctx, raw, callback)
}
func (c *SpotifyMetadataClient) getRawSpotifyData(ctx context.Context, parsed spotifyURI, batch bool, delay time.Duration) (interface{}, error) {
func (c *SpotifyMetadataClient) getRawSpotifyData(ctx context.Context, parsed spotifyURI, batch bool, delay time.Duration, callback MetadataCallback) (interface{}, error) {
switch parsed.Type {
case "playlist":
return c.fetchPlaylist(ctx, parsed.ID)
return c.fetchPlaylist(ctx, parsed.ID, callback)
case "album":
return c.fetchAlbum(ctx, parsed.ID)
return c.fetchAlbum(ctx, parsed.ID, callback)
case "track":
return c.fetchTrack(ctx, parsed.ID)
case "artist_discography":
return c.fetchArtistDiscography(ctx, parsed)
return c.fetchArtistDiscography(ctx, parsed, callback)
case "artist":
discographyParsed := spotifyURI{Type: "artist_discography", ID: parsed.ID, DiscographyGroup: "all"}
return c.fetchArtistDiscography(ctx, discographyParsed)
return c.fetchArtistDiscography(ctx, discographyParsed, callback)
default:
return nil, fmt.Errorf("unsupported Spotify type: %s", parsed.Type)
}
}
func (c *SpotifyMetadataClient) processSpotifyData(ctx context.Context, raw interface{}) (interface{}, error) {
func (c *SpotifyMetadataClient) processSpotifyData(ctx context.Context, raw interface{}, callback MetadataCallback) (interface{}, error) {
switch payload := raw.(type) {
case *apiPlaylistResponse:
return c.formatPlaylistData(payload), nil
return c.formatPlaylistData(payload, callback), nil
case *apiAlbumResponse:
return c.formatAlbumData(payload)
return c.formatAlbumData(payload, callback)
case *apiTrackResponse:
return c.formatTrackData(payload), nil
case *apiArtistResponse:
return c.formatArtistDiscographyData(ctx, payload)
return c.formatArtistDiscographyData(ctx, payload, callback)
default:
return nil, errors.New("unknown raw payload type")
}
@@ -437,7 +444,7 @@ func (c *SpotifyMetadataClient) fetchTrack(ctx context.Context, trackID string)
if albumID != "" {
albumResponse, err := c.fetchAlbumWithClient(ctx, client, albumID)
albumResponse, err := c.fetchAlbumWithClient(ctx, client, albumID, nil)
if err == nil && albumResponse != nil {
albumJSON, _ := json.Marshal(albumResponse)
@@ -482,7 +489,7 @@ func (c *SpotifyMetadataClient) fetchTrack(ctx context.Context, trackID string)
}
}
filteredData := FilterTrack(data, albumFetchData)
filteredData := FilterTrack(data, c.Separator, albumFetchData)
jsonData, err := json.Marshal(filteredData)
if err != nil {
@@ -497,15 +504,15 @@ func (c *SpotifyMetadataClient) fetchTrack(ctx context.Context, trackID string)
return &result, nil
}
func (c *SpotifyMetadataClient) fetchAlbum(ctx context.Context, albumID string) (*apiAlbumResponse, error) {
func (c *SpotifyMetadataClient) fetchAlbum(ctx context.Context, albumID string, callback MetadataCallback) (*apiAlbumResponse, error) {
client := NewSpotifyClient()
if err := client.Initialize(); err != nil {
return nil, fmt.Errorf("failed to initialize spotify client: %w", err)
}
return c.fetchAlbumWithClient(ctx, client, albumID)
return c.fetchAlbumWithClient(ctx, client, albumID, callback)
}
func (c *SpotifyMetadataClient) fetchAlbumWithClient(ctx context.Context, client *SpotifyClient, albumID string) (*apiAlbumResponse, error) {
func (c *SpotifyMetadataClient) fetchAlbumWithClient(ctx context.Context, client *SpotifyClient, albumID string, callback MetadataCallback) (*apiAlbumResponse, error) {
allItems := []interface{}{}
offset := 0
@@ -537,6 +544,15 @@ func (c *SpotifyMetadataClient) fetchAlbumWithClient(ctx context.Context, client
if data == nil {
data = response
if callback != nil {
filtered := FilterAlbum(data, c.Separator)
jsonData, _ := json.Marshal(filtered)
var result apiAlbumResponse
if json.Unmarshal(jsonData, &result) == nil {
formatted, _ := c.formatAlbumData(&result, nil)
callback(formatted)
}
}
}
albumData := getMap(getMap(response, "data"), "albumUnion")
@@ -579,7 +595,7 @@ func (c *SpotifyMetadataClient) fetchAlbumWithClient(ctx context.Context, client
tracksV2["totalCount"] = len(allItems)
}
filteredData := FilterAlbum(data)
filteredData := FilterAlbum(data, c.Separator)
jsonData, err := json.Marshal(filteredData)
if err != nil {
@@ -594,7 +610,7 @@ func (c *SpotifyMetadataClient) fetchAlbumWithClient(ctx context.Context, client
return &result, nil
}
func (c *SpotifyMetadataClient) fetchPlaylist(ctx context.Context, playlistID string) (*apiPlaylistResponse, error) {
func (c *SpotifyMetadataClient) fetchPlaylist(ctx context.Context, playlistID string, callback MetadataCallback) (*apiPlaylistResponse, error) {
client := NewSpotifyClient()
if err := client.Initialize(); err != nil {
return nil, fmt.Errorf("failed to initialize spotify client: %w", err)
@@ -630,6 +646,15 @@ func (c *SpotifyMetadataClient) fetchPlaylist(ctx context.Context, playlistID st
if data == nil {
data = response
if callback != nil {
filtered := FilterPlaylist(data, c.Separator)
jsonData, _ := json.Marshal(filtered)
var result apiPlaylistResponse
if json.Unmarshal(jsonData, &result) == nil {
formatted := c.formatPlaylistData(&result, nil)
callback(formatted)
}
}
}
playlistData := getMap(getMap(response, "data"), "playlistV2")
@@ -672,7 +697,7 @@ func (c *SpotifyMetadataClient) fetchPlaylist(ctx context.Context, playlistID st
content["totalCount"] = len(allItems)
}
filteredData := FilterPlaylist(data)
filteredData := FilterPlaylist(data, c.Separator)
jsonData, err := json.Marshal(filteredData)
if err != nil {
@@ -687,7 +712,7 @@ func (c *SpotifyMetadataClient) fetchPlaylist(ctx context.Context, playlistID st
return &result, nil
}
func (c *SpotifyMetadataClient) fetchArtistDiscography(ctx context.Context, parsed spotifyURI) (*apiArtistResponse, error) {
func (c *SpotifyMetadataClient) fetchArtistDiscography(ctx context.Context, parsed spotifyURI, callback MetadataCallback) (*apiArtistResponse, error) {
client := NewSpotifyClient()
if err := client.Initialize(); err != nil {
return nil, fmt.Errorf("failed to initialize spotify client: %w", err)
@@ -712,6 +737,16 @@ func (c *SpotifyMetadataClient) fetchArtistDiscography(ctx context.Context, pars
return nil, fmt.Errorf("failed to query artist overview: %w", err)
}
if callback != nil {
filtered := FilterArtist(data, c.Separator)
jsonData, _ := json.Marshal(filtered)
var result apiArtistResponse
if json.Unmarshal(jsonData, &result) == nil {
formatted, _ := c.formatArtistDiscographyData(ctx, &result, nil)
callback(formatted)
}
}
allDiscographyItems := []interface{}{}
offset := 0
limit := 50
@@ -841,7 +876,7 @@ func (c *SpotifyMetadataClient) fetchArtistDiscography(ctx context.Context, pars
}
}
filteredData := FilterArtist(data)
filteredData := FilterArtist(data, c.Separator)
jsonData, err := json.Marshal(filteredData)
if err != nil {
@@ -898,7 +933,7 @@ func (c *SpotifyMetadataClient) formatTrackData(raw *apiTrackResponse) TrackResp
}
}
func (c *SpotifyMetadataClient) formatAlbumData(raw *apiAlbumResponse) (*AlbumResponsePayload, error) {
func (c *SpotifyMetadataClient) formatAlbumData(raw *apiAlbumResponse, callback MetadataCallback) (*AlbumResponsePayload, error) {
var artistID, artistURL string
info := AlbumInfoMetadata{
@@ -911,6 +946,13 @@ func (c *SpotifyMetadataClient) formatAlbumData(raw *apiAlbumResponse) (*AlbumRe
ArtistURL: artistURL,
}
if callback != nil {
callback(AlbumResponsePayload{
AlbumInfo: info,
TrackList: []AlbumTrackMetadata{},
})
}
tracks := make([]AlbumTrackMetadata, 0, len(raw.Tracks))
for idx, item := range raw.Tracks {
durationMS := parseDuration(item.Duration)
@@ -955,13 +997,17 @@ func (c *SpotifyMetadataClient) formatAlbumData(raw *apiAlbumResponse) (*AlbumRe
})
}
if callback != nil {
callback(tracks)
}
return &AlbumResponsePayload{
AlbumInfo: info,
TrackList: tracks,
}, nil
}
func (c *SpotifyMetadataClient) formatPlaylistData(raw *apiPlaylistResponse) PlaylistResponsePayload {
func (c *SpotifyMetadataClient) formatPlaylistData(raw *apiPlaylistResponse, callback MetadataCallback) PlaylistResponsePayload {
var info PlaylistInfoMetadata
info.Tracks.Total = raw.Count
info.Followers.Total = raw.Followers
@@ -971,6 +1017,13 @@ func (c *SpotifyMetadataClient) formatPlaylistData(raw *apiPlaylistResponse) Pla
info.Cover = raw.Cover
info.Description = raw.Description
if callback != nil {
callback(PlaylistResponsePayload{
PlaylistInfo: info,
TrackList: []AlbumTrackMetadata{},
})
}
tracks := make([]AlbumTrackMetadata, 0, len(raw.Tracks))
for _, item := range raw.Tracks {
durationMS := parseDuration(item.Duration)
@@ -1015,13 +1068,17 @@ func (c *SpotifyMetadataClient) formatPlaylistData(raw *apiPlaylistResponse) Pla
})
}
if callback != nil {
callback(tracks)
}
return PlaylistResponsePayload{
PlaylistInfo: info,
TrackList: tracks,
}
}
func (c *SpotifyMetadataClient) formatArtistDiscographyData(ctx context.Context, raw *apiArtistResponse) (*ArtistDiscographyPayload, error) {
func (c *SpotifyMetadataClient) formatArtistDiscographyData(ctx context.Context, raw *apiArtistResponse, callback MetadataCallback) (*ArtistDiscographyPayload, error) {
discType := "all"
info := ArtistInfoMetadata{
@@ -1067,7 +1124,17 @@ func (c *SpotifyMetadataClient) formatArtistDiscographyData(ctx context.Context,
Images: alb.Cover,
ExternalURL: fmt.Sprintf("https://open.spotify.com/album/%s", alb.ID),
})
}
if callback != nil {
callback(ArtistDiscographyPayload{
ArtistInfo: info,
AlbumList: albumList,
TrackList: []AlbumTrackMetadata{},
})
}
for _, alb := range raw.Discography.All {
go func(albumID string, albumName string) {
sem <- struct{}{}
@@ -1081,7 +1148,7 @@ func (c *SpotifyMetadataClient) formatArtistDiscographyData(ctx context.Context,
default:
}
albumData, err := c.fetchAlbumWithClient(ctx, sharedClient, albumID)
albumData, err := c.fetchAlbumWithClient(ctx, sharedClient, albumID, nil)
if err != nil {
fmt.Printf("Error getting tracks for album %s: %v\n", albumName, err)
resultsChan <- fetchResult{tracks: []AlbumTrackMetadata{}}
@@ -1131,6 +1198,9 @@ func (c *SpotifyMetadataClient) formatArtistDiscographyData(ctx context.Context,
IsExplicit: tr.IsExplicit,
})
}
if callback != nil {
callback(tracks)
}
resultsChan <- fetchResult{tracks: tracks}
}(alb.ID, alb.Name)
}
@@ -1290,7 +1360,7 @@ func (c *SpotifyMetadataClient) Search(ctx context.Context, query string, limit
return nil, fmt.Errorf("failed to query search: %w", err)
}
filteredData := FilterSearch(data)
filteredData := FilterSearch(data, c.Separator)
jsonData, err := json.Marshal(filteredData)
if err != nil {
@@ -1407,7 +1477,7 @@ func (c *SpotifyMetadataClient) SearchByType(ctx context.Context, query string,
return nil, fmt.Errorf("failed to query search: %w", err)
}
filteredData := FilterSearch(data)
filteredData := FilterSearch(data, c.Separator)
jsonData, err := json.Marshal(filteredData)
if err != nil {