From 6c0f4ddb37e3c5a06216b18e0646a6aa3cb934dd Mon Sep 17 00:00:00 2001 From: "Sean E. Russell" Date: Tue, 15 Oct 2024 15:03:22 -0500 Subject: [PATCH 1/6] feature: #76, clarify search results --- page_search.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/page_search.go b/page_search.go index ac106fa..8e666f6 100644 --- a/page_search.go +++ b/page_search.go @@ -218,7 +218,6 @@ func (s *SearchPage) search(search chan string) { artOff = 0 albOff = 0 songOff = 0 - s.logger.Printf("searching for %q [%d, %d, %d]", query, artOff, albOff, songOff) for len(more) > 0 { <-more } @@ -226,7 +225,6 @@ func (s *SearchPage) search(search chan string) { continue } case <-more: - s.logger.Printf("fetching more %q [%d, %d, %d]", query, artOff, albOff, songOff) } res, err := s.ui.connection.Search(query, artOff, albOff, songOff) if err != nil { @@ -265,6 +263,10 @@ func (s *SearchPage) search(search chan string) { s.songList.Box.SetTitle(fmt.Sprintf(" song matches (%d) ", len(s.songs))) }) + // Only do this the one time, to prevent loops from stealing the user's focus + if artOff == 0 && albOff == 0 && songOff == 0 { + s.aproposFocus() + } artOff += len(res.SearchResults.Artist) albOff += len(res.SearchResults.Album) songOff += len(res.SearchResults.Song) From 1a744b7d9cb4ff658de7fac645ae49ec13cb128c Mon Sep 17 00:00:00 2001 From: "Sean E. Russell" Date: Tue, 15 Oct 2024 15:58:39 -0500 Subject: [PATCH 2/6] add: search now pro-actively searches until no more results are returned. It still queries in batches of 20, and updates the list(s) as results are available. --- page_search.go | 1 + 1 file changed, 1 insertion(+) diff --git a/page_search.go b/page_search.go index 8e666f6..e859192 100644 --- a/page_search.go +++ b/page_search.go @@ -267,6 +267,7 @@ func (s *SearchPage) search(search chan string) { if artOff == 0 && albOff == 0 && songOff == 0 { s.aproposFocus() } + artOff += len(res.SearchResults.Artist) albOff += len(res.SearchResults.Album) songOff += len(res.SearchResults.Song) From 1fe546ff50e2e4b3669ac2daa54da233e14a5267 Mon Sep 17 00:00:00 2001 From: "Sean E. Russell" Date: Mon, 28 Oct 2024 11:09:16 -0500 Subject: [PATCH 3/6] feat: #52, search by genre --- README.md | 5 +- help_text.go | 1 + page_playlist.go | 5 -- page_search.go | 161 ++++++++++++++++++++++++++++++++++++----------- subsonic/api.go | 39 ++++++++++++ 5 files changed, 170 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index bb84615..02990e3 100644 --- a/README.md +++ b/README.md @@ -177,13 +177,16 @@ In any of the columns: - `Enter` / `a`: Adds the selected item recursively to the queue. - Left/right arrow keys (`←`, `→`) navigate between the columns - Up/down arrow keys (`↓`, `↑`) navigate the selected column list +- `g`: toggle genre search In the search field: - `Enter`: Perform the query. - `Escape`: Escapes into the columns, where the global key bindings work. -Note that the Search page is *not* a browser like the Browser page: it displays the search results returned by the server. Selecting a different artist will not change the album or song search results. OpenSubsonic servers implement the search function differently; in gonic, if you search for "black", you will get artists with "black" in their names in the artists column; albums with "black" in their titles in the albums column; and songs with "black" in their titles in the songs column. Navidrome appears to include all results with "black" anywhere in their IDv3 metadata. Since the API search results filteres these matches into sections -- artists, albums, and songs -- this means that, with Navidrome, you may see albums that don't have "black" in their names; maybe "black" is in their artist title. +Note that the Search page is *not* a browser like the Browser page: it displays the search results returned by the server. Selecting a different artist will not change the album or song search results. + +In Genre Search mode, the genres known by the server are displayed in the middle column. Pressing `Enter` on one of these will load all of the songs with that genre in the third column. Searching with the search field will fill the third column with songs whose genres match the search. Searching for a genre by typing it in should return the same songs as selecting it in the middle column. Note that genre searches may (depending on your Subsonic server's search implementation) be case sensitive. ## Advanced Configuration and Features diff --git a/help_text.go b/help_text.go index c958127..afece4b 100644 --- a/help_text.go +++ b/help_text.go @@ -52,6 +52,7 @@ artist, album, or song column Left previous column Right next column Enter/a recursively add item to quue + g toggle genre search / start search search field Enter search for text diff --git a/page_playlist.go b/page_playlist.go index 4ece28b..e59d123 100644 --- a/page_playlist.go +++ b/page_playlist.go @@ -251,11 +251,6 @@ func (p *PlaylistPage) UpdatePlaylists() { stop <- true return } - if response == nil { - p.logger.Printf("no error from GetPlaylists, but also no response!") - stop <- true - return - } p.updatingMutex.Lock() defer p.updatingMutex.Unlock() p.ui.playlists = response.Playlists.Playlists diff --git a/page_search.go b/page_search.go index e859192..766df00 100644 --- a/page_search.go +++ b/page_search.go @@ -5,6 +5,7 @@ package main import ( "fmt" + "slices" "sort" "strings" @@ -24,6 +25,7 @@ type SearchPage struct { albumList *tview.List songList *tview.List searchField *tview.InputField + queryGenre bool artists []*subsonic.Artist albums []*subsonic.Album @@ -108,12 +110,24 @@ func (ui *Ui) createSearchPage() *SearchPage { } return event case '/': + searchPage.searchField.SetLabel("search:") searchPage.ui.app.SetFocus(searchPage.searchField) return nil + case 'g': + if searchPage.queryGenre { + searchPage.albumList.SetTitle(" album matches ") + } else { + searchPage.albumList.SetTitle(" genres ") + searchPage.populateGenres() + searchPage.ui.app.SetFocus(searchPage.albumList) + } + searchPage.queryGenre = !searchPage.queryGenre + return nil } return event }) + search := make(chan string, 5) searchPage.albumList.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { switch event.Key() { case tcell.KeyLeft: @@ -123,19 +137,35 @@ func (ui *Ui) createSearchPage() *SearchPage { ui.app.SetFocus(searchPage.songList) return nil case tcell.KeyEnter: - if len(searchPage.albums) != 0 { + if !searchPage.queryGenre { idx := searchPage.albumList.GetCurrentItem() - searchPage.addAlbumToQueue(searchPage.albums[idx]) + if idx >= 0 && idx < len(searchPage.albums) { + searchPage.addAlbumToQueue(searchPage.albums[idx]) + return nil + } + return event + } else { + search <- "" + searchPage.artistList.Clear() + searchPage.artists = make([]*subsonic.Artist, 0) + searchPage.songList.Clear() + searchPage.songs = make([]*subsonic.SubsonicEntity, 0) + + idx := searchPage.albumList.GetCurrentItem() + // searchPage.logger.Printf("current item index = %d; albumList len = %d", idx, searchPage.albumList.GetItemCount()) + queryStr, _ := searchPage.albumList.GetItemText(idx) + search <- queryStr return nil } - return event } switch event.Rune() { case 'a': - if len(searchPage.albums) != 0 { - idx := searchPage.albumList.GetCurrentItem() - searchPage.logger.Printf("albumList adding (%d) %s", idx, searchPage.albums[idx].Name) + if searchPage.queryGenre { + return event + } + idx := searchPage.albumList.GetCurrentItem() + if idx >= 0 && idx < len(searchPage.albums) { searchPage.addAlbumToQueue(searchPage.albums[idx]) return nil } @@ -143,6 +173,16 @@ func (ui *Ui) createSearchPage() *SearchPage { case '/': searchPage.ui.app.SetFocus(searchPage.searchField) return nil + case 'g': + if searchPage.queryGenre { + searchPage.albumList.SetTitle(" album matches ") + } else { + searchPage.albumList.SetTitle(" genres ") + searchPage.populateGenres() + searchPage.ui.app.SetFocus(searchPage.albumList) + } + searchPage.queryGenre = !searchPage.queryGenre + return nil } return event @@ -177,11 +217,20 @@ func (ui *Ui) createSearchPage() *SearchPage { case '/': searchPage.ui.app.SetFocus(searchPage.searchField) return nil + case 'g': + if searchPage.queryGenre { + searchPage.albumList.SetTitle(" album matches ") + } else { + searchPage.albumList.SetTitle(" genres ") + searchPage.populateGenres() + searchPage.ui.app.SetFocus(searchPage.albumList) + } + searchPage.queryGenre = !searchPage.queryGenre + return nil } return event }) - search := make(chan string, 5) searchPage.searchField.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { switch event.Key() { case tcell.KeyUp, tcell.KeyESC: @@ -190,8 +239,10 @@ func (ui *Ui) createSearchPage() *SearchPage { search <- "" searchPage.artistList.Clear() searchPage.artists = make([]*subsonic.Artist, 0) - searchPage.albumList.Clear() - searchPage.albums = make([]*subsonic.Album, 0) + if !searchPage.queryGenre { + searchPage.albumList.Clear() + searchPage.albums = make([]*subsonic.Album, 0) + } searchPage.songList.Clear() searchPage.songs = make([]*subsonic.SubsonicEntity, 0) @@ -226,41 +277,62 @@ func (s *SearchPage) search(search chan string) { } case <-more: } - res, err := s.ui.connection.Search(query, artOff, albOff, songOff) + var res *subsonic.SubsonicResponse + var err error + if s.queryGenre { + res, err = s.ui.connection.GetSongsByGenre(query, songOff, "") + if len(res.SongsByGenre.Song) == 0 { + continue + } + } else { + res, err = s.ui.connection.Search(query, artOff, albOff, songOff) + // Quit searching if there are no more results + if len(res.SearchResults.Artist) == 0 && + len(res.SearchResults.Album) == 0 && + len(res.SearchResults.Song) == 0 { + continue + } + } if err != nil { s.logger.PrintError("SearchPage.search", err) return } - // Quit searching if there are no more results - if len(res.SearchResults.Artist) == 0 && - len(res.SearchResults.Album) == 0 && - len(res.SearchResults.Song) == 0 { - continue - } query = strings.ToLower(query) s.ui.app.QueueUpdate(func() { - for _, artist := range res.SearchResults.Artist { - if strings.Contains(strings.ToLower(artist.Name), query) { - s.artistList.AddItem(tview.Escape(artist.Name), "", 0, nil) - s.artists = append(s.artists, &artist) + if s.queryGenre { + if songOff == 0 { + s.artistList.Box.SetTitle(" artist matches ") + s.albumList.Box.SetTitle(" genres ") } - } - s.artistList.Box.SetTitle(fmt.Sprintf(" artist matches (%d) ", len(s.artists))) - for _, album := range res.SearchResults.Album { - if strings.Contains(strings.ToLower(album.Name), query) { - s.albumList.AddItem(tview.Escape(album.Name), "", 0, nil) - s.albums = append(s.albums, &album) - } - } - s.albumList.Box.SetTitle(fmt.Sprintf(" album matches (%d) ", len(s.albums))) - for _, song := range res.SearchResults.Song { - if strings.Contains(strings.ToLower(song.Title), query) { + for _, song := range res.SongsByGenre.Song { s.songList.AddItem(tview.Escape(song.Title), "", 0, nil) s.songs = append(s.songs, &song) } + s.songList.Box.SetTitle(fmt.Sprintf(" genre song matches (%d) ", len(s.songs))) + } else { + for _, artist := range res.SearchResults.Artist { + if strings.Contains(strings.ToLower(artist.Name), query) { + s.artistList.AddItem(tview.Escape(artist.Name), "", 0, nil) + s.artists = append(s.artists, &artist) + } + } + s.artistList.Box.SetTitle(fmt.Sprintf(" artist matches (%d) ", len(s.artists))) + for _, album := range res.SearchResults.Album { + if strings.Contains(strings.ToLower(album.Name), query) { + s.albumList.AddItem(tview.Escape(album.Name), "", 0, nil) + s.albums = append(s.albums, &album) + } + } + s.albumList.Box.SetTitle(fmt.Sprintf(" album matches (%d) ", len(s.albums))) + for _, song := range res.SearchResults.Song { + if strings.Contains(strings.ToLower(song.Title), query) { + s.songList.AddItem(tview.Escape(song.Title), "", 0, nil) + s.songs = append(s.songs, &song) + } + } + s.songList.Box.SetTitle(fmt.Sprintf(" song matches (%d) ", len(s.songs))) } - s.songList.Box.SetTitle(fmt.Sprintf(" song matches (%d) ", len(s.songs))) }) // Only do this the one time, to prevent loops from stealing the user's focus @@ -268,9 +340,14 @@ func (s *SearchPage) search(search chan string) { s.aproposFocus() } - artOff += len(res.SearchResults.Artist) - albOff += len(res.SearchResults.Album) - songOff += len(res.SearchResults.Song) + if !s.queryGenre { + artOff += len(res.SearchResults.Artist) + albOff += len(res.SearchResults.Album) + songOff += len(res.SearchResults.Song) + } else { + songOff += len(res.SongsByGenre.Song) + } + s.ui.app.Draw() more <- true } } @@ -339,3 +416,17 @@ func (s *SearchPage) aproposFocus() { s.ui.app.SetFocus(s.artistList) } } + +func (s *SearchPage) populateGenres() { + resp, err := s.ui.connection.GetGenres() + if err != nil { + s.logger.PrintError("populateGenres", err) + return + } + slices.SortFunc(resp.Genres.Genres, func(a, b subsonic.GenreEntry) int { + return strings.Compare(a.Name, b.Name) + }) + for _, entry := range resp.Genres.Genres { + s.albumList.AddItem(tview.Escape(entry.Name), "", 0, nil) + } +} diff --git a/subsonic/api.go b/subsonic/api.go index 9c64041..8eb2a85 100644 --- a/subsonic/api.go +++ b/subsonic/api.go @@ -131,6 +131,16 @@ type PlayQueue struct { Entries SubsonicEntities `json:"entry"` } +type GenreEntries struct { + Genres []GenreEntry `json:"genre"` +} + +type GenreEntry struct { + SongCount int `json:"songCount"` + AlbumCount int `json:"albumCount"` + Name string `json:"value"` +} + type Artist struct { Id string `json:"id"` Name string `json:"name"` @@ -278,6 +288,8 @@ type SubsonicResponse struct { SearchResults SubsonicResults `json:"searchResult3"` ScanStatus ScanStatus `json:"scanStatus"` PlayQueue PlayQueue `json:"playQueue"` + Genres GenreEntries `json:"genres"` + SongsByGenre SubsonicSongs `json:"songsByGenre"` } type responseWrapper struct { @@ -688,3 +700,30 @@ func (connection *SubsonicConnection) LoadPlayQueue() (*SubsonicResponse, error) requestUrl := fmt.Sprintf("%s/rest/getPlayQueue?%s", connection.Host, query.Encode()) return connection.getResponse("GetPlayQueue", requestUrl) } + +func (connection *SubsonicConnection) GetGenres() (*SubsonicResponse, error) { + query := defaultQuery(connection) + requestUrl := connection.Host + "/rest/getGenres" + "?" + query.Encode() + resp, err := connection.getResponse("GetGenres", requestUrl) + if err != nil { + return resp, err + } + return resp, nil +} + +func (connection *SubsonicConnection) GetSongsByGenre(genre string, offset int, musicFolderID string) (*SubsonicResponse, error) { + query := defaultQuery(connection) + query.Add("genre", genre) + if offset != 0 { + query.Add("offset", strconv.Itoa(offset)) + } + if musicFolderID != "" { + query.Add("musicFolderId", musicFolderID) + } + requestUrl := connection.Host + "/rest/getSongsByGenre" + "?" + query.Encode() + resp, err := connection.getResponse("GetPlaylists", requestUrl) + if err != nil { + return resp, err + } + return resp, nil +} From 8c70ad82e796244dd176ecc10fc0f1ffafb35100 Mon Sep 17 00:00:00 2001 From: "Sean E. Russell" Date: Fri, 18 Oct 2024 10:53:58 -0500 Subject: [PATCH 4/6] fix: clarifies search help text --- help_text.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/help_text.go b/help_text.go index afece4b..f7e4a9f 100644 --- a/help_text.go +++ b/help_text.go @@ -47,13 +47,19 @@ a add playlist or song to queue ` const helpSearchPage = ` -artist, album, or song column +artist, album/genre, or song column Down/Up navigate within the column Left previous column Right next column Enter/a recursively add item to quue g toggle genre search / start search +In album tab + Enter/a recursively add item to quue +In genre tab + Enter shows songs with genre +In song tab + Enter/a adds song to queue search field Enter search for text Esc cancel search From cfef2414fc99546c279ef6172fe07b4d6f706de8 Mon Sep 17 00:00:00 2001 From: "Sean E. Russell" Date: Fri, 18 Oct 2024 11:43:15 -0500 Subject: [PATCH 5/6] feat: add entire genre; show item counts in queue and genre columns --- gui_handlers.go | 4 +++- help_text.go | 4 +--- mpvplayer/player.go | 4 ++-- mpvplayer/queue_item.go | 5 +++++ page_queue.go | 1 + page_search.go | 48 +++++++++++++++++++++++++++++++++-------- subsonic/api.go | 1 + 7 files changed, 52 insertions(+), 15 deletions(-) diff --git a/gui_handlers.go b/gui_handlers.go index e0294d2..1033c06 100644 --- a/gui_handlers.go +++ b/gui_handlers.go @@ -191,6 +191,7 @@ func (ui *Ui) addSongToQueue(entity *subsonic.SubsonicEntity) { TrackNumber: entity.Track, CoverArtId: entity.CoverArtId, DiscNumber: entity.DiscNumber, + Year: entity.Year, } ui.player.AddToQueue(queueItem) } @@ -206,6 +207,7 @@ func makeSongHandler(entity *subsonic.SubsonicEntity, ui *Ui, fallbackArtist str track := entity.Track coverArtId := entity.CoverArtId disc := entity.DiscNumber + year := entity.Year response, err := ui.connection.GetAlbum(entity.Parent) album := "" @@ -223,7 +225,7 @@ func makeSongHandler(entity *subsonic.SubsonicEntity, ui *Ui, fallbackArtist str } return func() { - if err := ui.player.PlayUri(id, uri, title, artist, album, duration, track, disc, coverArtId); err != nil { + if err := ui.player.PlayUri(id, uri, title, artist, album, duration, track, disc, coverArtId, year); err != nil { ui.logger.PrintError("SongHandler Play", err) return } diff --git a/help_text.go b/help_text.go index f7e4a9f..2ffbc41 100644 --- a/help_text.go +++ b/help_text.go @@ -55,11 +55,9 @@ artist, album/genre, or song column g toggle genre search / start search In album tab - Enter/a recursively add item to quue + Enter recursively add item to quue In genre tab Enter shows songs with genre -In song tab - Enter/a adds song to queue search field Enter search for text Esc cancel search diff --git a/mpvplayer/player.go b/mpvplayer/player.go index febe14e..d71d3c5 100644 --- a/mpvplayer/player.go +++ b/mpvplayer/player.go @@ -125,8 +125,8 @@ func (p *Player) PlayNextTrack() error { return nil } -func (p *Player) PlayUri(id, uri, title, artist, album string, duration, track, disc int, coverArtId string) error { - p.queue = []QueueItem{{id, uri, title, artist, duration, album, track, coverArtId, disc}} +func (p *Player) PlayUri(id, uri, title, artist, album string, duration, track, disc int, coverArtId string, year int) error { + p.queue = []QueueItem{{id, uri, title, artist, duration, album, track, coverArtId, disc, year}} p.replaceInProgress = true if ip, e := p.IsPaused(); ip && e == nil { if err := p.Pause(); err != nil { diff --git a/mpvplayer/queue_item.go b/mpvplayer/queue_item.go index 2b147e4..04fb54c 100644 --- a/mpvplayer/queue_item.go +++ b/mpvplayer/queue_item.go @@ -17,6 +17,7 @@ type QueueItem struct { TrackNumber int CoverArtId string DiscNumber int + Year int } var _ remote.TrackInterface = (*QueueItem)(nil) @@ -60,3 +61,7 @@ func (q QueueItem) GetTrackNumber() int { func (q QueueItem) GetDiscNumber() int { return q.DiscNumber } + +func (q QueueItem) GetYear() int { + return q.Year +} diff --git a/page_queue.go b/page_queue.go index 500dcea..6810edf 100644 --- a/page_queue.go +++ b/page_queue.go @@ -268,6 +268,7 @@ func (q *QueuePage) updateQueue() { q.queueList.ScrollToBeginning() } + q.queueList.Box.SetTitle(fmt.Sprintf(" queue (%d) ", q.queueList.GetRowCount())) r, c := q.queueList.GetSelection() q.changeSelection(r, c) } diff --git a/page_search.go b/page_search.go index 766df00..2350798 100644 --- a/page_search.go +++ b/page_search.go @@ -117,8 +117,8 @@ func (ui *Ui) createSearchPage() *SearchPage { if searchPage.queryGenre { searchPage.albumList.SetTitle(" album matches ") } else { - searchPage.albumList.SetTitle(" genres ") searchPage.populateGenres() + searchPage.albumList.SetTitle(fmt.Sprintf(" genres (%d) ", searchPage.albumList.GetItemCount())) searchPage.ui.app.SetFocus(searchPage.albumList) } searchPage.queryGenre = !searchPage.queryGenre @@ -162,7 +162,12 @@ func (ui *Ui) createSearchPage() *SearchPage { switch event.Rune() { case 'a': if searchPage.queryGenre { - return event + idx := searchPage.albumList.GetCurrentItem() + if idx < searchPage.albumList.GetItemCount() { + genre, _ := searchPage.albumList.GetItemText(idx) + searchPage.addGenreToQueue(genre) + } + return nil } idx := searchPage.albumList.GetCurrentItem() if idx >= 0 && idx < len(searchPage.albums) { @@ -177,8 +182,8 @@ func (ui *Ui) createSearchPage() *SearchPage { if searchPage.queryGenre { searchPage.albumList.SetTitle(" album matches ") } else { - searchPage.albumList.SetTitle(" genres ") searchPage.populateGenres() + searchPage.albumList.SetTitle(fmt.Sprintf(" genres (%d) ", searchPage.albumList.GetItemCount())) searchPage.ui.app.SetFocus(searchPage.albumList) } searchPage.queryGenre = !searchPage.queryGenre @@ -221,8 +226,8 @@ func (ui *Ui) createSearchPage() *SearchPage { if searchPage.queryGenre { searchPage.albumList.SetTitle(" album matches ") } else { - searchPage.albumList.SetTitle(" genres ") searchPage.populateGenres() + searchPage.albumList.SetTitle(fmt.Sprintf(" genres (%d) ", searchPage.albumList.GetItemCount())) searchPage.ui.app.SetFocus(searchPage.albumList) } searchPage.queryGenre = !searchPage.queryGenre @@ -262,6 +267,8 @@ func (s *SearchPage) search(search chan string) { var query string var artOff, albOff, songOff int more := make(chan bool, 5) + var res *subsonic.SubsonicResponse + var err error for { // quit searching if we receive an interrupt select { @@ -277,11 +284,11 @@ func (s *SearchPage) search(search chan string) { } case <-more: } - var res *subsonic.SubsonicResponse - var err error if s.queryGenre { + s.logger.Printf("genre %q %d", query, songOff) res, err = s.ui.connection.GetSongsByGenre(query, songOff, "") if len(res.SongsByGenre.Song) == 0 { + s.logger.Printf("found a total of %d songs", songOff) continue } } else { @@ -298,19 +305,19 @@ func (s *SearchPage) search(search chan string) { return } - query = strings.ToLower(query) s.ui.app.QueueUpdate(func() { if s.queryGenre { if songOff == 0 { s.artistList.Box.SetTitle(" artist matches ") - s.albumList.Box.SetTitle(" genres ") } for _, song := range res.SongsByGenre.Song { s.songList.AddItem(tview.Escape(song.Title), "", 0, nil) s.songs = append(s.songs, &song) } s.songList.Box.SetTitle(fmt.Sprintf(" genre song matches (%d) ", len(s.songs))) + songOff += len(res.SongsByGenre.Song) } else { + query = strings.ToLower(query) for _, artist := range res.SearchResults.Artist { if strings.Contains(strings.ToLower(artist.Name), query) { s.artistList.AddItem(tview.Escape(artist.Name), "", 0, nil) @@ -332,7 +339,11 @@ func (s *SearchPage) search(search chan string) { } } s.songList.Box.SetTitle(fmt.Sprintf(" song matches (%d) ", len(s.songs))) + artOff += len(res.SearchResults.Artist) + albOff += len(res.SearchResults.Album) + songOff += len(res.SearchResults.Song) } + more <- true }) // Only do this the one time, to prevent loops from stealing the user's focus @@ -348,10 +359,29 @@ func (s *SearchPage) search(search chan string) { songOff += len(res.SongsByGenre.Song) } s.ui.app.Draw() - more <- true } } +func (s *SearchPage) addGenreToQueue(query string) { + var songOff int + for { + res, err := s.ui.connection.GetSongsByGenre(query, songOff, "") + if err != nil { + s.logger.PrintError("SearchPage.addGenreToQueue", err) + return + } + if len(res.SongsByGenre.Song) == 0 { + break + } + for _, song := range res.SongsByGenre.Song { + s.ui.addSongToQueue(&song) + } + songOff += len(res.SongsByGenre.Song) + } + s.logger.Printf("added a total of %d songs to the queue for %q", songOff, query) + s.ui.queuePage.UpdateQueue() +} + func (s *SearchPage) addArtistToQueue(entity subsonic.Ider) { response, err := s.ui.connection.GetArtist(entity.ID()) if err != nil { diff --git a/subsonic/api.go b/subsonic/api.go index 8eb2a85..638864a 100644 --- a/subsonic/api.go +++ b/subsonic/api.go @@ -193,6 +193,7 @@ type SubsonicEntity struct { DiscNumber int `json:"discNumber"` Path string `json:"path"` CoverArtId string `json:"coverArt"` + Year int `json:"year"` } func (s SubsonicEntity) ID() string { From 5cac4ace8bcc1b3ead0bfc7d8923763c8aa868ca Mon Sep 17 00:00:00 2001 From: "Sean E. Russell" Date: Mon, 28 Oct 2024 11:26:48 -0500 Subject: [PATCH 6/6] fix: clear search columns when genre search mode changes fix: make sure _something_ other than the search field gets focused on Enter --- page_search.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/page_search.go b/page_search.go index 2350798..b2cde8d 100644 --- a/page_search.go +++ b/page_search.go @@ -114,6 +114,9 @@ func (ui *Ui) createSearchPage() *SearchPage { searchPage.ui.app.SetFocus(searchPage.searchField) return nil case 'g': + searchPage.albumList.Clear() + searchPage.artistList.Clear() + searchPage.songList.Clear() if searchPage.queryGenre { searchPage.albumList.SetTitle(" album matches ") } else { @@ -179,6 +182,9 @@ func (ui *Ui) createSearchPage() *SearchPage { searchPage.ui.app.SetFocus(searchPage.searchField) return nil case 'g': + searchPage.albumList.Clear() + searchPage.artistList.Clear() + searchPage.songList.Clear() if searchPage.queryGenre { searchPage.albumList.SetTitle(" album matches ") } else { @@ -223,6 +229,9 @@ func (ui *Ui) createSearchPage() *SearchPage { searchPage.ui.app.SetFocus(searchPage.searchField) return nil case 'g': + searchPage.albumList.Clear() + searchPage.artistList.Clear() + searchPage.songList.Clear() if searchPage.queryGenre { searchPage.albumList.SetTitle(" album matches ") } else { @@ -253,6 +262,7 @@ func (ui *Ui) createSearchPage() *SearchPage { queryStr := searchPage.searchField.GetText() search <- queryStr + searchPage.aproposFocus() default: return event }