Skip to content

Commit

Permalink
Merge branch 'main' into feature_48_display_cover_art
Browse files Browse the repository at this point in the history
  • Loading branch information
spezifisch authored Oct 11, 2024
2 parents 6f1d133 + b45b937 commit 6e36e7e
Show file tree
Hide file tree
Showing 15 changed files with 478 additions and 27 deletions.
17 changes: 15 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,19 @@ These screenshots use [Navidrome's demo server](https://demo.navidrome.org/) ([c

### Queue

![Queue View](./docs/screenshots/queue.png)
![Queue View](./docs/screenshots/queue_scaled.png)

### Browser

![Browser View](./docs/screenshots/browser.png)
![Browser View](./docs/screenshots/browser_scaled.png)

### Playlists

![Playlists View](./docs/screenshots/playlists_scaled.png)

### Search

![Search View](./docs/screenshots/search_scaled.png)

## Dependencies

Expand Down Expand Up @@ -96,6 +104,7 @@ These controls are accessible from any view:
- `-`/`=`: Volume down/volume up
- `,`/`.`: Seek -10/+10 seconds
- `r`: Add 50 random songs to the queue
- `s`: Start a server library scan

### Browser Controls

Expand All @@ -116,9 +125,13 @@ These controls are accessible from any view:
- `y`: Toggle star on song
- `k`: Move song up in queue
- `j`: Move song down in queue
- `s`: Save the queue as a playlist
- `S`: Shuffle the songs in the queue

If the currently playing song is moved, the music is stopped before the move, and must be re-started manually.

The save function includes an autocomplete function; if an existing playlist is selected (or manually entered), the `Overwrite` checkbox **must** be checked, or else the queue will not be saved. If a playlist is saved over, it will be **replaced** with the queue contents.

### Playlist Controls

- `n`: New playlist
Expand Down
Binary file modified docs/screenshots/browser.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/screenshots/queue.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
27 changes: 23 additions & 4 deletions gui.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,12 @@ type Ui struct {
logPage *LogPage

// modals
addToPlaylistList *tview.List
messageBox *tview.Modal
helpModal tview.Primitive
helpWidget *HelpWidget
addToPlaylistList *tview.List
messageBox *tview.Modal
helpModal tview.Primitive
helpWidget *HelpWidget
selectPlaylistModal tview.Primitive
selectPlaylistWidget *PlaylistSelectionWidget

starIdList map[string]struct{}

Expand All @@ -72,6 +74,7 @@ const (
PageAddToPlaylist = "addToPlaylist"
PageMessageBox = "messageBox"
PageHelpBox = "helpBox"
PageSelectPlaylist = "selectPlaylist"
)

func InitGui(indexes *[]subsonic.SubsonicIndex,
Expand Down Expand Up @@ -112,6 +115,7 @@ func InitGui(indexes *[]subsonic.SubsonicIndex,

ui.menuWidget = ui.createMenuWidget()
ui.helpWidget = ui.createHelpWidget()
ui.selectPlaylistWidget = ui.createPlaylistSelectionWidget()

// same as 'playlistList' except for the addToPlaylistModal
// - we need a specific version of this because we need different keybinds
Expand All @@ -126,6 +130,8 @@ func InitGui(indexes *[]subsonic.SubsonicIndex,
return event
})

ui.selectPlaylistModal = makeModal(ui.selectPlaylistWidget.Root, 80, 5)

// help box modal
ui.helpModal = makeModal(ui.helpWidget.Root, 80, 30)
ui.helpWidget.Root.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
Expand Down Expand Up @@ -166,6 +172,7 @@ func InitGui(indexes *[]subsonic.SubsonicIndex,
AddPage(PageDeletePlaylist, ui.playlistPage.DeletePlaylistModal, true, false).
AddPage(PageNewPlaylist, ui.playlistPage.NewPlaylistModal, true, false).
AddPage(PageAddToPlaylist, ui.browserPage.AddToPlaylistModal, true, false).
AddPage(PageSelectPlaylist, ui.selectPlaylistModal, true, false).
AddPage(PageMessageBox, ui.messageBox, true, false).
AddPage(PageHelpBox, ui.helpModal, true, false).
AddPage(PageLog, ui.logPage.Root, true, false)
Expand Down Expand Up @@ -217,6 +224,18 @@ func (ui *Ui) CloseHelp() {
ui.pages.HidePage(PageHelpBox)
}

func (ui *Ui) ShowSelectPlaylist() {
ui.pages.ShowPage(PageSelectPlaylist)
ui.pages.SendToFront(PageSelectPlaylist)
ui.app.SetFocus(ui.selectPlaylistModal)
ui.selectPlaylistWidget.visible = true
}

func (ui *Ui) CloseSelectPlaylist() {
ui.pages.HidePage(PageSelectPlaylist)
ui.selectPlaylistWidget.visible = false
}

func (ui *Ui) showMessageBox(text string) {
ui.pages.ShowPage(PageMessageBox)
ui.messageBox.SetText(text)
Expand Down
11 changes: 10 additions & 1 deletion gui_handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import (
func (ui *Ui) handlePageInput(event *tcell.EventKey) *tcell.EventKey {
// we don't want any of these firing if we're trying to add a new playlist
focused := ui.app.GetFocus()
if ui.playlistPage.IsNewPlaylistInputFocused(focused) || ui.browserPage.IsSearchFocused(focused) || focused == ui.searchPage.searchField {
if ui.playlistPage.IsNewPlaylistInputFocused(focused) || ui.browserPage.IsSearchFocused(focused) || focused == ui.searchPage.searchField || ui.selectPlaylistWidget.visible {
return event
}

Expand Down Expand Up @@ -99,6 +99,11 @@ func (ui *Ui) handlePageInput(event *tcell.EventKey) *tcell.EventKey {
}
ui.queuePage.UpdateQueue()

case 's':
if err := ui.connection.StartScan(); err != nil {
ui.logger.PrintError("startScan:", err)
}

default:
return event
}
Expand All @@ -112,6 +117,7 @@ func (ui *Ui) ShowPage(name string) {
}

func (ui *Ui) Quit() {
// TODO savePlayQueue/getPlayQueue
ui.player.Quit()
ui.app.Stop()
}
Expand Down Expand Up @@ -166,6 +172,7 @@ func (ui *Ui) addSongToQueue(entity *subsonic.SubsonicEntity) {
Album: album,
TrackNumber: entity.Track,
CoverArtId: entity.CoverArtId,
DiscNumber: entity.DiscNumber,
}
ui.player.AddToQueue(queueItem)
}
Expand All @@ -180,6 +187,7 @@ func makeSongHandler(entity *subsonic.SubsonicEntity, ui *Ui, fallbackArtist str
duration := entity.Duration
track := entity.Track
coverArtId := entity.CoverArtId
disc := entity.DiscNumber

response, err := ui.connection.GetAlbum(entity.Parent)
album := ""
Expand All @@ -198,6 +206,7 @@ func makeSongHandler(entity *subsonic.SubsonicEntity, ui *Ui, fallbackArtist str

return func() {
if err := ui.player.PlayUri(id, uri, title, artist, album, duration, track, coverArtId); err != nil {
if err := ui.player.PlayUri(id, uri, title, artist, album, duration, track, disc); err != nil {
ui.logger.PrintError("SongHandler Play", err)
return
}
Expand Down
2 changes: 2 additions & 0 deletions help_text.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ P stop
-/=(+) volume down/volume up
,/. seek -10/+10 seconds
r add 50 random songs to queue
s start server library scan
`

const helpPageBrowser = `
Expand All @@ -34,6 +35,7 @@ D remove all songs from queue
y toggle star on song
k move selected song up in queue
j move selected song down in queue
s save queue as a playlist
S shuffle the current queue
`

Expand Down
4 changes: 2 additions & 2 deletions mpvplayer/player.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,8 @@ func (p *Player) PlayNextTrack() error {
return nil
}

func (p *Player) PlayUri(id, uri, title, artist, album string, duration, track int, coverArtId string) error {
p.queue = []QueueItem{{id, uri, title, artist, duration, album, track, coverArtId}}
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, disc, coverArtId}}
p.replaceInProgress = true
if ip, e := p.IsPaused(); ip && e == nil {
if err := p.Pause(); err != nil {
Expand Down
5 changes: 5 additions & 0 deletions mpvplayer/queue_item.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ type QueueItem struct {
Album string
TrackNumber int
CoverArtId string
DiscNumber int
}

var _ remote.TrackInterface = (*QueueItem)(nil)
Expand Down Expand Up @@ -55,3 +56,7 @@ func (q QueueItem) GetAlbum() string {
func (q QueueItem) GetTrackNumber() int {
return q.TrackNumber
}

func (q QueueItem) GetDiscNumber() int {
return q.DiscNumber
}
2 changes: 2 additions & 0 deletions page_browser.go
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,8 @@ func (b *BrowserPage) handleAddArtistToQueue() {
return
}

sort.Sort(b.currentDirectory.Entities)

for _, entity := range b.currentDirectory.Entities {
if entity.IsDirectory {
b.addDirectoryToQueue(&entity)
Expand Down
10 changes: 7 additions & 3 deletions page_playlist.go
Original file line number Diff line number Diff line change
Expand Up @@ -256,8 +256,7 @@ func (p *PlaylistPage) UpdatePlaylists() {
p.ui.addToPlaylistList.Clear()

for _, playlist := range p.ui.playlists {
p.playlistList.AddItem(tview.Escape(playlist.Name), "", 0, nil)
p.ui.addToPlaylistList.AddItem(tview.Escape(playlist.Name), "", 0, nil)
p.addPlaylist(playlist)
}

p.isUpdating = false
Expand All @@ -266,6 +265,11 @@ func (p *PlaylistPage) UpdatePlaylists() {
}()
}

func (p *PlaylistPage) addPlaylist(playlist subsonic.SubsonicPlaylist) {
p.playlistList.AddItem(tview.Escape(playlist.Name), "", 0, nil)
p.ui.addToPlaylistList.AddItem(tview.Escape(playlist.Name), "", 0, nil)
}

func (p *PlaylistPage) handleAddPlaylistSongToQueue() {
playlistIndex := p.playlistList.GetCurrentItem()
entityIndex := p.selectedPlaylist.GetCurrentItem()
Expand Down Expand Up @@ -321,7 +325,7 @@ func (p *PlaylistPage) handlePlaylistSelected(playlist subsonic.SubsonicPlaylist
}

func (p *PlaylistPage) newPlaylist(name string) {
response, err := p.ui.connection.CreatePlaylist(name)
response, err := p.ui.connection.CreatePlaylist("", name, nil)
if err != nil {
p.logger.Printf("newPlaylist: CreatePlaylist %s -- %s", name, err.Error())
return
Expand Down
83 changes: 79 additions & 4 deletions page_queue.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,11 @@ import (
"github.com/rivo/tview"
"github.com/spezifisch/stmps/logger"
"github.com/spezifisch/stmps/mpvplayer"
"github.com/spezifisch/stmps/subsonic"
)

// TODO show total # of entries somewhere (top?)

// columns: star, title, artist, duration
const queueDataColumns = 4
const starIcon = "♥"
Expand Down Expand Up @@ -67,7 +70,7 @@ func init() {
func (ui *Ui) createQueuePage() *QueuePage {
tmpl := template.New("song info").Funcs(template.FuncMap{
"formatTime": func(i int) string {
return fmt.Sprintf("%s", time.Duration(i)*time.Second)
return (time.Duration(i) * time.Second).String()
},
})
songInfoTemplate, err := tmpl.Parse(songInfoTemplateString)
Expand Down Expand Up @@ -99,6 +102,12 @@ func (ui *Ui) createQueuePage() *QueuePage {
queuePage.moveSongDown()
case 'k':
queuePage.moveSongUp()
case 's':
if len(queuePage.queueData.playerQueue) == 0 {
queuePage.logger.Print("no items in queue to save")
return nil
}
queuePage.ui.ShowSelectPlaylist()
case 'S':
queuePage.shuffle()
default:
Expand Down Expand Up @@ -255,7 +264,8 @@ func (q *QueuePage) moveSongUp() {
}

if currentIndex == 1 {
q.ui.player.Stop()
// An error here won't affect re-arranging the queue.
_ = q.ui.player.Stop()
}

// remove the item from the queue
Expand All @@ -280,7 +290,8 @@ func (q *QueuePage) moveSongDown() {
}

if currentIndex == 0 {
q.ui.player.Stop()
// An error here won't affect re-arranging the queue.
_ = q.ui.player.Stop()
}

if currentIndex > queueLen-2 {
Expand All @@ -294,12 +305,75 @@ func (q *QueuePage) moveSongDown() {
q.updateQueue()
}

// saveQueue persists the current queue as a playlist. It presents the user
// with a way of choosing the playlist name, and if a playlist with the
// same name already exists it requires the user to confirm that they
// want to overwrite the existing playlist.
//
// Errors are reported to the user and require confirmation to dismiss,
// and logged.
func (q *QueuePage) saveQueue(playlistName string) {
// When updating an existing playlist, there are two options:
// updatePlaylist, and createPlaylist. createPlaylist on an
// existing playlist is a replace function.
//
// updatePlaylist is more surgical: it can selectively add and
// remove songs, and update playlist attributes. It is more
// network efficient than using createPlaylist to change an
// existing playlist. However, using it here would require
// a more complex diffing algorithm, and much more code.
// Consequently, this version of save() uses the more simple
// brute-force approach of always using createPlaylist().
songIds := make([]string, len(q.queueData.playerQueue))
for i, it := range q.queueData.playerQueue {
songIds[i] = it.Id
}

var playlistId string
for _, p := range q.ui.playlists {
if p.Name == playlistName {
playlistId = string(p.Id)
break
}
}
var response *subsonic.SubsonicResponse
var err error
if playlistId == "" {
q.logger.Printf("Saving %d items to playlist %s", len(q.queueData.playerQueue), playlistName)
response, err = q.ui.connection.CreatePlaylist("", playlistName, songIds)
} else {
q.logger.Printf("Replacing playlist %s with %d", playlistId, len(q.queueData.playerQueue))
response, err = q.ui.connection.CreatePlaylist(playlistId, "", songIds)
}
if err != nil {
message := fmt.Sprintf("Error saving queue: %s", err)
q.ui.showMessageBox(message)
q.logger.Print(message)
} else {
if playlistId != "" {
for i, pl := range q.ui.playlists {
if string(pl.Id) == playlistId {
q.ui.playlists[i] = response.Playlist
break
}
}
} else {
q.ui.playlistPage.addPlaylist(response.Playlist)
q.ui.playlists = append(q.ui.playlists, response.Playlist)
}
q.ui.playlistPage.handlePlaylistSelected(response.Playlist)
}
}

// shuffle randomly shuffles entries in the queue, updates it, and moves
// the selected-item to the new first entry.
func (q *QueuePage) shuffle() {
if len(q.queueData.playerQueue) == 0 {
return
}

q.ui.player.Stop()
// An error here won't affect re-arranging the queue.
_ = q.ui.player.Stop()
q.ui.player.Shuffle()

q.queueList.Select(0, 0)
Expand Down Expand Up @@ -368,6 +442,7 @@ func (q *queueData) GetColumnCount() int {
var songInfoTemplateString = `[blue::b]Title:[-:-:-:-] [green::i]{{.Title}}[-:-:-:-]
[blue::b]Artist:[-:-:-:-] [::i]{{.Artist}}[-:-:-:-]
[blue::b]Album:[-:-:-:-] [::i]{{.GetAlbum}}[-:-:-:-]
[blue::b]Disc:[-:-:-:-] [::i]{{.GetDiscNumber}}[-:-:-:-]
[blue::b]Track:[-:-:-:-] [::i]{{.GetTrackNumber}}[-:-:-:-]
[blue::b]Duration:[-:-:-:-] [::i]{{formatTime .Duration}}[-:-:-:-] `

Expand Down
Loading

0 comments on commit 6e36e7e

Please sign in to comment.