1package spotify
2
3import (
4	"fmt"
5	"net/url"
6	"strconv"
7	"strings"
8)
9
10// Seeds contains IDs of artists, genres and/or tracks
11// to be used as seeds for recommendations
12type Seeds struct {
13	Artists []ID
14	Tracks  []ID
15	Genres  []string
16}
17
18// count returns the total number of seeds contained in s
19func (s Seeds) count() int {
20	return len(s.Artists) + len(s.Tracks) + len(s.Genres)
21}
22
23// Recommendations contains a list of recommended tracks based on seeds
24type Recommendations struct {
25	Seeds  []RecommendationSeed `json:"seeds"`
26	Tracks []SimpleTrack        `json:"tracks"`
27}
28
29// RecommendationSeed represents a recommendation seed after
30// being processed by the Spotify API
31type RecommendationSeed struct {
32	AfterFilteringSize int    `json:"afterFilteringSize"`
33	AfterRelinkingSize int    `json:"afterRelinkingSize"`
34	Endpoint           string `json:"href"`
35	ID                 ID     `json:"id"`
36	InitialPoolSize    int    `json:"initialPoolSize"`
37	Type               string `json:"type"`
38}
39
40// MaxNumberOfSeeds allowed by Spotify for a recommendation request
41const MaxNumberOfSeeds = 5
42
43// setSeedValues sets url values into v for each seed in seeds
44func setSeedValues(seeds Seeds, v url.Values) {
45	if len(seeds.Artists) != 0 {
46		v.Set("seed_artists", strings.Join(toStringSlice(seeds.Artists), ","))
47	}
48	if len(seeds.Tracks) != 0 {
49		v.Set("seed_tracks", strings.Join(toStringSlice(seeds.Tracks), ","))
50	}
51	if len(seeds.Genres) != 0 {
52		v.Set("seed_genres", strings.Join(seeds.Genres, ","))
53	}
54}
55
56// setTrackAttributesValues sets track attributes values to the given url values
57func setTrackAttributesValues(trackAttributes *TrackAttributes, values url.Values) {
58	if trackAttributes == nil {
59		return
60	}
61	for attr, val := range trackAttributes.intAttributes {
62		values.Set(attr, strconv.Itoa(val))
63	}
64	for attr, val := range trackAttributes.floatAttributes {
65		values.Set(attr, strconv.FormatFloat(val, 'f', -1, 64))
66	}
67}
68
69// GetRecommendations returns a list of recommended tracks based on the given seeds.
70// Recommendations are generated based on the available information for a given seed entity
71// and matched against similar artists and tracks. If there is sufficient information
72// about the provided seeds, a list of tracks will be returned together with pool size details.
73// For artists and tracks that are very new or obscure
74// there might not be enough data to generate a list of tracks.
75func (c *Client) GetRecommendations(seeds Seeds, trackAttributes *TrackAttributes, opt *Options) (*Recommendations, error) {
76	v := url.Values{}
77
78	if seeds.count() == 0 {
79		return nil, fmt.Errorf("spotify: at least one seed is required")
80	}
81	if seeds.count() > MaxNumberOfSeeds {
82		return nil, fmt.Errorf("spotify: exceeded maximum of %d seeds", MaxNumberOfSeeds)
83	}
84
85	setSeedValues(seeds, v)
86	setTrackAttributesValues(trackAttributes, v)
87
88	if opt != nil {
89		if opt.Limit != nil {
90			v.Set("limit", strconv.Itoa(*opt.Limit))
91		}
92		if opt.Country != nil {
93			v.Set("market", *opt.Country)
94		}
95	}
96
97	spotifyURL := c.baseURL + "recommendations?" + v.Encode()
98
99	var recommendations Recommendations
100	err := c.get(spotifyURL, &recommendations)
101	if err != nil {
102		return nil, err
103	}
104
105	return &recommendations, err
106}
107
108// GetAvailableGenreSeeds retrieves a list of available genres seed parameter values for
109// recommendations.
110func (c *Client) GetAvailableGenreSeeds() ([]string, error) {
111	spotifyURL := c.baseURL + "recommendations/available-genre-seeds"
112
113	genreSeeds := make(map[string][]string)
114
115	err := c.get(spotifyURL, &genreSeeds)
116	if err != nil {
117		return nil, err
118	}
119
120	return genreSeeds["genres"], nil
121}
122