Skip to content

Commit

Permalink
feat: #52, search by genre
Browse files Browse the repository at this point in the history
  • Loading branch information
xxxserxxx committed Oct 18, 2024
1 parent 6c02269 commit 9773899
Show file tree
Hide file tree
Showing 5 changed files with 170 additions and 34 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,12 +154,15 @@ 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.

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

### MPRIS2 Integration
Expand Down
1 change: 1 addition & 0 deletions help_text.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ artist, album, or song tab
Right next column
Enter recursively add item to quue
a recursively add item to quue
g toggle genre search
/ start search
search field
Enter search for text
Expand Down
3 changes: 3 additions & 0 deletions page_playlist.go
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,9 @@ func (p *PlaylistPage) UpdatePlaylists() {
response, err := p.ui.connection.GetPlaylists()
if err != nil {
p.logger.PrintError("GetPlaylists", err)
p.isUpdating = false
stop <- true
return
}
p.updatingMutex.Lock()
defer p.updatingMutex.Unlock()
Expand Down
158 changes: 124 additions & 34 deletions page_search.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package main

import (
"fmt"
"slices"
"sort"
"strings"

Expand All @@ -24,6 +25,7 @@ type SearchPage struct {
albumList *tview.List
songList *tview.List
searchField *tview.InputField
queryGenre bool

artists []*subsonic.Artist
albums []*subsonic.Album
Expand Down Expand Up @@ -99,12 +101,24 @@ func (ui *Ui) createSearchPage() *SearchPage {
searchPage.addArtistToQueue(searchPage.artists[idx])
return nil
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:
Expand All @@ -114,20 +128,47 @@ func (ui *Ui) createSearchPage() *SearchPage {
ui.app.SetFocus(searchPage.songList)
return nil
case tcell.KeyEnter:
idx := searchPage.albumList.GetCurrentItem()
searchPage.addAlbumToQueue(searchPage.albums[idx])
return nil
if !searchPage.queryGenre {
idx := searchPage.albumList.GetCurrentItem()
searchPage.addAlbumToQueue(searchPage.albums[idx])
return nil
} 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
}
}

switch event.Rune() {
case 'a':
if searchPage.queryGenre {
return event
}
idx := searchPage.albumList.GetCurrentItem()
searchPage.logger.Printf("albumList adding (%d) %s", idx, searchPage.albums[idx].Name)
searchPage.addAlbumToQueue(searchPage.albums[idx])
return nil
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
Expand Down Expand Up @@ -156,11 +197,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:
Expand All @@ -177,8 +227,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)

Expand All @@ -205,56 +257,80 @@ 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
}
if query == "" {
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)
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)))
})

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
}
}
Expand Down Expand Up @@ -311,3 +387,17 @@ func (s *SearchPage) addAlbumToQueue(entity subsonic.Ider) {
}
s.ui.queuePage.UpdateQueue()
}

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)
}
}
39 changes: 39 additions & 0 deletions subsonic/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,16 @@ type ScanStatus struct {
Count int `json:"count"`
}

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"`
Expand Down Expand Up @@ -271,6 +281,8 @@ type SubsonicResponse struct {
Album Album `json:"album"`
SearchResults SubsonicResults `json:"searchResult3"`
ScanStatus ScanStatus `json:"scanStatus"`
Genres GenreEntries `json:"genres"`
SongsByGenre SubsonicSongs `json:"songsByGenre"`
}

type responseWrapper struct {
Expand Down Expand Up @@ -663,3 +675,30 @@ func (connection *SubsonicConnection) StartScan() error {
}
return nil
}

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
}

0 comments on commit 9773899

Please sign in to comment.