From 6223e3a41441669b15753d2bd3c349cf0609914b Mon Sep 17 00:00:00 2001 From: Jun Ohtani Date: Tue, 27 Feb 2024 23:44:46 +0900 Subject: [PATCH] =?UTF-8?q?search=20music=E3=81=A8search=20play=E3=82=B3?= =?UTF-8?q?=E3=83=9E=E3=83=B3=E3=83=89=E3=81=AE=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Related #29 --- subcommand/action/owntone/client.go | 63 ++++++++ subcommand/action/owntone/client_test.go | 188 +++++++++++++++++++++++ subcommand/action/owntone/search.go | 117 ++++++++++++++ subcommand/search-and-play.go | 32 ++++ subcommand/search-music.go | 27 ++++ subcommand/subcommand.go | 2 + 6 files changed, 429 insertions(+) create mode 100644 subcommand/action/owntone/search.go create mode 100644 subcommand/search-and-play.go create mode 100644 subcommand/search-music.go diff --git a/subcommand/action/owntone/client.go b/subcommand/action/owntone/client.go index e5e786d..83225e3 100644 --- a/subcommand/action/owntone/client.go +++ b/subcommand/action/owntone/client.go @@ -208,3 +208,66 @@ func (c Client) ClearQueue() error { } return nil } + +type SearchType string + +const ( + //playlist SearchType = "playlist" + artist SearchType = "artist" + album SearchType = "album" + track SearchType = "track" + //genre SearchType = "genre" +) + +type SearchItem struct { + Title string `json:"title"` + Uri string `json:"uri"` + Name string `json:"name"` + Artist string `json:"artist"` +} + +type Items struct { + Items []SearchItem `json:"items"` + Total int `json:"total"` + Offset int `json:"offset"` + Limit int `json:"limit"` +} + +type SearchResult struct { + Tracks Items `json:"tracks"` + Artists Items `json:"artists"` + Albums Items `json:"albums"` + Playlists Items `json:"playlists"` +} + +func (c Client) Search(keyword string, resultType []SearchType) (*SearchResult, error) { + params := map[string]string{} + params["query"] = keyword + params["limit"] = "5" + var types []string + for _, s := range resultType { + types = append(types, string(s)) + } + params["type"] = strings.Join(types, ",") + req, err := internal.BuildHttpRequestWithParams(http.MethodGet, c.buildUrl("api/search"), params) + if err != nil { + return nil, err + } + res, err := c.Do(req) + if err != nil { + return nil, err + } + defer func(Body io.ReadCloser) { + _ = Body.Close() + }(res.Body) + if res.StatusCode != http.StatusOK { + return nil, fmt.Errorf("something wrong... status code is %d. %v", res.StatusCode, res.Header) + } + + // Decode the JSON response into a Response struct + var results SearchResult + if err := json.NewDecoder(res.Body).Decode(&results); err != nil { + return nil, fmt.Errorf("failed to decode response: %v", err) + } + return &results, nil +} diff --git a/subcommand/action/owntone/client_test.go b/subcommand/action/owntone/client_test.go index ea325e2..3cfbbaf 100644 --- a/subcommand/action/owntone/client_test.go +++ b/subcommand/action/owntone/client_test.go @@ -357,3 +357,191 @@ func TestClient_ClearQueue(t *testing.T) { }) } } + +func searchSampleJSONResponse() string { + return `{ + "tracks": { + "items": [ + { + "id": 35, + "title": "Another Love", + "artist": "Tom Odell", + "artist_sort": "Tom Odell", + "album": "Es is was es is", + "album_sort": "Es is was es is", + "album_id": "6494853621007413058", + "album_artist": "Various artists", + "album_artist_sort": "Various artists", + "album_artist_id": "8395563705718003786", + "genre": "Singer/Songwriter", + "year": 2013, + "track_number": 7, + "disc_number": 1, + "length_ms": 251030, + "play_count": 0, + "media_kind": "music", + "data_kind": "file", + "path": "/music/srv/Compilations/Es is was es is/07 Another Love.m4a", + "uri": "library:track:35" + }, + { + "id": 215, + "title": "Away From the Sun", + "artist": "3 Doors Down", + "artist_sort": "3 Doors Down", + "album": "Away From the Sun", + "album_sort": "Away From the Sun", + "album_id": "8264078270267374619", + "album_artist": "3 Doors Down", + "album_artist_sort": "3 Doors Down", + "album_artist_id": "5030128490104968038", + "genre": "Rock", + "year": 2002, + "track_number": 2, + "disc_number": 1, + "length_ms": 233278, + "play_count": 0, + "media_kind": "music", + "data_kind": "file", + "path": "/music/srv/Away From the Sun/02 Away From the Sun.mp3", + "uri": "library:track:215" + } + ], + "total": 14, + "offset": 0, + "limit": 2 + }, + "artists": { + "items": [ + { + "id": "8737690491750445895", + "name": "The xx", + "name_sort": "xx, The", + "album_count": 2, + "track_count": 25, + "length_ms": 5229196, + "uri": "library:artist:8737690491750445895" + } + ], + "total": 1, + "offset": 0, + "limit": 2 + }, + "albums": { + "items": [ + { + "id": "8264078270267374619", + "name": "Away From the Sun", + "name_sort": "Away From the Sun", + "artist": "3 Doors Down", + "artist_id": "5030128490104968038", + "track_count": 12, + "length_ms": 2818174, + "uri": "library:album:8264078270267374619" + }, + { + "id": "6835720495312674468", + "name": "The Better Life", + "name_sort": "Better Life", + "artist": "3 Doors Down", + "artist_id": "5030128490104968038", + "track_count": 11, + "length_ms": 2393332, + "uri": "library:album:6835720495312674468" + } + ], + "total": 3, + "offset": 0, + "limit": 2 + }, + "playlists": { + "items": [], + "total": 0, + "offset": 0, + "limit": 2 + } +} +` +} + +func TestClient_Search(t *testing.T) { + type fields struct { + statusCode int + method string + path string + response string + } + type args struct { + keyword string + resultType []SearchType + } + path := "/api/search" + tests := []struct { + name string + fields fields + args args + want *SearchResult + wantErr bool + }{ + { + name: "OK", + fields: fields{statusCode: http.StatusOK, method: http.MethodGet, path: path, response: searchSampleJSONResponse()}, + args: args{keyword: "keyword", resultType: []SearchType{track}}, + want: &SearchResult{ + Tracks: Items{Items: []SearchItem{ + { + Title: "Another Love", + Uri: "library:track:35", + Name: "", + Artist: "Tom Odell", + }, + { + Title: "Away From the Sun", + Uri: "library:track:215", + Name: "", + Artist: "3 Doors Down", + }}, Total: 14, Offset: 0, Limit: 2}, + Artists: Items{Items: []SearchItem{{ + Title: "", + Uri: "library:artist:8737690491750445895", + Name: "The xx", + Artist: "", + }}, Total: 1, Offset: 0, Limit: 2}, + Albums: Items{Items: []SearchItem{ + { + Title: "", + Uri: "library:album:8264078270267374619", + Name: "Away From the Sun", + Artist: "3 Doors Down", + }, + { + Title: "", + Uri: "library:album:6835720495312674468", + Name: "The Better Life", + Artist: "3 Doors Down", + }, + }, Total: 3, Offset: 0, Limit: 2}, + Playlists: Items{Items: []SearchItem{}, Total: 0, Offset: 0, Limit: 2}, + }, + wantErr: false, + }, + {"NG", fields{statusCode: http.StatusInternalServerError, method: http.MethodGet, path: path, response: searchSampleJSONResponse()}, args{keyword: "keyword", resultType: []SearchType{track}}, nil, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + server := createMockServerWithResponse(tt.fields.statusCode, tt.fields.method, tt.fields.path, nil, tt.fields.response) + defer server.Close() + config := Config{Url: server.URL} + c := NewClient(config) + + got, err := c.Search(tt.args.keyword, tt.args.resultType) + if (err != nil) != tt.wantErr { + t.Errorf("Search() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Search() got = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/subcommand/action/owntone/search.go b/subcommand/action/owntone/search.go new file mode 100644 index 0000000..ac5c2e7 --- /dev/null +++ b/subcommand/action/owntone/search.go @@ -0,0 +1,117 @@ +package owntone + +import ( + "fmt" + "strings" +) + +type SearchAndPlayAction struct { + name string + c *Client +} + +func (a SearchAndPlayAction) Run(query string) (string, error) { + //TODO resultTypeを複数選択できるようにする or 引数から割り出す or actionを別にする + msg := []string{"Search Results..."} + types := []SearchType{track, album, artist} + result, err := a.c.Search(query, types) + if err != nil { + fmt.Println("error in SearchAndDisplayAction") + return "Something wrong...", err + } + var uris []string + if result.Artists.Total > 0 { + msg = append(msg, "# Artists") + for _, item := range result.Artists.Items { + msg = append(msg, fmt.Sprintf(" %v", item.Name)) + uris = append(uris, item.Uri) + } + } + if result.Albums.Total > 0 { + msg = append(msg, "# Albums") + for _, item := range result.Albums.Items { + msg = append(msg, fmt.Sprintf(" %v / %v", item.Name, item.Artist)) + uris = append(uris, item.Uri) + } + } + if result.Tracks.Total > 0 { + msg = append(msg, "# Tracks") + for _, item := range result.Tracks.Items { + msg = append(msg, fmt.Sprintf(" %v / %v ", item.Title, item.Artist)) + uris = append(uris, item.Uri) + } + } + if len(uris) > 0 { + err := a.c.ClearQueue() + if err != nil { + fmt.Println("error in ClearQueue") + return "", err + } + err = a.c.AddItem2Queue(strings.Join(uris, ",")) + if err != nil { + fmt.Println("error calling AddItem2Queue") + return "", err + } + err = a.c.Play() + if err != nil { + fmt.Println("error in Play") + return "", err + } + } + if len(msg) > 1 { + msg = append(msg, "And play these items") + } else { + msg = append(msg, "And no play items...") + } + return strings.Join(msg, "\n"), nil +} + +func NewSearchAndPlayAction(client *Client) SearchAndPlayAction { + return SearchAndPlayAction{ + name: "Search and Play music on Owntone by keyword", + c: client, + } +} + +type SearchAndDisplayAction struct { + name string + c *Client +} + +func (a SearchAndDisplayAction) Run(query string) (string, error) { + msg := []string{"Search Results..."} + types := []SearchType{track, album, artist} + result, err := a.c.Search(query, types) + if err != nil { + fmt.Println("error in SearchAndDisplayAction") + return "Something wrong...", err + } + if result.Artists.Total > 0 { + msg = append(msg, "# Artists") + for _, item := range result.Artists.Items { + msg = append(msg, fmt.Sprintf(" %v", item.Name)) + } + } + if result.Albums.Total > 0 { + msg = append(msg, "# Albums") + for _, item := range result.Albums.Items { + msg = append(msg, fmt.Sprintf(" %v / %v", item.Name, item.Artist)) + } + } + if result.Tracks.Total > 0 { + msg = append(msg, "# Tracks") + for _, item := range result.Tracks.Items { + msg = append(msg, fmt.Sprintf(" %v / %v ", item.Title, item.Artist)) + } + } + + return strings.Join(msg, "\n"), nil +} + +func NewSearchAndDisplayAction(client *Client) SearchAndDisplayAction { + return SearchAndDisplayAction{ + name: "Search music by keyword on Owntone and display results", + c: client, + } + +} diff --git a/subcommand/search-and-play.go b/subcommand/search-and-play.go new file mode 100644 index 0000000..68c9956 --- /dev/null +++ b/subcommand/search-and-play.go @@ -0,0 +1,32 @@ +package subcommand + +import ( + "github.com/johtani/smarthome/subcommand/action" + "github.com/johtani/smarthome/subcommand/action/owntone" + "time" +) + +const SearchAndPlayMusicCmd = "search and play" +const SearchPlayCmd = "search play" + +func NewSearchAndPlayMusicCmdDefinition() Definition { + return Definition{ + Name: SearchAndPlayMusicCmd, + Description: "Search Music by keyword And play", + Factory: NewSearchAndPlayMusicSubcommand, + shortnames: []string{SearchPlayCmd}, + } +} + +func NewSearchAndPlayMusicSubcommand(definition Definition, config Config) Subcommand { + owntoneClient := owntone.NewClient(config.Owntone) + return Subcommand{ + Definition: definition, + actions: []action.Action{ + owntone.NewSearchAndPlayAction(owntoneClient), + action.NewNoOpAction(3 * time.Second), + owntone.NewSetVolumeAction(owntoneClient), + }, + ignoreError: true, + } +} diff --git a/subcommand/search-music.go b/subcommand/search-music.go new file mode 100644 index 0000000..921d60f --- /dev/null +++ b/subcommand/search-music.go @@ -0,0 +1,27 @@ +package subcommand + +import ( + "github.com/johtani/smarthome/subcommand/action" + "github.com/johtani/smarthome/subcommand/action/owntone" +) + +const SearchMusicCmd = "search music" + +func NewSearchMusicCmdDefinition() Definition { + return Definition{ + Name: SearchMusicCmd, + Description: "Search Music by keyword", + Factory: NewSearchMusicSubcommand, + } +} + +func NewSearchMusicSubcommand(definition Definition, config Config) Subcommand { + owntoneClient := owntone.NewClient(config.Owntone) + return Subcommand{ + Definition: definition, + actions: []action.Action{ + owntone.NewSearchAndDisplayAction(owntoneClient), + }, + ignoreError: true, + } +} diff --git a/subcommand/subcommand.go b/subcommand/subcommand.go index f7f85a9..2a4ca21 100644 --- a/subcommand/subcommand.go +++ b/subcommand/subcommand.go @@ -106,6 +106,8 @@ func NewCommands() Commands { NewStartMusicCmdDefinition(), NewStopMusicDefinition(), NewChangePlaylistCmdDefinition(), + NewSearchMusicCmdDefinition(), + NewSearchAndPlayMusicCmdDefinition(), NewSwitchBotDeviceListDefinition(), NewSwitchBotSceneListDefinition(), NewLightOffDefinition(),