Skip to content

Commit

Permalink
Merge pull request #6 from gleich/apple-music-cache
Browse files Browse the repository at this point in the history
Apple music cache
  • Loading branch information
gleich authored Nov 23, 2024
2 parents 5e5eada + 17e3807 commit fccc365
Show file tree
Hide file tree
Showing 8 changed files with 249 additions and 1 deletion.
3 changes: 2 additions & 1 deletion .vscode/settings.json
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"]
}
2 changes: 2 additions & 0 deletions cmd/lcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"net/http"
"time"

"github.com/gleich/lcp-v2/internal/apis/applemusic"
"github.com/gleich/lcp-v2/internal/apis/github"
"github.com/gleich/lcp-v2/internal/apis/steam"
"github.com/gleich/lcp-v2/internal/apis/strava"
Expand All @@ -29,6 +30,7 @@ func main() {
github.Setup(r)
strava.Setup(r)
steam.Setup(r)
applemusic.Setup(r)

lumber.Info("starting server")
err := http.ListenAndServe(":8000", r)
Expand Down
53 changes: 53 additions & 0 deletions internal/apis/applemusic/api.go
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
}
52 changes: 52 additions & 0 deletions internal/apis/applemusic/applemusic.go
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")
}
62 changes: 62 additions & 0 deletions internal/apis/applemusic/playlists.go
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
}
20 changes: 20 additions & 0 deletions internal/apis/applemusic/recent.go
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
}
55 changes: 55 additions & 0 deletions internal/apis/applemusic/song.go
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,
}
}
3 changes: 3 additions & 0 deletions internal/secrets/secrets.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ type Secrets struct {
SteamID string `env:"STEAM_ID"`

GitHubAccessToken string `env:"GITHUB_ACCESS_TOKEN"`

AppleMusicAppToken string `env:"APPLE_MUSIC_APP_TOKEN"`
AppleMusicUserToken string `env:"APPLE_MUSIC_USER_TOKEN"`
}

func Load() {
Expand Down

0 comments on commit fccc365

Please sign in to comment.