diff --git a/entity.go b/entity.go index f0c6160..e514173 100644 --- a/entity.go +++ b/entity.go @@ -15,3 +15,7 @@ type Entity struct { Provider *Provider Type EntityType } + +func entityFullTitle(artist, title string) string { + return artist + " – " + title +} diff --git a/internal/yandex/client.go b/internal/yandex/client.go index 189b207..d91d156 100644 --- a/internal/yandex/client.go +++ b/internal/yandex/client.go @@ -20,9 +20,9 @@ var ( type Client interface { FetchTrack(ctx context.Context, id string) (*Track, error) - SearchTrack(ctx context.Context, artistName, trackName string) (*Track, error) + SearchTrack(ctx context.Context, query string) (*Track, error) FetchAlbum(ctx context.Context, id string) (*Album, error) - SearchAlbum(ctx context.Context, artistName, albumName string) (*Album, error) + SearchAlbum(ctx context.Context, query string) (*Album, error) } type HTTPClient struct { @@ -87,11 +87,11 @@ func (c *HTTPClient) FetchTrack(ctx context.Context, trackID string) (*Track, er return &tr.Result[0], nil } -func (c *HTTPClient) SearchTrack(ctx context.Context, artistName, trackName string) (*Track, error) { +func (c *HTTPClient) SearchTrack(ctx context.Context, query string) (*Track, error) { body, err := c.getAPI(ctx, "/search", url.Values{ "type": []string{"track"}, "page": []string{"0"}, - "text": []string{fmt.Sprintf("%s – %s", artistName, trackName)}, + "text": []string{query}, }) if err != nil { return nil, fmt.Errorf("failed to get api: %s", err) @@ -127,11 +127,11 @@ func (c *HTTPClient) FetchAlbum(ctx context.Context, albumID string) (*Album, er return ar.Result, nil } -func (c *HTTPClient) SearchAlbum(ctx context.Context, artistName, albumName string) (*Album, error) { +func (c *HTTPClient) SearchAlbum(ctx context.Context, query string) (*Album, error) { body, err := c.getAPI(ctx, "/search", url.Values{ "type": []string{"album"}, "page": []string{"0"}, - "text": []string{fmt.Sprintf("%s – %s", artistName, albumName)}, + "text": []string{query}, }) if err != nil { return nil, fmt.Errorf("failed to get api: %s", err) diff --git a/internal/yandex/client_test.go b/internal/yandex/client_test.go index 246303a..62013a2 100644 --- a/internal/yandex/client_test.go +++ b/internal/yandex/client_test.go @@ -163,16 +163,14 @@ func TestClient_FetchAlbum(t *testing.T) { func TestClient_SearchTrack(t *testing.T) { tests := []struct { - name string - queryArtist string - queryTrack string - want *Track - wantErr error + name string + query string + want *Track + wantErr error }{ { - name: "when track found", - queryArtist: "Found artist", - queryTrack: "Found track", + name: "when track found", + query: "found query", want: &Track{ ID: "1", Title: "sample title", @@ -197,10 +195,9 @@ func TestClient_SearchTrack(t *testing.T) { }, }, { - name: "when track not found", - queryArtist: "any impossible artist", - queryTrack: "any impossible track", - wantErr: NotFoundError, + name: "when track not found", + query: "any not found query", + wantErr: NotFoundError, }, } for _, tt := range tests { @@ -212,7 +209,7 @@ func TestClient_SearchTrack(t *testing.T) { require.Equal(t, "track", searchType) query := r.URL.Query().Get("text") - if query == "Found artist – Found track" { + if query == "found query" { _, err := w.Write([]byte(`{ "result": { "tracks":{ @@ -244,7 +241,7 @@ func TestClient_SearchTrack(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() - result, err := client.SearchTrack(ctx, tt.queryArtist, tt.queryTrack) + result, err := client.SearchTrack(ctx, tt.query) if tt.wantErr != nil { require.ErrorIs(t, err, tt.wantErr) } else { @@ -257,16 +254,14 @@ func TestClient_SearchTrack(t *testing.T) { func TestClient_SearchAlbum(t *testing.T) { tests := []struct { - name string - queryArtist string - queryAlbum string - want *Album - wantErr error + name string + query string + want *Album + wantErr error }{ { - name: "when track found", - queryArtist: "Found artist", - queryAlbum: "Found album", + name: "when track found", + query: "found query", want: &Album{ ID: 1, Title: "Sample Title", @@ -279,10 +274,9 @@ func TestClient_SearchAlbum(t *testing.T) { }, }, { - name: "when track not found", - queryArtist: "any impossible artist", - queryAlbum: "any impossible album", - wantErr: NotFoundError, + name: "when track not found", + query: "any not found query", + wantErr: NotFoundError, }, } for _, tt := range tests { @@ -294,7 +288,7 @@ func TestClient_SearchAlbum(t *testing.T) { require.Equal(t, "album", searchType) query := r.URL.Query().Get("text") - if query == "Found artist – Found album" { + if query == "found query" { _, err := w.Write([]byte(`{ "result": { "albums":{ @@ -319,7 +313,7 @@ func TestClient_SearchAlbum(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() - result, err := client.SearchAlbum(ctx, tt.queryArtist, tt.queryAlbum) + result, err := client.SearchAlbum(ctx, tt.query) if tt.wantErr != nil { require.ErrorIs(t, err, tt.wantErr) } else { diff --git a/internal/youtube/client.go b/internal/youtube/client.go index 1f35e7d..e39872c 100644 --- a/internal/youtube/client.go +++ b/internal/youtube/client.go @@ -3,6 +3,7 @@ package youtube import ( "context" "encoding/json" + "errors" "fmt" "io" "net/http" @@ -14,14 +15,14 @@ const ( ) var ( - NotFoundError = fmt.Errorf("not found") + NotFoundError = errors.New("not found") ) type Client interface { GetVideo(ctx context.Context, id string) (*Video, error) - SearchVideo(ctx context.Context, term string) (*Video, error) + SearchVideo(ctx context.Context, term string) (*SearchResponse, error) GetPlaylist(ctx context.Context, id string) (*Playlist, error) - SearchPlaylist(ctx context.Context, term string) (*Playlist, error) + SearchPlaylist(ctx context.Context, term string) (*SearchResponse, error) GetPlaylistItems(ctx context.Context, id string) ([]Video, error) } @@ -40,16 +41,15 @@ type getSnippetItem struct { Snippet *snippet `json:"snippet"` } -type searchSnippetResponse struct { - Items []*searchSnippetItem `json:"items"` +type SearchResponse struct { + Items []SearchItem `json:"items"` } -type searchSnippetItem struct { - ID *searchSnippetID `json:"id"` - Snippet *snippet `json:"snippet"` +type SearchItem struct { + ID SearchID `json:"id"` } -type searchSnippetID struct { +type SearchID struct { VideoID string `json:"videoId"` PlaylistID string `json:"playlistId"` } @@ -104,7 +104,7 @@ func (c *HTTPClient) GetVideo(ctx context.Context, id string) (*Video, error) { } // https://developers.google.com/youtube/v3/docs/search/list -func (c *HTTPClient) SearchVideo(ctx context.Context, query string) (*Video, error) { +func (c *HTTPClient) SearchVideo(ctx context.Context, query string) (*SearchResponse, error) { body, err := c.getWithKey(ctx, "/youtube/v3/search", url.Values{ "q": {query}, "part": {"snippet"}, @@ -116,7 +116,7 @@ func (c *HTTPClient) SearchVideo(ctx context.Context, query string) (*Video, err return nil, err } - response := searchSnippetResponse{} + response := SearchResponse{} if err := json.Unmarshal(body, &response); err != nil { return nil, fmt.Errorf("failed to decode api response: %w", err) } @@ -125,12 +125,7 @@ func (c *HTTPClient) SearchVideo(ctx context.Context, query string) (*Video, err return nil, NotFoundError } - item := response.Items[0] - return &Video{ - ID: item.ID.VideoID, - Title: item.Snippet.Title, - ChannelTitle: item.Snippet.ownerChannelTitle(), - }, nil + return &response, nil } // https://developers.google.com/youtube/v3/docs/playlists/list @@ -160,7 +155,7 @@ func (c *HTTPClient) GetPlaylist(ctx context.Context, id string) (*Playlist, err } // https://developers.google.com/youtube/v3/docs/search/list -func (c *HTTPClient) SearchPlaylist(ctx context.Context, query string) (*Playlist, error) { +func (c *HTTPClient) SearchPlaylist(ctx context.Context, query string) (*SearchResponse, error) { body, err := c.getWithKey(ctx, "/youtube/v3/search", url.Values{ "q": {query}, "part": {"snippet"}, @@ -171,7 +166,7 @@ func (c *HTTPClient) SearchPlaylist(ctx context.Context, query string) (*Playlis return nil, err } - response := searchSnippetResponse{} + response := SearchResponse{} if err := json.Unmarshal(body, &response); err != nil { return nil, fmt.Errorf("failed to decode api response: %w", err) } @@ -179,11 +174,7 @@ func (c *HTTPClient) SearchPlaylist(ctx context.Context, query string) (*Playlis return nil, NotFoundError } - return &Playlist{ - ID: response.Items[0].ID.PlaylistID, - Title: response.Items[0].Snippet.Title, - ChannelTitle: response.Items[0].Snippet.ownerChannelTitle(), - }, nil + return &response, nil } // https://developers.google.com/youtube/v3/docs/playlistItems/list diff --git a/internal/youtube/client_test.go b/internal/youtube/client_test.go index 30d7d8f..0f57d9e 100644 --- a/internal/youtube/client_test.go +++ b/internal/youtube/client_test.go @@ -83,11 +83,11 @@ func TestHTTPClient_GetVideo(t *testing.T) { func TestHTTPClient_SearchVideo(t *testing.T) { tests := []struct { - name string - query string - responseMock string - expectedVideo *Video - expectedErr error + name string + query string + responseMock string + expectedResponse *SearchResponse + expectedErr error }{ { name: "when video found", @@ -105,10 +105,15 @@ func TestHTTPClient_SearchVideo(t *testing.T) { } ] }`, - expectedVideo: &Video{ - ID: "dQw4w9WgXcQ", - Title: "Rick Astley - Never Gonna Give You Up (Video)", - ChannelTitle: "RickAstleyVEVO", + expectedResponse: &SearchResponse{ + Items: []SearchItem{ + { + ID: SearchID{ + VideoID: "dQw4w9WgXcQ", + PlaylistID: "", + }, + }, + }, }, }, { @@ -117,8 +122,8 @@ func TestHTTPClient_SearchVideo(t *testing.T) { responseMock: `{ "items": [] }`, - expectedVideo: nil, - expectedErr: NotFoundError, + expectedResponse: nil, + expectedErr: NotFoundError, }, } for _, tt := range tests { @@ -127,7 +132,6 @@ func TestHTTPClient_SearchVideo(t *testing.T) { require.Equal(t, http.MethodGet, r.Method) require.Equal(t, "/youtube/v3/search", r.URL.Path) require.Equal(t, sampleAPIKey, r.URL.Query().Get("key")) - require.Equal(t, "snippet", r.URL.Query().Get("part")) require.Equal(t, tt.query, r.URL.Query().Get("q")) require.Equal(t, "10", r.URL.Query().Get("videoCategoryId")) require.Equal(t, "1", r.URL.Query().Get("maxResults")) @@ -143,12 +147,12 @@ func TestHTTPClient_SearchVideo(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() - video, err := client.SearchVideo(ctx, tt.query) + response, err := client.SearchVideo(ctx, tt.query) if tt.expectedErr != nil { require.ErrorIs(t, err, tt.expectedErr) } else { require.NoError(t, err) - require.Equal(t, tt.expectedVideo, video) + require.Equal(t, tt.expectedResponse, response) } }) } @@ -227,7 +231,7 @@ func TestHTTPClient_SearchPlaylist(t *testing.T) { name string query string responseMock string - expectedPlaylist *Playlist + expectedResponse *SearchResponse expectedErr error }{ { @@ -246,10 +250,15 @@ func TestHTTPClient_SearchPlaylist(t *testing.T) { } ] }`, - expectedPlaylist: &Playlist{ - ID: "PLH1JGOJgZ2u2J7bRnfjl-7kDj_vQKTPa6", - Title: "Portishead - (1994) Dummy [Full Album]", - ChannelTitle: "Harry", + expectedResponse: &SearchResponse{ + Items: []SearchItem{ + { + ID: SearchID{ + VideoID: "", + PlaylistID: "PLH1JGOJgZ2u2J7bRnfjl-7kDj_vQKTPa6", + }, + }, + }, }, }, { @@ -258,7 +267,7 @@ func TestHTTPClient_SearchPlaylist(t *testing.T) { responseMock: `{ "items": [] }`, - expectedPlaylist: nil, + expectedResponse: nil, expectedErr: NotFoundError, }, } @@ -283,12 +292,12 @@ func TestHTTPClient_SearchPlaylist(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() - video, err := client.SearchPlaylist(ctx, tt.query) + response, err := client.SearchPlaylist(ctx, tt.query) if tt.expectedErr != nil { require.ErrorIs(t, err, tt.expectedErr) } else { require.NoError(t, err) - require.Equal(t, tt.expectedPlaylist, video) + require.Equal(t, tt.expectedResponse, response) } }) } diff --git a/yandex_adapter.go b/yandex_adapter.go index 048f813..f4cda6c 100644 --- a/yandex_adapter.go +++ b/yandex_adapter.go @@ -35,11 +35,8 @@ func (a *YandexAdapter) FetchTrack(ctx context.Context, id string) (*Entity, err return a.adaptTrack(yandexTrack), nil } -func (a *YandexAdapter) SearchTrack(ctx context.Context, artist, track string) (*Entity, error) { - lowcasedArtist := strings.ToLower(artist) - lowcasedTrack := strings.ToLower(track) - - foundTrack, err := a.findTrack(ctx, lowcasedArtist, lowcasedTrack) +func (a *YandexAdapter) SearchTrack(ctx context.Context, artist, title string) (*Entity, error) { + foundTrack, err := a.findTrack(ctx, artist, title) if err != nil { if errors.Is(err, yandex.NotFoundError) { return nil, EntityNotFoundError @@ -62,11 +59,8 @@ func (a *YandexAdapter) FetchAlbum(ctx context.Context, id string) (*Entity, err return a.adaptAlbum(yandexAlbum), nil } -func (a *YandexAdapter) SearchAlbum(ctx context.Context, artistName, albumName string) (*Entity, error) { - lowcasedArtist := strings.ToLower(artistName) - lowcasedAlbum := strings.ToLower(albumName) - - foundAlbum, err := a.findAlbum(ctx, lowcasedArtist, lowcasedAlbum) +func (a *YandexAdapter) SearchAlbum(ctx context.Context, artist, title string) (*Entity, error) { + foundAlbum, err := a.findAlbum(ctx, artist, title) if err != nil { if errors.Is(err, yandex.NotFoundError) { return nil, EntityNotFoundError @@ -77,64 +71,79 @@ func (a *YandexAdapter) SearchAlbum(ctx context.Context, artistName, albumName s return a.adaptAlbum(foundAlbum), nil } -func (a *YandexAdapter) findTrack(ctx context.Context, artist, track string) (*yandex.Track, error) { - foundTrack, err := a.client.SearchTrack(ctx, artist, track) +func (a *YandexAdapter) findTrack(ctx context.Context, artist, title string) (*yandex.Track, error) { + track, err := a.searchTrackRequest(ctx, artist, title) if err != nil && !errors.Is(err, yandex.NotFoundError) { return nil, fmt.Errorf("error searching track: %w", err) } - if foundTrack != nil { - artistMatch, err := a.artistMatch(ctx, foundTrack.Artists[0].Name, artist) + if track != nil { + artistMatch, err := a.artistMatch(ctx, track.Artists[0].Name, artist) if err != nil { return nil, fmt.Errorf("failed to check artist match: %w", err) } if artistMatch { - return foundTrack, nil + return track, nil } } - if translator.HasCyrillic(track) { + if translator.HasCyrillic(title) { translited := translator.TranslitLatToCyr(artist) - foundTranslitedTrack, err := a.client.SearchTrack(ctx, translited, track) + track, err = a.searchTrackRequest(ctx, translited, title) if err != nil { return nil, fmt.Errorf("error searching yandex track: %w", err) } - if foundTranslitedTrack != nil { - return foundTranslitedTrack, nil + if track != nil { + return track, nil } } return nil, yandex.NotFoundError } -func (a *YandexAdapter) findAlbum(ctx context.Context, artist, album string) (*yandex.Album, error) { - foundAlbum, err := a.client.SearchAlbum(ctx, artist, album) +func (a *YandexAdapter) findAlbum(ctx context.Context, artist, title string) (*yandex.Album, error) { + album, err := a.searchAlbumRequest(ctx, artist, title) if err != nil && !errors.Is(err, yandex.NotFoundError) { return nil, fmt.Errorf("error searching album: %w", err) } - if foundAlbum != nil { - artistMatch, err := a.artistMatch(ctx, foundAlbum.Artists[0].Name, artist) + if album != nil { + artistMatch, err := a.artistMatch(ctx, album.Artists[0].Name, artist) if err != nil { return nil, fmt.Errorf("failed to check artist match: %w", err) } if artistMatch { - return foundAlbum, nil + return album, nil } } - if translator.HasCyrillic(album) { + if translator.HasCyrillic(title) { translited := translator.TranslitLatToCyr(artist) - foundTranslitedAlbum, err := a.client.SearchAlbum(ctx, translited, album) + album, err = a.searchAlbumRequest(ctx, translited, title) if err != nil { return nil, fmt.Errorf("error searching yandex album: %w", err) } - if foundTranslitedAlbum != nil { - return foundTranslitedAlbum, nil + if album != nil { + return album, nil } } return nil, yandex.NotFoundError } +func (a *YandexAdapter) searchTrackRequest(ctx context.Context, artist, title string) (*yandex.Track, error) { + query := a.prepareQuery(artist, title) + return a.client.SearchTrack(ctx, query) +} + +func (a *YandexAdapter) searchAlbumRequest(ctx context.Context, artist, title string) (*yandex.Album, error) { + query := a.prepareQuery(artist, title) + return a.client.SearchAlbum(ctx, query) +} + +func (a *YandexAdapter) prepareQuery(artist, title string) string { + query := entityFullTitle(artist, title) + return strings.ToLower(query) +} + func (a *YandexAdapter) adaptTrack(yandexTrack *yandex.Track) *Entity { return &Entity{ ID: yandexTrack.IDString(), @@ -157,23 +166,24 @@ func (a *YandexAdapter) adaptAlbum(yandexAlbum *yandex.Album) *Entity { } } -func (a *YandexAdapter) artistMatch(ctx context.Context, foundArtist, artistName string) (bool, error) { - lowcasedFoundArtist := strings.ToLower(foundArtist) - if artistName == lowcasedFoundArtist { +func (a *YandexAdapter) artistMatch(ctx context.Context, found, query string) (bool, error) { + lcFound := strings.ToLower(found) + lcQuery := strings.ToLower(query) + if lcQuery == lcFound { return true, nil } - translitedFoundArtist := translator.TranslitCyrToLat(lowcasedFoundArtist) - if artistName == translitedFoundArtist { + translitedFoundArtist := translator.TranslitCyrToLat(lcFound) + if lcQuery == translitedFoundArtist { return true, nil } - if translator.HasCyrillic(foundArtist) { - translatedArtist, err := a.translator.TranslateEnToRu(ctx, artistName) + if translator.HasCyrillic(found) { + translatedArtist, err := a.translator.TranslateEnToRu(ctx, lcQuery) if err != nil { return false, fmt.Errorf("failed to translate artist name: %w", err) } - if translatedArtist == lowcasedFoundArtist { + if translatedArtist == lcFound { return true, nil } } diff --git a/yandex_adapter_test.go b/yandex_adapter_test.go index b0f387d..58de85c 100644 --- a/yandex_adapter_test.go +++ b/yandex_adapter_test.go @@ -13,8 +13,8 @@ import ( type yandexClientMock struct { fetchTrack map[string]*yandex.Track fetchAlbum map[string]*yandex.Album - searchTrack map[string]map[string]*yandex.Track - searchAlbum map[string]map[string]*yandex.Album + searchTrack map[string]*yandex.Track + searchAlbum map[string]*yandex.Album } func (c *yandexClientMock) FetchTrack(_ context.Context, id string) (*yandex.Track, error) { @@ -25,12 +25,8 @@ func (c *yandexClientMock) FetchTrack(_ context.Context, id string) (*yandex.Tra return track, nil } -func (c *yandexClientMock) SearchTrack(_ context.Context, artistName, trackName string) (*yandex.Track, error) { - if tracks, ok := c.searchTrack[artistName]; ok { - track, ok := tracks[trackName] - if !ok { - return nil, yandex.NotFoundError - } +func (c *yandexClientMock) SearchTrack(_ context.Context, query string) (*yandex.Track, error) { + if track, ok := c.searchTrack[query]; ok { return track, nil } return nil, yandex.NotFoundError @@ -44,12 +40,8 @@ func (c *yandexClientMock) FetchAlbum(_ context.Context, id string) (*yandex.Alb return album, nil } -func (c *yandexClientMock) SearchAlbum(_ context.Context, artistName, albumName string) (*yandex.Album, error) { - if albums, ok := c.searchAlbum[artistName]; ok { - album, ok := albums[albumName] - if !ok { - return nil, yandex.NotFoundError - } +func (c *yandexClientMock) SearchAlbum(_ context.Context, query string) (*yandex.Album, error) { + if album, ok := c.searchAlbum[query]; ok { return album, nil } return nil, yandex.NotFoundError @@ -143,17 +135,15 @@ func TestYandexAdapter_SearchTrack(t *testing.T) { artistName: "sample artist", searchName: "sample name", yandexClientMock: yandexClientMock{ - searchTrack: map[string]map[string]*yandex.Track{ - "sample artist": { - "sample name": { - ID: 42, - Title: "sample name", - Artists: []yandex.Artist{ - {Name: "sample artist"}, - }, - Albums: []yandex.Album{ - {ID: 41}, - }, + searchTrack: map[string]*yandex.Track{ + "sample artist – sample name": { + ID: 42, + Title: "sample name", + Artists: []yandex.Artist{ + {Name: "sample artist"}, + }, + Albums: []yandex.Album{ + {ID: 41}, }, }, }, @@ -172,17 +162,15 @@ func TestYandexAdapter_SearchTrack(t *testing.T) { artistName: "sample artist not matching", searchName: "sample name", yandexClientMock: yandexClientMock{ - searchTrack: map[string]map[string]*yandex.Track{ - "sample artist": { - "sample artist not matching": { - ID: 42, - Title: "sample name", - Artists: []yandex.Artist{ - {Name: "not matching artist"}, - }, - Albums: []yandex.Album{ - {ID: 41}, - }, + searchTrack: map[string]*yandex.Track{ + "sample artist – sample artist not matching": { + ID: 42, + Title: "sample name", + Artists: []yandex.Artist{ + {Name: "not matching artist"}, + }, + Albums: []yandex.Album{ + {ID: 41}, }, }, }, @@ -195,17 +183,15 @@ func TestYandexAdapter_SearchTrack(t *testing.T) { artistName: "sample artist matching translit", searchName: "sample name", yandexClientMock: yandexClientMock{ - searchTrack: map[string]map[string]*yandex.Track{ - "sample artist matching translit": { - "sample name": { - ID: 42, - Title: "sample name", - Artists: []yandex.Artist{ - {Name: "сампле артист матчинг транслит"}, - }, - Albums: []yandex.Album{ - {ID: 41}, - }, + searchTrack: map[string]*yandex.Track{ + "sample artist matching translit – sample name": { + ID: 42, + Title: "sample name", + Artists: []yandex.Artist{ + {Name: "сампле артист матчинг транслит"}, + }, + Albums: []yandex.Album{ + {ID: 41}, }, }, }, @@ -224,17 +210,15 @@ func TestYandexAdapter_SearchTrack(t *testing.T) { artistName: "sample artist after translit", searchName: "кириллическое название", yandexClientMock: yandexClientMock{ - searchTrack: map[string]map[string]*yandex.Track{ - "сампле артист афтер транслит": { - "кириллическое название": { - ID: 42, - Title: "sample name", - Artists: []yandex.Artist{ - {Name: "сампле артист афтер транслит"}, - }, - Albums: []yandex.Album{ - {ID: 41}, - }, + searchTrack: map[string]*yandex.Track{ + "сампле артист афтер транслит – кириллическое название": { + ID: 42, + Title: "sample name", + Artists: []yandex.Artist{ + {Name: "сампле артист афтер транслит"}, + }, + Albums: []yandex.Album{ + {ID: 41}, }, }, }, @@ -253,17 +237,15 @@ func TestYandexAdapter_SearchTrack(t *testing.T) { artistName: "translatable artist", searchName: "sample name", yandexClientMock: yandexClientMock{ - searchTrack: map[string]map[string]*yandex.Track{ - "translatable artist": { - "sample name": { - ID: 42, - Title: "sample name", - Artists: []yandex.Artist{ - {Name: "переведенный артист"}, - }, - Albums: []yandex.Album{ - {ID: 41}, - }, + searchTrack: map[string]*yandex.Track{ + "translatable artist – sample name": { + ID: 42, + Title: "sample name", + Artists: []yandex.Artist{ + {Name: "переведенный артист"}, + }, + Albums: []yandex.Album{ + {ID: 41}, }, }, }, @@ -381,14 +363,12 @@ func TestYandexAdapter_SearchAlbum(t *testing.T) { artistName: "sample artist", searchName: "sample name", yandexClientMock: yandexClientMock{ - searchAlbum: map[string]map[string]*yandex.Album{ - "sample artist": { - "sample name": { - ID: 42, - Title: "sample name", - Artists: []yandex.Artist{ - {Name: "sample artist"}, - }, + searchAlbum: map[string]*yandex.Album{ + "sample artist – sample name": { + ID: 42, + Title: "sample name", + Artists: []yandex.Artist{ + {Name: "sample artist"}, }, }, }, @@ -407,14 +387,12 @@ func TestYandexAdapter_SearchAlbum(t *testing.T) { artistName: "sample artist not matching", searchName: "sample name", yandexClientMock: yandexClientMock{ - searchAlbum: map[string]map[string]*yandex.Album{ - "sample artist": { - "sample artist not matching": { - ID: 42, - Title: "sample name", - Artists: []yandex.Artist{ - {Name: "not matching artist"}, - }, + searchAlbum: map[string]*yandex.Album{ + "sample artist – sample artist not matching": { + ID: 42, + Title: "sample name", + Artists: []yandex.Artist{ + {Name: "not matching artist"}, }, }, }, @@ -427,14 +405,12 @@ func TestYandexAdapter_SearchAlbum(t *testing.T) { artistName: "sample artist matching translit", searchName: "sample name", yandexClientMock: yandexClientMock{ - searchAlbum: map[string]map[string]*yandex.Album{ - "sample artist matching translit": { - "sample name": { - ID: 42, - Title: "sample name", - Artists: []yandex.Artist{ - {Name: "сампле артист матчинг транслит"}, - }, + searchAlbum: map[string]*yandex.Album{ + "sample artist matching translit – sample name": { + ID: 42, + Title: "sample name", + Artists: []yandex.Artist{ + {Name: "сампле артист матчинг транслит"}, }, }, }, @@ -453,14 +429,12 @@ func TestYandexAdapter_SearchAlbum(t *testing.T) { artistName: "sample artist after translit", searchName: "кириллическое название", yandexClientMock: yandexClientMock{ - searchAlbum: map[string]map[string]*yandex.Album{ - "сампле артист афтер транслит": { - "кириллическое название": { - ID: 42, - Title: "sample name", - Artists: []yandex.Artist{ - {Name: "сампле артист афтер транслит"}, - }, + searchAlbum: map[string]*yandex.Album{ + "сампле артист афтер транслит – кириллическое название": { + ID: 42, + Title: "sample name", + Artists: []yandex.Artist{ + {Name: "сампле артист афтер транслит"}, }, }, }, @@ -479,14 +453,12 @@ func TestYandexAdapter_SearchAlbum(t *testing.T) { artistName: "translatable artist", searchName: "sample name", yandexClientMock: yandexClientMock{ - searchAlbum: map[string]map[string]*yandex.Album{ - "translatable artist": { - "sample name": { - ID: 42, - Title: "sample name", - Artists: []yandex.Artist{ - {Name: "переведенный артист"}, - }, + searchAlbum: map[string]*yandex.Album{ + "translatable artist – sample name": { + ID: 42, + Title: "sample name", + Artists: []yandex.Artist{ + {Name: "переведенный артист"}, }, }, }, diff --git a/youtube_adapter.go b/youtube_adapter.go index af3ce63..a9bf0b4 100644 --- a/youtube_adapter.go +++ b/youtube_adapter.go @@ -14,7 +14,10 @@ type YoutubeAdapter struct { client youtube.Client } -var nonTitleContentRe = regexp.MustCompile(`\s*\[.*?\]|\s*\{.*?\}|\s*\(.*?\)`) +var ( + nonTitleContentRe = regexp.MustCompile(`\s*\[.*?\]|\s*\{.*?\}|\s*\(.*?\)`) + titleSeparators = []string{" - ", " – ", " — ", "|"} +) func newYoutubeAdapter(client youtube.Client) *YoutubeAdapter { return &YoutubeAdapter{ @@ -33,8 +36,8 @@ func (a *YoutubeAdapter) FetchTrack(ctx context.Context, id string) (*Entity, er } func (a *YoutubeAdapter) SearchTrack(ctx context.Context, artistName, trackName string) (*Entity, error) { - query := fmt.Sprintf("%s – %s", artistName, trackName) - video, err := a.client.SearchVideo(ctx, query) + query := entityFullTitle(artistName, trackName) + search, err := a.client.SearchVideo(ctx, query) if err != nil { if errors.Is(err, youtube.NotFoundError) { return nil, EntityNotFoundError @@ -42,6 +45,12 @@ func (a *YoutubeAdapter) SearchTrack(ctx context.Context, artistName, trackName return nil, fmt.Errorf("failed to search video on youtube: %w", err) } + id := search.Items[0].ID.VideoID + video, err := a.client.GetVideo(ctx, id) + if err != nil { + return nil, fmt.Errorf("failed to get video from youtube: %w", err) + } + return a.adaptTrack(video), nil } @@ -57,8 +66,8 @@ func (a *YoutubeAdapter) FetchAlbum(ctx context.Context, id string) (*Entity, er } func (a *YoutubeAdapter) SearchAlbum(ctx context.Context, artistName, albumName string) (*Entity, error) { - query := fmt.Sprintf("%s – %s", artistName, albumName) - album, err := a.client.SearchPlaylist(ctx, query) + query := entityFullTitle(artistName, albumName) + search, err := a.client.SearchPlaylist(ctx, query) if err != nil { if errors.Is(err, youtube.NotFoundError) { return nil, EntityNotFoundError @@ -66,6 +75,12 @@ func (a *YoutubeAdapter) SearchAlbum(ctx context.Context, artistName, albumName return nil, fmt.Errorf("failed to search playlist on youtube: %w", err) } + id := search.Items[0].ID.PlaylistID + album, err := a.client.GetPlaylist(ctx, id) + if err != nil { + return nil, fmt.Errorf("failed to get playlist from youtube: %w", err) + } + return a.adaptAlbum(ctx, album) } @@ -131,8 +146,7 @@ func (a *YoutubeAdapter) extractAlbumTitle(ctx context.Context, playlist *youtub func (a *YoutubeAdapter) cleanAndSplitTitle(title string) (artist, entity string) { cleanTitle := nonTitleContentRe.ReplaceAllString(title, "") - separators := []string{" - ", " – ", " — ", "|"} - for _, sep := range separators { + for _, sep := range titleSeparators { if strings.Contains(cleanTitle, sep) { parts := strings.Split(cleanTitle, sep) return strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1]) diff --git a/youtube_adapter_test.go b/youtube_adapter_test.go index aa9ed1a..3b02012 100644 --- a/youtube_adapter_test.go +++ b/youtube_adapter_test.go @@ -13,8 +13,8 @@ import ( type youtubeClientMock struct { getVideo map[string]*youtube.Video getPlaylist map[string]*youtube.Playlist - searchVideo map[string]*youtube.Video - searchPlaylist map[string]*youtube.Playlist + searchVideo map[string]*youtube.SearchResponse + searchPlaylist map[string]*youtube.SearchResponse getPlaylistItems map[string][]youtube.Video } @@ -26,7 +26,7 @@ func (c *youtubeClientMock) GetVideo(_ context.Context, id string) (*youtube.Vid return video, nil } -func (c *youtubeClientMock) SearchVideo(_ context.Context, query string) (*youtube.Video, error) { +func (c *youtubeClientMock) SearchVideo(_ context.Context, query string) (*youtube.SearchResponse, error) { video, ok := c.searchVideo[query] if !ok { return nil, youtube.NotFoundError @@ -42,7 +42,7 @@ func (c *youtubeClientMock) GetPlaylist(_ context.Context, id string) (*youtube. return playlist, nil } -func (c *youtubeClientMock) SearchPlaylist(_ context.Context, query string) (*youtube.Playlist, error) { +func (c *youtubeClientMock) SearchPlaylist(_ context.Context, query string) (*youtube.SearchResponse, error) { playlist, ok := c.searchPlaylist[query] if !ok { return nil, youtube.NotFoundError @@ -145,8 +145,19 @@ func TestYoutubeAdapter_SearchTrack(t *testing.T) { artistName: "sample artist", searchName: "sample track", youtubeClientMock: youtubeClientMock{ - searchVideo: map[string]*youtube.Video{ + searchVideo: map[string]*youtube.SearchResponse{ "sample artist – sample track": { + Items: []youtube.SearchItem{ + { + ID: youtube.SearchID{ + VideoID: "sampleID", + }, + }, + }, + }, + }, + getVideo: map[string]*youtube.Video{ + "sampleID": { ID: "sampleID", Title: "sample artist – sample track", }, @@ -341,8 +352,19 @@ func TestYoutubeAdapter_SearchAlbum(t *testing.T) { artistName: "sample artist", searchName: "sample album", youtubeClientMock: youtubeClientMock{ - searchPlaylist: map[string]*youtube.Playlist{ + searchPlaylist: map[string]*youtube.SearchResponse{ "sample artist – sample album": { + Items: []youtube.SearchItem{ + { + ID: youtube.SearchID{ + PlaylistID: "sampleID", + }, + }, + }, + }, + }, + getPlaylist: map[string]*youtube.Playlist{ + "sampleID": { ID: "sampleID", Title: "sample artist – sample album", },