-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #6 from gleich/apple-music-cache
Apple music cache
- Loading branch information
Showing
8 changed files
with
249 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
{ | ||
"github-actions.workflows.pinned.workflows": [".github/workflows/deploy.yml"] | ||
"github-actions.workflows.pinned.workflows": [".github/workflows/deploy.yml"], | ||
"cSpell.words": ["applemusic"] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
package applemusic | ||
|
||
import ( | ||
"encoding/json" | ||
"fmt" | ||
"io" | ||
"net/http" | ||
|
||
"github.com/gleich/lcp-v2/internal/secrets" | ||
"github.com/gleich/lumber/v3" | ||
) | ||
|
||
func sendAPIRequest[T any](endpoint string) (T, error) { | ||
var zeroValue T // to be used as "nil" when returning errors | ||
req, err := http.NewRequest("GET", "https://api.music.apple.com/"+endpoint, nil) | ||
if err != nil { | ||
lumber.Error(err, "creating request failed") | ||
return zeroValue, err | ||
} | ||
req.Header.Set("Authorization", "Bearer "+secrets.SECRETS.AppleMusicAppToken) | ||
req.Header.Set("Music-User-Token", secrets.SECRETS.AppleMusicUserToken) | ||
|
||
resp, err := http.DefaultClient.Do(req) | ||
if err != nil { | ||
lumber.Error(err, "sending request failed") | ||
return zeroValue, err | ||
} | ||
defer resp.Body.Close() | ||
|
||
body, err := io.ReadAll(resp.Body) | ||
if err != nil { | ||
lumber.Error(err, "reading response body failed") | ||
return zeroValue, err | ||
} | ||
if resp.StatusCode != http.StatusOK { | ||
err = fmt.Errorf( | ||
"status code of %d returned from apple music API. Code of 200 expected", | ||
resp.StatusCode, | ||
) | ||
lumber.Error(err) | ||
return zeroValue, err | ||
} | ||
|
||
var data T | ||
err = json.Unmarshal(body, &data) | ||
if err != nil { | ||
lumber.Error(err, "failed to parse json") | ||
lumber.Debug(string(body)) | ||
return zeroValue, err | ||
} | ||
|
||
return data, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
package applemusic | ||
|
||
import ( | ||
"time" | ||
|
||
"github.com/gleich/lcp-v2/internal/cache" | ||
"github.com/gleich/lumber/v3" | ||
"github.com/go-chi/chi/v5" | ||
) | ||
|
||
type cacheData struct { | ||
RecentlyPlayed []song `json:"recently_played"` | ||
Playlists map[string]playlist `json:"playlists"` | ||
} | ||
|
||
func cacheUpdate() (cacheData, error) { | ||
recentlyPlayed, err := fetchRecentlyPlayed() | ||
if err != nil { | ||
return cacheData{}, err | ||
} | ||
|
||
playlistsIDs := []string{ | ||
"p.LV0PXNoCl0EpDLW", // DIVORCED DAD | ||
"p.qQXLxPLtA75zg8e", // HIGHSCHOOL 1989 | ||
"p.LV0PX3EIl0EpDLW", // jazz | ||
} | ||
playlists := map[string]playlist{} | ||
for _, id := range playlistsIDs { | ||
playlistData, err := fetchPlaylist(id) | ||
if err != nil { | ||
return cacheData{}, err | ||
} | ||
playlists[id] = playlistData | ||
} | ||
|
||
return cacheData{ | ||
RecentlyPlayed: recentlyPlayed, | ||
Playlists: playlists, | ||
}, nil | ||
} | ||
|
||
func Setup(router *chi.Mux) { | ||
data, err := cacheUpdate() | ||
if err != nil { | ||
lumber.Fatal(err, "initial fetch of cache data failed") | ||
} | ||
|
||
applemusicCache := cache.NewCache("applemusic", data) | ||
router.Get("/applemusic/cache", applemusicCache.ServeHTTP()) | ||
go applemusicCache.StartPeriodicUpdate(cacheUpdate, 1*time.Minute) | ||
lumber.Done("setup apple music cache") | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
package applemusic | ||
|
||
import ( | ||
"path" | ||
"time" | ||
|
||
"github.com/gleich/lumber/v3" | ||
) | ||
|
||
type playlist struct { | ||
Name string `json:"name"` | ||
Tracks []song `json:"tracks"` | ||
LastModified time.Time `json:"last_modified"` | ||
} | ||
|
||
type playlistTracksResponse struct { | ||
Next string `json:"next"` | ||
Data []songResponse `json:"data"` | ||
} | ||
|
||
type playlistResponse struct { | ||
Data []struct { | ||
Attributes struct { | ||
LastModifiedDate time.Time `json:"lastModifiedDate"` | ||
Name string `json:"name"` | ||
} `json:"attributes"` | ||
} `json:"data"` | ||
} | ||
|
||
func fetchPlaylist(id string) (playlist, error) { | ||
playlistData, err := sendAPIRequest[playlistResponse](path.Join("v1/me/library/playlists/", id)) | ||
if err != nil { | ||
lumber.Error(err, "failed to fetch playlist for", id) | ||
return playlist{}, err | ||
} | ||
|
||
var totalResponseData []songResponse | ||
trackData, err := sendAPIRequest[playlistTracksResponse]( | ||
path.Join("v1/me/library/playlists/", id, "tracks"), | ||
) | ||
if err != nil { | ||
lumber.Error(err, "failed to get tracks for playlist with id of", id) | ||
} | ||
totalResponseData = append(totalResponseData, trackData.Data...) | ||
for trackData.Next != "" { | ||
trackData, err = sendAPIRequest[playlistTracksResponse](trackData.Next) | ||
if err != nil { | ||
lumber.Error(err, "failed to paginate through tracks for playlist with id of", id) | ||
} | ||
} | ||
|
||
var tracks []song | ||
for _, t := range totalResponseData { | ||
tracks = append(tracks, songFromSongResponse(t)) | ||
} | ||
|
||
return playlist{ | ||
Name: playlistData.Data[0].Attributes.Name, | ||
LastModified: playlistData.Data[0].Attributes.LastModifiedDate, | ||
Tracks: tracks, | ||
}, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
package applemusic | ||
|
||
type recentlyPlayedResponse struct { | ||
Data []songResponse `json:"data"` | ||
} | ||
|
||
func fetchRecentlyPlayed() ([]song, error) { | ||
response, err := sendAPIRequest[recentlyPlayedResponse]( | ||
"v1/me/recent/played/tracks", | ||
) | ||
if err != nil { | ||
return []song{}, err | ||
} | ||
|
||
var songs []song | ||
for _, s := range response.Data { | ||
songs = append(songs, songFromSongResponse(s)) | ||
} | ||
return songs, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
package applemusic | ||
|
||
import ( | ||
"strconv" | ||
"strings" | ||
) | ||
|
||
type song struct { | ||
Track string `json:"track"` | ||
Artist string `json:"artist"` | ||
Album string `json:"album"` | ||
Genres []string `json:"genres"` | ||
ReleaseDate string `json:"release_date"` | ||
DurationInMillis int `json:"duration_in_millis"` | ||
AlbumArtURL string `json:"album_art_url"` | ||
URL string `json:"url"` | ||
} | ||
|
||
type songResponse struct { | ||
ID string `json:"id"` | ||
Type string `json:"type"` | ||
Href string `json:"href"` | ||
Attributes struct { | ||
AlbumName string `json:"albumName"` | ||
GenreNames []string `json:"genreNames"` | ||
TrackNumber int `json:"trackNumber"` | ||
ReleaseDate string `json:"releaseDate"` | ||
DurationInMillis int `json:"durationInMillis"` | ||
Artwork struct { | ||
Width int `json:"width"` | ||
Height int `json:"height"` | ||
URL string `json:"url"` | ||
} `json:"artwork"` | ||
URL string `json:"url"` | ||
Name string `json:"name"` | ||
ArtistName string `json:"artistName"` | ||
} `json:"attributes"` | ||
} | ||
|
||
func songFromSongResponse(s songResponse) song { | ||
return song{ | ||
Track: s.Attributes.Name, | ||
Artist: s.Attributes.ArtistName, | ||
Album: s.Attributes.AlbumName, | ||
Genres: s.Attributes.GenreNames, | ||
ReleaseDate: s.Attributes.ReleaseDate, | ||
DurationInMillis: s.Attributes.DurationInMillis, | ||
AlbumArtURL: strings.ReplaceAll(strings.ReplaceAll( | ||
s.Attributes.Artwork.URL, | ||
"{w}", | ||
strconv.Itoa(s.Attributes.Artwork.Width), | ||
), "{h}", strconv.Itoa(s.Attributes.Artwork.Height)), | ||
URL: s.Attributes.URL, | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters