608 lines
15 KiB
Go
608 lines
15 KiB
Go
package views
|
|
|
|
import (
|
|
"fmt"
|
|
"strconv"
|
|
"time"
|
|
|
|
"git.dayanhub.com/sagi/subsonic-tui/internal/client"
|
|
"git.dayanhub.com/sagi/subsonic-tui/internal/common"
|
|
"git.dayanhub.com/sagi/subsonic-tui/internal/config"
|
|
"github.com/delucks/go-subsonic"
|
|
"github.com/gdamore/tcell/v2"
|
|
"github.com/rivo/tview"
|
|
)
|
|
|
|
var _ View = &main{}
|
|
|
|
type mainviewmode int
|
|
|
|
const (
|
|
mainModeAlbum mainviewmode = iota
|
|
mainModePlaylist mainviewmode = iota
|
|
mainModeArtist mainviewmode = iota
|
|
mainModeSearch mainviewmode = iota
|
|
)
|
|
|
|
type main struct {
|
|
view *tview.Flex
|
|
client *client.Client
|
|
mode mainviewmode
|
|
query string
|
|
miniView bool
|
|
album *subsonic.AlbumID3
|
|
searchResult *subsonic.SearchResult3
|
|
playlist *subsonic.Playlist
|
|
songList *tview.Table
|
|
artist *subsonic.ArtistID3
|
|
artistInfo *subsonic.ArtistInfo2
|
|
playAllFunc func(song ...*subsonic.Child)
|
|
addSongsFunc func(song ...*subsonic.Child)
|
|
log func(format string, a ...any)
|
|
}
|
|
|
|
func NewMainView(client *client.Client, log func(format string, a ...any)) *main {
|
|
playlistAlbum := &main{
|
|
client: client,
|
|
log: log,
|
|
}
|
|
|
|
flex := tview.NewFlex().SetDirection(tview.FlexRow)
|
|
|
|
flex.SetBackgroundColor(tcell.ColorBlack)
|
|
flex.SetTitle("Subsonic TUI [`]")
|
|
flex.SetBorder(true)
|
|
flex.SetFocusFunc(func() {
|
|
flex.SetBorderColor(tcell.ColorRed)
|
|
if playlistAlbum.songList != nil {
|
|
//playlistAlbum.view.Blur()
|
|
playlistAlbum.songList.Focus(nil)
|
|
}
|
|
})
|
|
flex.SetBlurFunc(func() {
|
|
flex.SetBorderColor(tcell.ColorWhite)
|
|
if playlistAlbum.songList != nil {
|
|
playlistAlbum.songList.Blur()
|
|
}
|
|
})
|
|
|
|
playlistAlbum.view = flex
|
|
// Empty Box for starters...
|
|
playlistAlbum.view.AddItem(tview.NewBox(), 0, 1, false)
|
|
return playlistAlbum
|
|
}
|
|
|
|
func (m *main) SetMiniView(mini bool) {
|
|
m.miniView = mini
|
|
}
|
|
|
|
func (m *main) SetAlbum(album *subsonic.AlbumID3) {
|
|
m.mode = mainModeAlbum
|
|
m.album = album
|
|
m.Update()
|
|
m.log("")
|
|
}
|
|
|
|
func (m *main) SetPlaylist(playlist *subsonic.Playlist) {
|
|
m.mode = mainModePlaylist
|
|
m.playlist = playlist
|
|
m.Update()
|
|
m.log("")
|
|
}
|
|
|
|
func (m *main) SetArtist(artist *subsonic.ArtistID3) {
|
|
m.mode = mainModeArtist
|
|
m.artist = artist
|
|
info, _ := m.client.GetArtistInfo(artist.ID)
|
|
m.artistInfo = info
|
|
m.Update()
|
|
m.log("")
|
|
}
|
|
|
|
func (m *main) SetSearch(result *subsonic.SearchResult3, query string) {
|
|
m.mode = mainModeSearch
|
|
m.searchResult = result
|
|
m.query = query
|
|
m.Update()
|
|
m.log("Found #%d artists, #%d albums and #%d songs",
|
|
len(result.Artist),
|
|
len(result.Album),
|
|
len(result.Song),
|
|
)
|
|
}
|
|
|
|
func (m *main) drawPlaylist() {
|
|
subtitle := fmt.Sprintf("%s\n\nCreated by: %s | %s", m.playlist.Comment, m.playlist.Owner, time.Duration(m.playlist.Duration*int(time.Second)).String())
|
|
m.populateHeader(m.playlist.Name, subtitle, m.playlist.Duration, m.playlist.CoverArt)
|
|
playBtn := m.drawPlaylistAlbumButtons(m.playlist.Entry)
|
|
m.populateSongs(m.playlist.Entry, playBtn)
|
|
}
|
|
|
|
func (m *main) drawAlbum() {
|
|
subtitle := fmt.Sprintf("%s\n\n%d | %s", m.album.Artist, m.album.Year, time.Duration(m.album.Duration*int(time.Second)).String())
|
|
m.populateHeader(m.album.Name, subtitle, m.album.Duration, m.album.CoverArt)
|
|
playBtn := m.drawPlaylistAlbumButtons(m.album.Song)
|
|
m.populateSongs(m.album.Song, playBtn)
|
|
|
|
}
|
|
|
|
func (m *main) drawArtist() {
|
|
m.populateHeader(m.artist.Name, m.artistInfo.Biography, 0, m.artist.CoverArt)
|
|
btn := m.drawArtistButtons()
|
|
m.populateAlbums(btn)
|
|
}
|
|
|
|
func (m *main) drawSearch() {
|
|
sub := fmt.Sprintf("Query: %s", m.query)
|
|
m.populateHeader("Search Results", sub, 0, "")
|
|
m.populateSearchResults()
|
|
}
|
|
|
|
func (m *main) Update() {
|
|
m.view.Clear()
|
|
switch m.mode {
|
|
case mainModeAlbum:
|
|
m.drawAlbum()
|
|
case mainModePlaylist:
|
|
m.drawPlaylist()
|
|
case mainModeArtist:
|
|
m.drawArtist()
|
|
case mainModeSearch:
|
|
m.drawSearch()
|
|
}
|
|
m.songList.Focus(nil)
|
|
}
|
|
|
|
func (m *main) drawArtistButtons() *tview.Button {
|
|
// Add buttons: Radio
|
|
songs, _ := m.client.GetTopSongs(m.artist.Name, 10)
|
|
f := tview.NewFlex()
|
|
f.SetBackgroundColor(tcell.ColorBlack)
|
|
f.SetBorderPadding(0, 0, 2, 2)
|
|
// Buttons
|
|
radio := tview.NewButton("Radio")
|
|
top10 := tview.NewButton("Top 10")
|
|
// Button callbacks
|
|
top10.SetSelectedFunc(func() {
|
|
top10.Blur()
|
|
m.log("Playing %s's top 10 songs", m.artist.Name)
|
|
m.playAllFunc(songs...)
|
|
m.songList.Focus(nil)
|
|
})
|
|
radio.SetSelectedFunc(func() {
|
|
radio.Blur()
|
|
m.log("Generating %s's radio", m.artist.Name)
|
|
go func() {
|
|
songs := []*subsonic.Child{}
|
|
if config.ExperimentalRadioAlgo() {
|
|
songs, _ = m.client.GetExperimentalArtistRadio(m.artist, m.artistInfo, config.MaxRadioSongs())
|
|
}
|
|
if len(songs) == 0 {
|
|
songs, _ = m.client.GetSimilarSongs(m.artist.ID, config.MaxRadioSongs())
|
|
}
|
|
|
|
common.ShuffleSlice(songs)
|
|
m.playAllFunc(songs...)
|
|
}()
|
|
m.songList.Focus(nil)
|
|
})
|
|
radio.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
|
switch event.Key() {
|
|
case tcell.KeyDown:
|
|
radio.Blur()
|
|
m.songList.Focus(nil)
|
|
return nil
|
|
case tcell.KeyRight:
|
|
radio.Blur()
|
|
top10.Focus(nil)
|
|
return nil
|
|
}
|
|
return event
|
|
})
|
|
top10.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
|
switch event.Key() {
|
|
case tcell.KeyDown:
|
|
top10.Blur()
|
|
m.songList.Focus(nil)
|
|
return nil
|
|
case tcell.KeyLeft:
|
|
top10.Blur()
|
|
radio.Focus(nil)
|
|
return nil
|
|
}
|
|
return event
|
|
})
|
|
|
|
f.AddItem(radio, 0, 1, false)
|
|
f.AddItem(tview.NewBox(), 0, 1, false)
|
|
if len(songs) > 0 {
|
|
f.AddItem(top10, 0, 1, false)
|
|
}
|
|
f.AddItem(tview.NewBox(), 0, 1, false)
|
|
|
|
// Add the buttons to the view
|
|
m.view.AddItem(f, 1, 0, false)
|
|
// Margin bottom of 1 line
|
|
m.view.AddItem(tview.NewBox(), 1, 0, false)
|
|
|
|
return radio
|
|
}
|
|
|
|
func (m *main) populateAlbums(btn *tview.Button) {
|
|
table := tview.NewTable()
|
|
table.SetWrapSelection(true, false)
|
|
for i, album := range m.artist.Album {
|
|
year := tview.NewTableCell(fmt.Sprintf("%d", album.Year)).SetTextColor(tcell.ColorYellow)
|
|
name := tview.NewTableCell(album.Name).SetExpansion(2).SetAlign(tview.AlignCenter)
|
|
d := time.Second * time.Duration(album.Duration)
|
|
duration := tview.NewTableCell(d.String()).SetAlign(tview.AlignRight).SetExpansion(1)
|
|
|
|
table.SetCell(i, 0, year)
|
|
table.SetCell(i, 1, name)
|
|
table.SetCell(i, 2, duration)
|
|
}
|
|
|
|
table.SetFocusFunc(func() {
|
|
m.view.SetBorderColor(tcell.ColorRed)
|
|
table.SetSelectable(true, false)
|
|
})
|
|
table.SetBlurFunc(func() {
|
|
m.view.SetBorderColor(tcell.ColorWhite)
|
|
table.SetSelectable(false, false)
|
|
})
|
|
table.SetSelectedFunc(func(row, column int) {
|
|
alb, _ := m.client.GetAlbum(m.artist.Album[row].ID)
|
|
m.SetAlbum(alb)
|
|
m.view.Focus(nil)
|
|
})
|
|
table.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
|
row, _ := m.songList.GetSelection()
|
|
if row == 0 && event.Key() == tcell.KeyUp {
|
|
table.Blur()
|
|
m.view.SetBorderColor(tcell.ColorRed)
|
|
btn.Focus(nil)
|
|
return nil
|
|
}
|
|
return event
|
|
})
|
|
m.songList = table
|
|
|
|
m.view.AddItem(table, 0, 1, false)
|
|
|
|
}
|
|
|
|
func (m *main) drawPlaylistAlbumButtons(songs []*subsonic.Child) *tview.Button {
|
|
// Add buttons: Play | Shuffle | Add to queue
|
|
f := tview.NewFlex()
|
|
f.SetBackgroundColor(tcell.ColorBlack)
|
|
f.SetBorderPadding(0, 0, 2, 2)
|
|
// Buttons
|
|
play := tview.NewButton("Play")
|
|
shuffle := tview.NewButton("Shuffle")
|
|
queue := tview.NewButton("Queue")
|
|
artist := tview.NewButton("Artist")
|
|
// Button callbacks
|
|
play.SetSelectedFunc(func() {
|
|
play.Blur()
|
|
m.playAllFunc(songs...)
|
|
m.songList.Focus(nil)
|
|
})
|
|
play.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
|
switch event.Key() {
|
|
case tcell.KeyLeft:
|
|
play.Blur()
|
|
artist.Focus(nil)
|
|
return nil
|
|
case tcell.KeyRight:
|
|
play.Blur()
|
|
shuffle.Focus(nil)
|
|
return nil
|
|
case tcell.KeyDown:
|
|
play.Blur()
|
|
m.songList.Focus(nil)
|
|
return nil
|
|
}
|
|
return event
|
|
})
|
|
shuffle.SetSelectedFunc(func() {
|
|
shuffle.Blur()
|
|
cpy := make([]*subsonic.Child, len(songs))
|
|
copy(cpy, songs)
|
|
common.ShuffleSlice(cpy)
|
|
m.playAllFunc(cpy...)
|
|
m.songList.Focus(nil)
|
|
})
|
|
shuffle.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
|
switch event.Key() {
|
|
case tcell.KeyLeft:
|
|
shuffle.Blur()
|
|
play.Focus(nil)
|
|
return nil
|
|
case tcell.KeyRight:
|
|
shuffle.Blur()
|
|
queue.Focus(nil)
|
|
return nil
|
|
case tcell.KeyDown:
|
|
shuffle.Blur()
|
|
m.songList.Focus(nil)
|
|
return nil
|
|
}
|
|
return event
|
|
})
|
|
queue.SetSelectedFunc(func() {
|
|
queue.Blur()
|
|
m.addSongsFunc(songs...)
|
|
m.songList.Focus(nil)
|
|
})
|
|
queue.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
|
switch event.Key() {
|
|
case tcell.KeyLeft:
|
|
queue.Blur()
|
|
shuffle.Focus(nil)
|
|
return nil
|
|
case tcell.KeyRight:
|
|
queue.Blur()
|
|
artist.Focus(nil)
|
|
return nil
|
|
case tcell.KeyDown:
|
|
queue.Blur()
|
|
m.songList.Focus(nil)
|
|
return nil
|
|
}
|
|
return event
|
|
})
|
|
artist.SetSelectedFunc(func() {
|
|
artist.Blur()
|
|
ar, _ := m.client.GetArtist(m.album.ArtistID)
|
|
m.SetArtist(ar)
|
|
m.songList.Focus(nil)
|
|
})
|
|
artist.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
|
switch event.Key() {
|
|
case tcell.KeyLeft:
|
|
artist.Blur()
|
|
queue.Focus(nil)
|
|
return nil
|
|
case tcell.KeyRight:
|
|
artist.Blur()
|
|
play.Focus(nil)
|
|
return nil
|
|
case tcell.KeyDown:
|
|
artist.Blur()
|
|
m.songList.Focus(nil)
|
|
return nil
|
|
}
|
|
return event
|
|
})
|
|
|
|
f.AddItem(play, 0, 1, true)
|
|
f.AddItem(tview.NewBox(), 0, 1, false)
|
|
f.AddItem(shuffle, 0, 1, false)
|
|
f.AddItem(tview.NewBox(), 0, 1, false)
|
|
f.AddItem(queue, 0, 1, false)
|
|
f.AddItem(tview.NewBox(), 0, 1, false)
|
|
f.AddItem(artist, 0, 1, false)
|
|
|
|
// Add the buttons to the view
|
|
m.view.AddItem(f, 1, 0, false)
|
|
// Margin bottom of 1 line
|
|
m.view.AddItem(tview.NewBox(), 1, 0, false)
|
|
|
|
return play
|
|
}
|
|
|
|
func (m *main) populateSongs(songs []*subsonic.Child, play *tview.Button) {
|
|
table := tview.NewTable()
|
|
table.SetWrapSelection(true, false)
|
|
for i, song := range songs {
|
|
num := tview.NewTableCell(fmt.Sprintf("%d", i+1)).SetTextColor(tcell.ColorYellow)
|
|
title := tview.NewTableCell(song.Title).SetMaxWidth(15).SetExpansion(2)
|
|
artist := tview.NewTableCell(song.Artist).SetMaxWidth(15).SetExpansion(1)
|
|
d := time.Second * time.Duration(song.Duration)
|
|
duration := tview.NewTableCell(d.String()).SetAlign(tview.AlignRight).SetExpansion(1)
|
|
table.SetCell(i, 0, num)
|
|
table.SetCell(i, 1, title)
|
|
table.SetCell(i, 2, artist)
|
|
table.SetCell(i, 3, duration)
|
|
}
|
|
|
|
m.songList = table
|
|
table.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
|
// on first line and pressing up -> play button
|
|
row, _ := table.GetSelection()
|
|
if row == 0 && event.Key() == tcell.KeyUp {
|
|
table.Blur()
|
|
m.view.SetBorderColor(tcell.ColorRed)
|
|
play.Focus(nil)
|
|
return nil
|
|
} else if event.Rune() == 'r' {
|
|
song := songs[row]
|
|
m.log("Generating song (%s) radio....", song.Title)
|
|
go func() {
|
|
radioSongs, _ := m.client.GetSimilarSongs(song.ID, config.MaxRadioSongs())
|
|
m.playAllFunc(radioSongs...)
|
|
}()
|
|
}
|
|
return event
|
|
})
|
|
table.SetFocusFunc(func() {
|
|
m.view.SetBorderColor(tcell.ColorRed)
|
|
table.SetSelectable(true, false)
|
|
})
|
|
table.SetBlurFunc(func() {
|
|
m.view.SetBorderColor(tcell.ColorWhite)
|
|
table.SetSelectable(false, false)
|
|
})
|
|
m.songList.SetSelectedFunc(func(row, column int) {
|
|
m.addSongsFunc(songs[row])
|
|
})
|
|
|
|
m.view.AddItem(table, 0, 1, false)
|
|
}
|
|
|
|
func (m *main) populateSearchResults() {
|
|
table := tview.NewTable()
|
|
table.SetWrapSelection(true, false)
|
|
row := 0
|
|
lastArtist := 0
|
|
lastAlbum := 0
|
|
|
|
// Artists
|
|
if len(m.searchResult.Artist) > 0 {
|
|
// Header
|
|
header := tview.NewTableCell("Artists").SetSelectable(false)
|
|
table.SetCell(row, 0, header)
|
|
row++
|
|
//List
|
|
for i, artist := range m.searchResult.Artist {
|
|
index := tview.NewTableCell(fmt.Sprintf("%d", i+1)).
|
|
SetTextColor(tcell.ColorYellow)
|
|
a := tview.NewTableCell(artist.Name).SetExpansion(2).SetMaxWidth(15)
|
|
acount := tview.NewTableCell(fmt.Sprintf("%d Albums", artist.AlbumCount)).
|
|
SetExpansion(1).SetAlign(tview.AlignRight).SetMaxWidth(15)
|
|
table.SetCell(row, 0, index)
|
|
table.SetCell(row, 1, a)
|
|
table.SetCell(row, 2, acount)
|
|
row++
|
|
}
|
|
lastArtist = row
|
|
}
|
|
|
|
// Albums
|
|
|
|
if len(m.searchResult.Album) > 0 {
|
|
// Header
|
|
header := tview.NewTableCell("Albums").SetSelectable(false)
|
|
table.SetCell(row, 0, header)
|
|
row++
|
|
//List
|
|
for i, album := range m.searchResult.Album {
|
|
index := tview.NewTableCell(fmt.Sprintf("%d", i+1)).
|
|
SetTextColor(tcell.ColorYellow)
|
|
title := tview.NewTableCell(album.Name).SetExpansion(2).SetMaxWidth(15)
|
|
artist := tview.NewTableCell(album.Artist).
|
|
SetExpansion(1).SetAlign(tview.AlignRight).SetMaxWidth(15)
|
|
table.SetCell(row, 0, index)
|
|
table.SetCell(row, 1, title)
|
|
table.SetCell(row, 2, artist)
|
|
row++
|
|
}
|
|
lastAlbum = row
|
|
}
|
|
// Songs
|
|
if len(m.searchResult.Song) > 0 {
|
|
// Header
|
|
header := tview.NewTableCell("Songs").SetSelectable(false)
|
|
table.SetCell(row, 0, header)
|
|
row++
|
|
//List
|
|
for i, song := range m.searchResult.Song {
|
|
index := tview.NewTableCell(fmt.Sprintf("%d", i+1)).
|
|
SetTextColor(tcell.ColorYellow)
|
|
title := tview.NewTableCell(song.Title).SetExpansion(2).SetMaxWidth(15)
|
|
artist := tview.NewTableCell(song.Artist).
|
|
SetExpansion(1).SetAlign(tview.AlignRight).SetMaxWidth(15)
|
|
table.SetCell(row, 0, index)
|
|
table.SetCell(row, 1, title)
|
|
table.SetCell(row, 2, artist)
|
|
row++
|
|
}
|
|
}
|
|
|
|
m.songList = table
|
|
table.SetFocusFunc(func() {
|
|
m.view.SetBorderColor(tcell.ColorRed)
|
|
table.SetSelectable(true, false)
|
|
})
|
|
table.SetBlurFunc(func() {
|
|
m.view.SetBorderColor(tcell.ColorWhite)
|
|
table.SetSelectable(false, false)
|
|
})
|
|
table.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
|
row, _ := table.GetSelection()
|
|
if row <= lastAlbum {
|
|
return event
|
|
}
|
|
if event.Rune() != 'r' {
|
|
return event
|
|
}
|
|
cell := table.GetCell(row, 0)
|
|
index, err := strconv.Atoi(cell.Text)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
song := m.searchResult.Song[index-1]
|
|
m.log("Generating song (%s) radio....", song.Title)
|
|
go func() {
|
|
radioSongs, _ := m.client.GetSimilarSongs(song.ID, config.MaxRadioSongs())
|
|
m.playAllFunc(radioSongs...)
|
|
}()
|
|
return nil
|
|
})
|
|
m.songList.SetSelectedFunc(func(row, column int) {
|
|
cell := table.GetCell(row, 0)
|
|
index, err := strconv.Atoi(cell.Text)
|
|
if err != nil {
|
|
return
|
|
}
|
|
if row <= lastArtist {
|
|
artist, _ := m.client.GetArtist(m.searchResult.Artist[index-1].ID)
|
|
m.SetArtist(artist)
|
|
} else if lastArtist < row && row <= lastAlbum {
|
|
album, _ := m.client.GetAlbum(m.searchResult.Album[index-1].ID)
|
|
m.SetAlbum(album)
|
|
} else {
|
|
m.addSongsFunc(m.searchResult.Song[index-1])
|
|
}
|
|
})
|
|
|
|
m.view.AddItem(table, 0, 1, false)
|
|
}
|
|
|
|
func (m *main) populateHeader(title, subtitle string,
|
|
duration int, coverArtID string) {
|
|
|
|
header := tview.NewFlex()
|
|
art := tview.NewImage()
|
|
|
|
img, _ := m.client.GetCoverArt(coverArtID)
|
|
art.SetImage(img)
|
|
t := tview.NewTextView().
|
|
SetTextColor(tcell.ColorLightCyan).
|
|
SetTextAlign(tview.AlignCenter).
|
|
SetText(title)
|
|
s := tview.NewTextView().
|
|
SetTextColor(tcell.ColorYellow).
|
|
SetTextAlign(tview.AlignCenter).
|
|
SetText(subtitle).SetWordWrap(true)
|
|
|
|
g := tview.NewFlex().SetDirection(tview.FlexRow)
|
|
g.AddItem(tview.NewBox(), 1, 1, false)
|
|
g.AddItem(t, 1, 1, false)
|
|
g.AddItem(s, 0, 1, false)
|
|
|
|
header.AddItem(art, 0, 1, false)
|
|
header.AddItem(g, 0, 3, false)
|
|
size := 6
|
|
if m.miniView {
|
|
size = 4
|
|
}
|
|
m.view.AddItem(header, size, 1, false)
|
|
|
|
// Margin bottom of 1 line
|
|
if !m.miniView {
|
|
m.view.AddItem(tview.NewBox(), 1, 0, false)
|
|
}
|
|
}
|
|
|
|
func (m *main) SetPlayAllFunc(f func(song ...*subsonic.Child)) {
|
|
m.playAllFunc = f
|
|
}
|
|
|
|
func (m *main) SetPlayAddSongFunc(f func(song ...*subsonic.Child)) {
|
|
m.addSongsFunc = f
|
|
}
|
|
|
|
func (m *main) GetView() tview.Primitive {
|
|
return m.view
|
|
}
|