220 lines
6.4 KiB
Go
220 lines
6.4 KiB
Go
package model
|
|
|
|
import (
|
|
"strings"
|
|
|
|
tea "charm.land/bubbletea/v2"
|
|
|
|
"github.com/bjarneo/cliamp/playlist"
|
|
"github.com/bjarneo/cliamp/provider"
|
|
)
|
|
|
|
// resetProviderNav resets provider navigation and search state to the top.
|
|
func (m *Model) resetProviderNav() {
|
|
m.provCursor = 0
|
|
m.provScroll = 0
|
|
m.provLoading = true
|
|
m.provSearch.active = false
|
|
m.provSearch.query = ""
|
|
m.provSearch.results = nil
|
|
m.provSearch.cursor = 0
|
|
m.provSearch.scroll = 0
|
|
}
|
|
|
|
// StartInProvider configures the model to begin in the provider browse view.
|
|
// Call this from main when no CLI tracks or pending URLs were given.
|
|
func (m *Model) StartInProvider() {
|
|
if m.provider != nil {
|
|
m.focus = focusProvider
|
|
m.resetProviderNav()
|
|
}
|
|
}
|
|
|
|
// switchProvider sets the active provider by pill index and fetches its playlists.
|
|
func (m *Model) switchProvider(idx int) tea.Cmd {
|
|
if idx < 0 || idx >= len(m.providers) {
|
|
return nil
|
|
}
|
|
m.provPillIdx = idx
|
|
m.provider = m.providers[idx].Provider
|
|
m.providerLists = nil
|
|
m.provSignIn = false
|
|
m.catalogBatch = catalogBatchState{}
|
|
m.activeProviderPlaylistID = ""
|
|
m.resetProviderNav()
|
|
m.focus = focusProvider
|
|
return fetchPlaylistsCmd(m.provider)
|
|
}
|
|
|
|
// quickSwitchProvider closes any browser overlays and jumps to the provider
|
|
// matched by key. Use the same Shift+letter shortcuts that switch providers
|
|
// from the main pane (S, N, P, J, E, Y, M, R, L). Returns nil when the key doesn't
|
|
// match a known provider.
|
|
func (m *Model) quickSwitchProvider(key string) tea.Cmd {
|
|
provKey := providerKeyForShortcut(key)
|
|
if provKey == "" {
|
|
return nil
|
|
}
|
|
// Close any open overlays so the user lands on the provider pane.
|
|
m.navBrowser.visible = false
|
|
m.plManager.visible = false
|
|
m.fileBrowser.visible = false
|
|
return m.switchToProvider(provKey)
|
|
}
|
|
|
|
// providerKeyForShortcut maps the Shift+letter provider shortcuts to the
|
|
// config key used by switchToProvider, or "" when the key is unrelated.
|
|
func providerKeyForShortcut(key string) string {
|
|
switch key {
|
|
case "S":
|
|
return "spotify"
|
|
case "N":
|
|
return "navidrome"
|
|
case "P":
|
|
return "plex"
|
|
case "J":
|
|
return "jellyfin"
|
|
case "E":
|
|
return "emby"
|
|
case "Y":
|
|
return "yt"
|
|
case "M":
|
|
return "netease"
|
|
case "L":
|
|
return "local"
|
|
case "R":
|
|
return "radio"
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// switchToProvider finds a provider by config key and switches to it.
|
|
// Returns nil if the provider is not configured.
|
|
func (m *Model) switchToProvider(key string) tea.Cmd {
|
|
for i, pe := range m.providers {
|
|
if pe.Key == key {
|
|
return m.switchProvider(i)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// SetPendingURLs stores remote URLs (feeds, M3U) for async resolution after Init.
|
|
func (m *Model) SetPendingURLs(urls []string) {
|
|
m.pendingURLs = urls
|
|
m.feedLoading = len(urls) > 0
|
|
}
|
|
|
|
// SetLoadedPlaylist records that the live queue exactly mirrors a local saved
|
|
// playlist, allowing path-based write-backs such as bookmarks and removals.
|
|
func (m *Model) SetLoadedPlaylist(name string) {
|
|
m.loadedPlaylist = name
|
|
}
|
|
|
|
// findBrowseProvider returns the first provider that supports browsing
|
|
// (ArtistBrowser or AlbumBrowser), preferring the active provider.
|
|
func (m *Model) findBrowseProvider() playlist.Provider {
|
|
return m.findProviderWith(func(p playlist.Provider) bool {
|
|
if _, ok := p.(provider.ArtistBrowser); ok {
|
|
return true
|
|
}
|
|
_, ok := p.(provider.AlbumBrowser)
|
|
return ok
|
|
})
|
|
}
|
|
|
|
func (m *Model) openNavBrowserWith(prov playlist.Provider) {
|
|
m.navBrowser.prov = prov
|
|
m.navBrowser.visible = true
|
|
m.navBrowser.mode = navBrowseModeMenu
|
|
m.navBrowser.screen = navBrowseScreenList
|
|
m.navBrowser.cursor = 0
|
|
m.navBrowser.scroll = 0
|
|
m.navBrowser.artists = nil
|
|
m.navBrowser.albums = nil
|
|
m.navBrowser.tracks = nil
|
|
m.navBrowser.loading = false
|
|
m.navBrowser.albumLoading = false
|
|
m.navBrowser.albumDone = false
|
|
m.navBrowser.searching = false
|
|
m.navBrowser.search = ""
|
|
m.navBrowser.searchIdx = nil
|
|
m.navBrowser.selArtist = provider.ArtistInfo{}
|
|
m.navBrowser.selAlbum = provider.AlbumInfo{}
|
|
if ab, ok := prov.(provider.AlbumBrowser); ok {
|
|
m.navBrowser.sortType = ab.DefaultAlbumSort()
|
|
} else {
|
|
m.navBrowser.sortType = ""
|
|
}
|
|
}
|
|
|
|
// navUpdateSearch rebuilds navSearchIdx from the current navSearch query
|
|
// against whichever list is active on the current nav screen.
|
|
func (m *Model) navUpdateSearch() {
|
|
q := strings.ToLower(m.navBrowser.search)
|
|
if q == "" {
|
|
m.navBrowser.searchIdx = nil
|
|
return
|
|
}
|
|
m.navBrowser.searchIdx = nil
|
|
switch {
|
|
case m.navBrowser.mode == navBrowseModeByArtist && m.navBrowser.screen == navBrowseScreenList,
|
|
m.navBrowser.mode == navBrowseModeByArtistAlbum && m.navBrowser.screen == navBrowseScreenList:
|
|
for i, a := range m.navBrowser.artists {
|
|
if strings.Contains(strings.ToLower(a.Name), q) {
|
|
m.navBrowser.searchIdx = append(m.navBrowser.searchIdx, i)
|
|
}
|
|
}
|
|
case m.navBrowser.mode == navBrowseModeByAlbum && m.navBrowser.screen == navBrowseScreenList,
|
|
m.navBrowser.mode == navBrowseModeByArtistAlbum && m.navBrowser.screen == navBrowseScreenAlbums:
|
|
for i, a := range m.navBrowser.albums {
|
|
if strings.Contains(strings.ToLower(a.Name), q) ||
|
|
strings.Contains(strings.ToLower(a.Artist), q) {
|
|
m.navBrowser.searchIdx = append(m.navBrowser.searchIdx, i)
|
|
}
|
|
}
|
|
case m.navBrowser.screen == navBrowseScreenTracks:
|
|
for i, t := range m.navBrowser.tracks {
|
|
if strings.Contains(strings.ToLower(t.Title), q) ||
|
|
strings.Contains(strings.ToLower(t.Artist), q) ||
|
|
strings.Contains(strings.ToLower(t.Album), q) {
|
|
m.navBrowser.searchIdx = append(m.navBrowser.searchIdx, i)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// navClearSearch resets the nav search state.
|
|
func (m *Model) navClearSearch() {
|
|
m.navBrowser.searching = false
|
|
m.navBrowser.search = ""
|
|
m.navBrowser.searchIdx = nil
|
|
m.navBrowser.cursor = 0
|
|
m.navBrowser.scroll = 0
|
|
}
|
|
|
|
// fetchNavArtistAllTracksCmd first fetches the artist's album list, then fetches
|
|
// all tracks across every album. This is used by the "By Artist" browse mode.
|
|
// The provider must implement both ArtistBrowser and AlbumTrackLoader.
|
|
func (m *Model) fetchNavArtistAllTracksCmd(ab provider.ArtistBrowser, artistID string) tea.Cmd {
|
|
loader, _ := m.navBrowser.prov.(provider.AlbumTrackLoader)
|
|
return func() tea.Msg {
|
|
albums, err := ab.ArtistAlbums(artistID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if loader == nil {
|
|
return navTracksLoadedMsg(nil)
|
|
}
|
|
var all []playlist.Track
|
|
for _, album := range albums {
|
|
tracks, err := loader.AlbumTracks(album.ID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
all = append(all, tracks...)
|
|
}
|
|
return navTracksLoadedMsg(all)
|
|
}
|
|
}
|