diff --git a/Dockerfile b/Dockerfile index d2c989f..9a0bf10 100644 --- a/Dockerfile +++ b/Dockerfile @@ -26,7 +26,9 @@ COPY web/tv/*.go web/tv/ COPY web/tv/*.html web/tv/ COPY web/settings/*.go web/settings/ COPY web/settings/*.html web/settings/ -COPY web/server.go web/index.html web/static/ /web/ +COPY web/server.go web/ +COPY web/index.html web/ +COPY web/static/ web/static/ # Build RUN CGO_ENABLED=0 GOOS=linux go build -o /plex-lookup diff --git a/TODO b/TODO index 09cc378..ebffa9b 100644 --- a/TODO +++ b/TODO @@ -2,13 +2,13 @@ ## features -- similar artist search for music - ## bugs +- allow amazon tv search for indivdual series - allow amazon tv search for newer series - update movies to use tv like search -- allow amazon tv search for indivdual series +- update movies, remove the plex resolution filter add plex resolution as a column +- music, a-ha/ash doesnt match as an artist why ? ## done @@ -28,3 +28,5 @@ - remove dead fields from the tv data types - remove links to tv series we already have in plex. eg dont show adventure time series 1 and 2 ? - write a function to calculate plex dates +- similar artist search for music +- parallel requests for spotify search diff --git a/musicbrainz/musicbrainz.go b/musicbrainz/musicbrainz.go index afd40d8..09d14ca 100644 --- a/musicbrainz/musicbrainz.go +++ b/musicbrainz/musicbrainz.go @@ -68,7 +68,7 @@ func SearchMusicBrainzArtist(plexArtist *types.PlexMusicArtist) (artist types.Se if resp.Artists[i].Name != plexArtist.Name { continue } - found := types.MusicSearchResult{ + found := types.MusicArtistSearchResult{ Name: resp.Artists[i].Name, ID: fmt.Sprintf("%v", resp.Artists[i].ID), } @@ -85,7 +85,7 @@ func SearchMusicBrainzArtist(plexArtist *types.PlexMusicArtist) (artist types.Se return artist, err } -func SearchMusicBrainzAlbums(artistID string) (albums []types.MusicSearchAlbumResult, err error) { +func SearchMusicBrainzAlbums(artistID string) (albums []types.MusicAlbumSearchResult, err error) { client, err := gomusicbrainz.NewWS2Client( musicBrainzURL, agent, agentVersion, "") @@ -106,7 +106,7 @@ func SearchMusicBrainzAlbums(artistID string) (albums []types.MusicSearchAlbumRe for i := range resp.ReleaseGroups { if resp.ReleaseGroups[i].Type == "Album" { year := resp.ReleaseGroups[i].FirstReleaseDate.Year() - albums = append(albums, types.MusicSearchAlbumResult{ + albums = append(albums, types.MusicAlbumSearchResult{ Title: resp.ReleaseGroups[i].Title, ID: fmt.Sprintf("%v", resp.ReleaseGroups[i].ID), Year: fmt.Sprintf("%v", year), diff --git a/musicbrainz/musicbrainz_test.go b/musicbrainz/musicbrainz_test.go index 71a18fa..89867f6 100644 --- a/musicbrainz/musicbrainz_test.go +++ b/musicbrainz/musicbrainz_test.go @@ -18,12 +18,12 @@ func TestSearchMusicBrainzArtist(t *testing.T) { args: &types.PlexMusicArtist{Name: "The Beatles"}, wantArtist: types.SearchResults{ SearchURL: "https://musicbrainz.org/artist/b10bbbfc-cf9e-42e0-be17-e2c3e1d2600d", - MusicSearchResults: []types.MusicSearchResult{ + MusicSearchResults: []types.MusicArtistSearchResult{ { Name: "The Beatles", ID: "b10bbbfc-cf9e-42e0-be17-e2c3e1d2600d", - Albums: make([]types.MusicSearchAlbumResult, 16), + Albums: make([]types.MusicAlbumSearchResult, 16), }, }, }, @@ -34,11 +34,11 @@ func TestSearchMusicBrainzArtist(t *testing.T) { args: &types.PlexMusicArtist{Name: "AC/DC"}, wantArtist: types.SearchResults{ SearchURL: "https://musicbrainz.org/artist/66c662b6-6e2f-4930-8610-912e24c63ed1", - MusicSearchResults: []types.MusicSearchResult{ + MusicSearchResults: []types.MusicArtistSearchResult{ { Name: "AC/DC", ID: "66c662b6-6e2f-4930-8610-912e24c63ed1", - Albums: make([]types.MusicSearchAlbumResult, 18), + Albums: make([]types.MusicAlbumSearchResult, 17), }, }, }, diff --git a/spotify/spotify.go b/spotify/spotify.go index acd7fb4..c055cf5 100644 --- a/spotify/spotify.go +++ b/spotify/spotify.go @@ -7,6 +7,7 @@ import ( "io" "net/http" "net/url" + "strconv" "strings" "time" @@ -93,44 +94,72 @@ type AlbumsResponse struct { Total int64 `json:"total"` } -func SearchSpotifyArtist(plexArtist *types.PlexMusicArtist, clientID, clientSecret string) (artist types.SearchResults, err error) { - artist.PlexMusicArtist = *plexArtist +type SimilarArtistsResponse struct { + Artists []struct { + Name string `json:"name"` + ID string `json:"id"` + } +} + +func SearchSpotifyArtist(plexArtist *types.PlexMusicArtist, clientID, clientSecret string, ch chan<- *types.SearchResults) { + // context with a timeout of 30 seconds + ctx, cancel := context.WithTimeout(context.Background(), time.Second*time.Duration(lookupTimeout)) + defer cancel() + searchResults := types.SearchResults{} + searchResults.PlexMusicArtist = *plexArtist // get oauth token - if oauthToken == "" { - oauthToken, err = spotifyOauthToken(context.Background(), clientID, clientSecret) - if err != nil { - return artist, fmt.Errorf("SearchSpotifyArtist: unable to get oauth token: %s", err.Error()) - } + err := SpotifyOauthToken(ctx, clientID, clientSecret) + if err != nil { + fmt.Printf("SearchSpotifyArtist: unable to get oauth token: %s\n", err.Error()) + ch <- &searchResults + return } urlEncodedArtist := url.QueryEscape(plexArtist.Name) artistURL := fmt.Sprintf("%s/search?q=%s&type=artist&limit=10", spotifyAPIURL, urlEncodedArtist) client := &http.Client{ Timeout: time.Second * lookupTimeout, } - req, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, artistURL, http.NoBody) + req, _ := http.NewRequestWithContext(ctx, http.MethodGet, artistURL, http.NoBody) bearer := fmt.Sprintf("Bearer %s", oauthToken) req.Header.Add("Authorization", bearer) - response, err := client.Do(req) - if err != nil { - return artist, fmt.Errorf("lookupArtist: get failed from spotify: %s", err.Error()) - } - if response.StatusCode == http.StatusTooManyRequests { - return artist, fmt.Errorf("lookupArtist: rate limited by spotify") + var response *http.Response + for { + response, err = client.Do(req) + if err != nil { + response.Body.Close() + fmt.Printf("lookupArtist: get failed from spotify: %s\n", err.Error()) + ch <- &searchResults + return + } + if response.StatusCode == http.StatusTooManyRequests { + // rate limited + wait := response.Header.Get("Retry-After") + waitSeconds, _ := strconv.Atoi(wait) + time.Sleep(time.Duration(waitSeconds) * time.Second) + continue + } + if response.StatusCode == http.StatusOK { + break + } } defer response.Body.Close() body, err := io.ReadAll(response.Body) if err != nil { - return artist, fmt.Errorf("lookupArtist: unable to parse response from spotify: %s", err.Error()) + fmt.Printf("lookupArtist: unable to read response from spotify: %s\n", err.Error()) + ch <- &searchResults + return } var artistResponse ArtistResponse jsonErr := json.Unmarshal(body, &artistResponse) if jsonErr != nil { - return artist, fmt.Errorf("lookupArtist: unable to parse response from spotify: %s", jsonErr.Error()) + fmt.Printf("lookupArtist: unable to parse response from spotify: %s\n", jsonErr.Error()) + ch <- &searchResults + return } for i := range artistResponse.Artists.Items { if artistStringMatcher(plexArtist.Name, artistResponse.Artists.Items[i].Name) { // only get the first match - artist.MusicSearchResults = append(artist.MusicSearchResults, types.MusicSearchResult{ + searchResults.MusicSearchResults = append(searchResults.MusicSearchResults, types.MusicArtistSearchResult{ Name: artistResponse.Artists.Items[i].Name, ID: artistResponse.Artists.Items[i].ID, URL: artistResponse.Artists.Items[i].ExternalUrls.Spotify, @@ -139,44 +168,67 @@ func SearchSpotifyArtist(plexArtist *types.PlexMusicArtist, clientID, clientSecr break } } - if len(artist.MusicSearchResults) == 0 { - return artist, err - } - // get the albums - artist.MusicSearchResults[0].Albums, err = SearchSpotifyAlbums(artist.MusicSearchResults[0].ID, clientID, clientSecret) - return artist, nil + ch <- &searchResults } -func SearchSpotifyAlbums(artistID, clientID, clientSecret string) (albums []types.MusicSearchAlbumResult, err error) { - if oauthToken == "" { - oauthToken, err = spotifyOauthToken(context.Background(), clientID, clientSecret) - if err != nil { - return albums, fmt.Errorf("SearchSpotifyAlbums: unable to get oauth token: %s", err.Error()) - } +func SearchSpotifyAlbum(m *types.SearchResults, clientID, clientSecret string, ch chan<- *types.SearchResults) { + // get oauth token + err := SpotifyOauthToken(context.Background(), clientID, clientSecret) + if err != nil { + fmt.Printf("SearchSpotifyAlbums: unable to get oauth token: %s\n", err.Error()) + ch <- m + return + } + if len(m.MusicSearchResults) == 0 { + // no artist found for the plex artist + fmt.Printf("SearchSpotifyAlbums: no artist found for %s\n", m.PlexMusicArtist.Name) + ch <- m + return } - albumURL := fmt.Sprintf("%s/artists/%s/albums?include_groups=album&limit=50&", spotifyAPIURL, artistID) + albumURL := fmt.Sprintf("%s/artists/%s/albums?include_groups=album&limit=50&", spotifyAPIURL, m.MusicSearchResults[0].ID) client := &http.Client{ Timeout: time.Second * lookupTimeout, } req, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, albumURL, http.NoBody) bearer := fmt.Sprintf("Bearer %s", oauthToken) req.Header.Add("Authorization", bearer) - response, err := client.Do(req) - if err != nil { - return albums, fmt.Errorf("lookupArtistAlbums: get failed from spotify: %s", err.Error()) + var response *http.Response + for { + response, err = client.Do(req) + if err != nil { + response.Body.Close() + fmt.Printf("lookupArtistAlbums: get failed from spotify: %s\n", err.Error()) + ch <- m + return + } + if response.StatusCode == http.StatusTooManyRequests { + wait := response.Header.Get("Retry-After") + waitSeconds, _ := strconv.Atoi(wait) + if waitSeconds > lookupTimeout { + fmt.Printf("lookupArtistAlbums: rate limited for %d seconds\n", waitSeconds) + } + time.Sleep(time.Duration(waitSeconds) * time.Second) + continue + } + if response.StatusCode == http.StatusOK { + break + } } defer response.Body.Close() body, err := io.ReadAll(response.Body) if err != nil { - return albums, fmt.Errorf("lookupArtistAlbums: unable to parse response from spotify: %s", err.Error()) + fmt.Printf("lookupArtistAlbums: unable to parse response from spotify: %s\n", err.Error()) + ch <- m + return } var albumsResponse AlbumsResponse _ = json.Unmarshal(body, &albumsResponse) + albums := make([]types.MusicAlbumSearchResult, 0) for i := range albumsResponse.Items { // convert "2022-06-03" to "2022" year := strings.Split(albumsResponse.Items[i].ReleaseDate, "-")[0] - albums = append(albums, types.MusicSearchAlbumResult{ + albums = append(albums, types.MusicAlbumSearchResult{ Title: albumsResponse.Items[i].Name, ID: albumsResponse.Items[i].ID, URL: albumsResponse.Items[i].ExternalUrls.Spotify, @@ -184,12 +236,81 @@ func SearchSpotifyAlbums(artistID, clientID, clientSecret string) (albums []type }) } - return albums, err + m.MusicSearchResults[0].Albums = albums + ch <- m +} + +func SearchSpotifySimilarArtist(m *types.SearchResults, clientID, clientSecret string, ch chan<- SimilarArtistsResponse) { + err := SpotifyOauthToken(context.Background(), clientID, clientSecret) + if err != nil { + fmt.Printf("SearchSpotifySimilarArtist: unable to get oauth token: %s\n", err.Error()) + ch <- SimilarArtistsResponse{} + return + } + if len(m.MusicSearchResults) == 0 { + // no artist found for the plex artist + fmt.Printf("SearchSpotifySimilarArtist: no artist found for %s\n", m.PlexMusicArtist.Name) + ch <- SimilarArtistsResponse{} + return + } + similarArtistURL := fmt.Sprintf("%s/artists/%s/related-artists", spotifyAPIURL, m.MusicSearchResults[0].ID) + client := &http.Client{ + Timeout: time.Second * lookupTimeout, + } + req, httpErr := http.NewRequestWithContext(context.Background(), http.MethodGet, similarArtistURL, http.NoBody) + if httpErr != nil { + fmt.Printf("SearchSpotifySimilarArtist: get failed from spotify: %s\n", httpErr.Error()) + ch <- SimilarArtistsResponse{} + return + } + bearer := fmt.Sprintf("Bearer %s", oauthToken) + req.Header.Add("Authorization", bearer) + var response *http.Response + for { + response, err = client.Do(req) + if err != nil { + response.Body.Close() + fmt.Printf("SearchSpotifySimilarArtist: get failed from spotify: %s\n", err.Error()) + ch <- SimilarArtistsResponse{} + return + } + if response.StatusCode == http.StatusTooManyRequests { + wait := response.Header.Get("Retry-After") + waitSeconds, _ := strconv.Atoi(wait) + if waitSeconds > lookupTimeout { + fmt.Printf("SearchSpotifySimilarArtist: rate limited for %d seconds\n", waitSeconds) + } + time.Sleep(time.Duration(waitSeconds) * time.Second) + continue + } + if response.StatusCode == http.StatusOK { + break + } + } + + defer response.Body.Close() + body, err := io.ReadAll(response.Body) + if err != nil { + fmt.Printf("SearchSpotifySimilarArtist: unable to parse response from spotify: %s\n", err.Error()) + ch <- SimilarArtistsResponse{} + return + } + var similarArtistsResponse SimilarArtistsResponse + jsonErr := json.Unmarshal(body, &similarArtistsResponse) + if jsonErr != nil { + fmt.Printf("SearchSpotifySimilarArtist: unable to unmarshal response from spotify: %s\n", jsonErr.Error()) + ch <- SimilarArtistsResponse{} + return + } + ch <- similarArtistsResponse } // function that gets an oauth token from spotify from the client id and secret -func spotifyOauthToken(ctx context.Context, clientID, clientSecret string) (oauth string, err error) { +func SpotifyOauthToken(ctx context.Context, clientID, clientSecret string) (err error) { // get oauth token + if oauthToken != "" { + return nil + } oauthURL := "https://accounts.spotify.com/api/token" client := &http.Client{ Timeout: time.Second * lookupTimeout, @@ -202,12 +323,12 @@ func spotifyOauthToken(ctx context.Context, clientID, clientSecret string) (oaut req.Header.Add("Content-Type", "application/x-www-form-urlencoded") response, err := client.Do(req) if err != nil { - return "", fmt.Errorf("spotifyOauthToken: get failed from spotify: %s", err.Error()) + return fmt.Errorf("spotifyOauthToken: get failed from spotify: %s", err.Error()) } defer response.Body.Close() body, err := io.ReadAll(response.Body) if err != nil { - return "", fmt.Errorf("spotifyOauthToken: unable to read response from spotify: %s", err.Error()) + return fmt.Errorf("spotifyOauthToken: unable to read response from spotify: %s", err.Error()) } var oauthResponse struct { AccessToken string `json:"access_token"` @@ -215,9 +336,10 @@ func spotifyOauthToken(ctx context.Context, clientID, clientSecret string) (oaut } err = json.Unmarshal(body, &oauthResponse) if err != nil { - return "", fmt.Errorf("getOauthToken: unable to parse response from spotify: %s", err.Error()) + return fmt.Errorf("getOauthToken: unable to parse response from spotify: %s", err.Error()) } - return oauthResponse.AccessToken, nil + oauthToken = oauthResponse.AccessToken + return nil } func artistStringMatcher(dbName, webName string) bool { diff --git a/spotify/spotify_test.go b/spotify/spotify_test.go index 8e6e5fe..a3647c1 100644 --- a/spotify/spotify_test.go +++ b/spotify/spotify_test.go @@ -16,84 +16,46 @@ func TestSearchSpotifyArtist(t *testing.T) { if spotifyClientID == "" || spotifyClientSecret == "" { t.Skip("SPOTIFY_CLIENT_ID and SPOTIFY_CLIENT_SECRET not set") } - tests := []struct { - name string - args *types.PlexMusicArtist - wantArtist types.SearchResults - wantErr bool - }{ - { - name: "artist exists", - args: &types.PlexMusicArtist{Name: "The Beatles"}, - wantArtist: types.SearchResults{ - SearchURL: "https://open.spotify.com/artist/711MCceyCBcFnzjGY4Q7Un", - MusicSearchResults: []types.MusicSearchResult{ - { - Name: "The Beatles", - ID: "b10bbbfc-cf9e-42e0-be17-e2c3e1d2600d", - Albums: make([]types.MusicSearchAlbumResult, 27), - }, - }, - }, - wantErr: false, - }, - { - name: "artist has special characters", - args: &types.PlexMusicArtist{Name: "AC/DC"}, - wantArtist: types.SearchResults{ - SearchURL: "https://open.spotify.com/artist/711MCceyCBcFnzjGY4Q7Un", - MusicSearchResults: []types.MusicSearchResult{ - { - Name: "AC/DC", - ID: "711MCceyCBcFnzjGY4Q7Un", - Albums: make([]types.MusicSearchAlbumResult, 21), - }, - }, - }, - wantErr: false, - }, + + plexArtist := &types.PlexMusicArtist{Name: "The Beatles"} + + ch := make(chan *types.SearchResults, 1) + SearchSpotifyArtist(plexArtist, spotifyClientID, spotifyClientSecret, ch) + + got := <-ch + if len(got.MusicSearchResults) != 1 { + t.Errorf("SearchSpotifyArtist() returned %d results, expected 1", len(got.MusicSearchResults)) } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - gotArtist, err := SearchSpotifyArtist(tt.args, spotifyClientID, spotifyClientSecret) - if (err != nil) != tt.wantErr { - t.Errorf("SearchSpotifyArtist() error = %v, wantErr %v", err, tt.wantErr) - return - } - if gotArtist.MusicSearchResults[0].Name != tt.wantArtist.MusicSearchResults[0].Name { - t.Errorf("SearchSpotifyArtist() Name = %v, want %v", gotArtist, tt.wantArtist) - } - if len(gotArtist.MusicSearchResults[0].Albums) != len(tt.wantArtist.MusicSearchResults[0].Albums) { - t.Errorf("SearchSpotifyArtist() Albums size = %v, want %v", - len(gotArtist.MusicSearchResults[0].Albums), len(tt.wantArtist.MusicSearchResults[0].Albums)) - } - }) + + expectedArtist := types.MusicArtistSearchResult{ + Name: "The Beatles", + ID: "3WrFJ7ztbogyGnTHbHJFl2", + URL: "https://open.spotify.com/artist/3WrFJ7ztbogyGnTHbHJFl2", } -} -// debug test for individual artists -func TestSearchSpotifyArtistDebug(t *testing.T) { - if spotifyClientID == "" || spotifyClientSecret == "" { - t.Skip("SPOTIFY_CLIENT_ID and SPOTIFY_CLIENT_SECRET not set") + if got.MusicSearchResults[0].Name != expectedArtist.Name { + t.Errorf("SearchSpotifyArtist() returned %s, expected %s", got.MusicSearchResults[0].Name, expectedArtist.Name) + } + if got.MusicSearchResults[0].ID != expectedArtist.ID { + t.Errorf("SearchSpotifyArtist() returned %s, expected %s", got.MusicSearchResults[0].ID, expectedArtist.ID) } - artist := &types.PlexMusicArtist{Name: "Angel Olsen"} - artistSearchResult, err := SearchSpotifyArtist(artist, spotifyClientID, spotifyClientSecret) - if err != nil { - t.Errorf("SearchSpotifyArtist() error = %v", err) + if got.MusicSearchResults[0].URL != expectedArtist.URL { + t.Errorf("SearchSpotifyArtist() returned %s, expected %s", got.MusicSearchResults[0].URL, expectedArtist.URL) } - t.Logf("SearchSpotifyArtist() = %v", artistSearchResult) } -func TestSearchSpotifyAlbumsDebug(t *testing.T) { +// debug test for individual artists +func TestSearchSpotifyArtistDebug(t *testing.T) { if spotifyClientID == "" || spotifyClientSecret == "" { t.Skip("SPOTIFY_CLIENT_ID and SPOTIFY_CLIENT_SECRET not set") } - artistID := "6mKqFxGMS5TGDZI3XkT5Rt" - artistSearchResult, err := SearchSpotifyAlbums(artistID, spotifyClientID, spotifyClientSecret) - if err != nil { - t.Errorf("SearchSpotifyAlbum() error = %v", err) - } - t.Logf("SearchSpotifyAlbum() = %v", artistSearchResult) + plexArtist := &types.PlexMusicArtist{Name: "Angel Olsen"} + + ch := make(chan *types.SearchResults, 1) + SearchSpotifyArtist(plexArtist, spotifyClientID, spotifyClientSecret, ch) + + got := <-ch + t.Logf("SearchSpotifyArtist() = %+v", got) } func TestSearchSpotifyAlbums(t *testing.T) { @@ -101,7 +63,7 @@ func TestSearchSpotifyAlbums(t *testing.T) { t.Skip("SPOTIFY_CLIENT_ID and SPOTIFY_CLIENT_SECRET not set") } type args struct { - artistID string + m *types.SearchResults } tests := []struct { name string @@ -111,20 +73,69 @@ func TestSearchSpotifyAlbums(t *testing.T) { }{ { name: "albums exist", - args: args{artistID: "711MCceyCBcFnzjGY4Q7Un"}, + args: args{m: &types.SearchResults{MusicSearchResults: []types.MusicArtistSearchResult{{ID: "711MCceyCBcFnzjGY4Q7Un"}}}}, albumCount: 21, wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - gotAlbums, err := SearchSpotifyAlbums(tt.args.artistID, spotifyClientID, spotifyClientSecret) - if (err != nil) != tt.wantErr { - t.Errorf("SearchSpotifyAlbums() error = %v, wantErr %v", err, tt.wantErr) - return + ch := make(chan *types.SearchResults, 1) + SearchSpotifyAlbum(tt.args.m, spotifyClientID, spotifyClientSecret, ch) + got := <-ch + + if len(got.MusicSearchResults[0].Albums) != tt.albumCount { + t.Errorf("SearchSpotifyAlbums() = %v, want %v", len(got.MusicSearchResults[0].Albums), tt.albumCount) } - if len(gotAlbums) != tt.albumCount { - t.Errorf("SearchSpotifyAlbums() = %v, want %v", len(gotAlbums), tt.albumCount) + }) + } +} + +func TestSearchSpotifyAlbumsDebug(t *testing.T) { + if spotifyClientID == "" || spotifyClientSecret == "" { + t.Skip("SPOTIFY_CLIENT_ID and SPOTIFY_CLIENT_SECRET not set") + } + want := types.SearchResults{ + MusicSearchResults: []types.MusicArtistSearchResult{ + { + Name: "", + ID: "16GcWuvvybAoaHr0NqT8Eh", + URL: "", + }, + }, + } + ch := make(chan *types.SearchResults, 1) + SearchSpotifyAlbum(&want, spotifyClientID, spotifyClientSecret, ch) + got := <-ch + + t.Logf("SearchSpotifyAlbum() = %v", got.MusicSearchResults) +} + +func TestSpotifyLookupSimilarArtists(t *testing.T) { + if spotifyClientID == "" || spotifyClientSecret == "" { + t.Skip("SPOTIFY_CLIENT_ID and SPOTIFY_CLIENT_SECRET not set") + } + tests := []struct { + name string + searchResult types.SearchResults + wantErr bool + wantLength int + }{ + { + name: "similar artists exist", + searchResult: types.SearchResults{MusicSearchResults: []types.MusicArtistSearchResult{{ID: "711MCceyCBcFnzjGY4Q7Un"}}}, + wantErr: false, + wantLength: 20, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ch := make(chan SimilarArtistsResponse, 1) + searchResult := tt.searchResult // Create a local variable to avoid implicit memory aliasing + SearchSpotifySimilarArtist(&searchResult, spotifyClientID, spotifyClientSecret, ch) + got := <-ch + if len(got.Artists) != tt.wantLength { + t.Errorf("SpotifyLookupSimilarArtists() = %v, want %v", len(got.Artists), tt.wantLength) } }) } diff --git a/types/types.go b/types/types.go index 69fa540..b8b2306 100644 --- a/types/types.go +++ b/types/types.go @@ -27,7 +27,7 @@ type SearchResults struct { MatchesBluray int MovieSearchResults []MovieSearchResult TVSearchResults []TVSearchResult - MusicSearchResults []MusicSearchResult + MusicSearchResults []MusicArtistSearchResult } type Configuration struct { @@ -123,20 +123,28 @@ type PlexMusicAlbum struct { DateAdded time.Time } -type MusicSearchResult struct { - Name string - ID string - URL string - Albums []MusicSearchAlbumResult +type MusicArtistSearchResult struct { + Name string + ID string + URL string + OwnedAlbums int + Albums []MusicAlbumSearchResult } -type MusicSearchAlbumResult struct { +type MusicAlbumSearchResult struct { Title string ID string URL string Year string } +type MusicSimilarArtistResult struct { + Name string + URL string + Owned bool + SimilarityCount int +} + // ============================================================================================================== type PlexLibrary struct { Title string diff --git a/web/music/music.go b/web/music/music.go index 5fd0c43..38543e8 100644 --- a/web/music/music.go +++ b/web/music/music.go @@ -22,9 +22,14 @@ var ( numberOfArtistsProcessed int = 0 artistsJobRunning bool = false totalArtists int = 0 - plexMusic []types.PlexMusicArtist - artistsSearchResults []types.SearchResults - albumReleaseYearCutoff int = 5 + + plexMusic []types.PlexMusicArtist + artistsSearchResults []types.SearchResults + similarArtistsResults map[string]types.MusicSimilarArtistResult + lookup string + lookupType string + spotifyThreads int = 2 + albumReleaseYearCutoff int = 5 ) type MusicConfig struct { @@ -42,8 +47,8 @@ func MusicHandler(w http.ResponseWriter, _ *http.Request) { // nolint: lll, nolintlint func (c MusicConfig) ProcessHTML(w http.ResponseWriter, r *http.Request) { - lookup := r.FormValue("lookup") - lookupType := r.FormValue("lookuptype") + lookup = r.FormValue("lookup") + lookupType = r.FormValue("lookuptype") // only get the artists from plex once if len(plexMusic) == 0 { plexMusic = plex.GetPlexMusicArtists(c.Config.PlexIP, c.Config.PlexMusicLibraryID, c.Config.PlexToken) @@ -61,35 +66,41 @@ func (c MusicConfig) ProcessHTML(w http.ResponseWriter, r *http.Request) { case "musicbrainz": go func() { startTime := time.Now() + totalArtists = 49 for i := 0; i < 50; i++ { fmt.Print(".") searchResult, _ = musicbrainz.SearchMusicBrainzArtist(&plexMusic[i]) artistsSearchResults = append(artistsSearchResults, searchResult) numberOfArtistsProcessed = i } + totalArtists = numberOfArtistsProcessed artistsJobRunning = false fmt.Printf("Processed %d artists in %v\n", numberOfArtistsProcessed, time.Since(startTime)) }() default: // search spotify go func() { - startTime := time.Now() - for i := range plexMusic { - fmt.Print(".") - searchResult, _ = spotify.SearchSpotifyArtist(&plexMusic[i], c.Config.SpotifyClientID, c.Config.SpotifyClientSecret) - artistsSearchResults = append(artistsSearchResults, searchResult) - numberOfArtistsProcessed = i - } + getSpotifyArtistsInParallel(c.Config.SpotifyClientID, c.Config.SpotifyClientSecret) + numberOfArtistsProcessed = 0 + getSpotifyAlbumsInParallel(c.Config.SpotifyClientID, c.Config.SpotifyClientSecret) + totalArtists = numberOfArtistsProcessed artistsJobRunning = false - fmt.Printf("Processed %d artists in %v\n", numberOfArtistsProcessed, time.Since(startTime)) }() } } else { - fmt.Println("Processing new artists") - fmt.Printf("running %v\n", artistsJobRunning) - time.Sleep(1 * time.Second) - artistsJobRunning = false - fmt.Println(searchResult) + switch lookup { + case "spotify": + go func() { + getSpotifyArtistsInParallel(c.Config.SpotifyClientID, c.Config.SpotifyClientSecret) + numberOfArtistsProcessed = 0 + getSpotifySimilarArtistsInParallel(c.Config.SpotifyClientID, c.Config.SpotifyClientSecret) + totalArtists = numberOfArtistsProcessed + artistsJobRunning = false + }() + fmt.Println("Searching Spotify for similar artists") + default: + fmt.Fprintf(w, `