Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

search musicとsearch playコマンドの追加 #45

Merged
merged 1 commit into from Feb 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
63 changes: 63 additions & 0 deletions subcommand/action/owntone/client.go
Expand Up @@ -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
}
188 changes: 188 additions & 0 deletions subcommand/action/owntone/client_test.go
Expand Up @@ -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)
}
})
}
}
117 changes: 117 additions & 0 deletions 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,
}

}