Skip to content

Commit

Permalink
Merge pull request #55 from xxxserxxx/feature_48_display_cover_art
Browse files Browse the repository at this point in the history
Feature 48 display cover art
  • Loading branch information
spezifisch authored Oct 11, 2024
2 parents b45b937 + 6e36e7e commit d17acb0
Show file tree
Hide file tree
Showing 7 changed files with 124 additions and 5 deletions.
5 changes: 5 additions & 0 deletions docs/logo.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# stmps Logo

The logo was created with [ArtBot](https://tinybots.net/artbot/create) with the prompt:

> A logo for a software project called "stmps" that provides a terminal user interface for playing music from a Subsonic music server.
Binary file added docs/stmps_logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions gui_handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ func (ui *Ui) addSongToQueue(entity *subsonic.SubsonicEntity) {
Duration: entity.Duration,
Album: album,
TrackNumber: entity.Track,
CoverArtId: entity.CoverArtId,
DiscNumber: entity.DiscNumber,
}
ui.player.AddToQueue(queueItem)
Expand All @@ -185,6 +186,7 @@ func makeSongHandler(entity *subsonic.SubsonicEntity, ui *Ui, fallbackArtist str
artist := stringOr(entity.Artist, fallbackArtist)
duration := entity.Duration
track := entity.Track
coverArtId := entity.CoverArtId
disc := entity.DiscNumber

response, err := ui.connection.GetAlbum(entity.Parent)
Expand All @@ -203,6 +205,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
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, disc int) error {
p.queue = []QueueItem{{id, uri, title, artist, duration, album, track, disc}}
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}}

Check failure on line 129 in mpvplayer/player.go

View workflow job for this annotation

GitHub Actions / Lint Golang

cannot use disc (variable of type int) as string value in struct literal

Check failure on line 129 in mpvplayer/player.go

View workflow job for this annotation

GitHub Actions / Lint Golang

cannot use coverArtId (variable of type string) as int value in struct literal (typecheck)

Check failure on line 129 in mpvplayer/player.go

View workflow job for this annotation

GitHub Actions / build (ubuntu-22.04, 1.22, arm64)

cannot use disc (variable of type int) as string value in struct literal

Check failure on line 129 in mpvplayer/player.go

View workflow job for this annotation

GitHub Actions / build (ubuntu-22.04, 1.22, arm64)

cannot use coverArtId (variable of type string) as int value in struct literal

Check failure on line 129 in mpvplayer/player.go

View workflow job for this annotation

GitHub Actions / build (ubuntu-22.04, 1.22, arm)

cannot use disc (variable of type int) as string value in struct literal

Check failure on line 129 in mpvplayer/player.go

View workflow job for this annotation

GitHub Actions / build (ubuntu-22.04, 1.22, arm)

cannot use coverArtId (variable of type string) as int value in struct literal

Check failure on line 129 in mpvplayer/player.go

View workflow job for this annotation

GitHub Actions / build (ubuntu-22.04, stable, amd64)

cannot use disc (variable of type int) as string value in struct literal

Check failure on line 129 in mpvplayer/player.go

View workflow job for this annotation

GitHub Actions / build (ubuntu-22.04, stable, amd64)

cannot use coverArtId (variable of type string) as int value in struct literal

Check failure on line 129 in mpvplayer/player.go

View workflow job for this annotation

GitHub Actions / build (ubuntu-20.04, 1.22, riscv64)

cannot use disc (variable of type int) as string value in struct literal

Check failure on line 129 in mpvplayer/player.go

View workflow job for this annotation

GitHub Actions / build (ubuntu-20.04, 1.22, riscv64)

cannot use coverArtId (variable of type string) as int value in struct literal

Check failure on line 129 in mpvplayer/player.go

View workflow job for this annotation

GitHub Actions / build (ubuntu-20.04, stable, riscv64)

cannot use disc (variable of type int) as string value in struct literal

Check failure on line 129 in mpvplayer/player.go

View workflow job for this annotation

GitHub Actions / build (ubuntu-20.04, stable, riscv64)

cannot use coverArtId (variable of type string) as int value in struct literal

Check failure on line 129 in mpvplayer/player.go

View workflow job for this annotation

GitHub Actions / build (ubuntu-20.04, stable, arm64)

cannot use disc (variable of type int) as string value in struct literal

Check failure on line 129 in mpvplayer/player.go

View workflow job for this annotation

GitHub Actions / build (ubuntu-20.04, stable, arm64)

cannot use coverArtId (variable of type string) as int value in struct literal

Check failure on line 129 in mpvplayer/player.go

View workflow job for this annotation

GitHub Actions / build (ubuntu-22.04, stable, arm)

cannot use disc (variable of type int) as string value in struct literal

Check failure on line 129 in mpvplayer/player.go

View workflow job for this annotation

GitHub Actions / build (ubuntu-22.04, stable, arm)

cannot use coverArtId (variable of type string) as int value in struct literal

Check failure on line 129 in mpvplayer/player.go

View workflow job for this annotation

GitHub Actions / build (ubuntu-20.04, stable, arm)

cannot use disc (variable of type int) as string value in struct literal

Check failure on line 129 in mpvplayer/player.go

View workflow job for this annotation

GitHub Actions / build (ubuntu-20.04, stable, arm)

cannot use coverArtId (variable of type string) as int value in struct literal

Check failure on line 129 in mpvplayer/player.go

View workflow job for this annotation

GitHub Actions / build (ubuntu-20.04, stable, amd64)

cannot use disc (variable of type int) as string value in struct literal

Check failure on line 129 in mpvplayer/player.go

View workflow job for this annotation

GitHub Actions / build (ubuntu-20.04, stable, amd64)

cannot use coverArtId (variable of type string) as int value in struct literal

Check failure on line 129 in mpvplayer/player.go

View workflow job for this annotation

GitHub Actions / build (ubuntu-20.04, 1.22, arm)

cannot use disc (variable of type int) as string value in struct literal

Check failure on line 129 in mpvplayer/player.go

View workflow job for this annotation

GitHub Actions / build (ubuntu-20.04, 1.22, arm)

cannot use coverArtId (variable of type string) as int value in struct literal

Check failure on line 129 in mpvplayer/player.go

View workflow job for this annotation

GitHub Actions / build (ubuntu-20.04, 1.22, arm64)

cannot use disc (variable of type int) as string value in struct literal

Check failure on line 129 in mpvplayer/player.go

View workflow job for this annotation

GitHub Actions / build (ubuntu-20.04, 1.22, arm64)

cannot use coverArtId (variable of type string) as int value in struct literal

Check failure on line 129 in mpvplayer/player.go

View workflow job for this annotation

GitHub Actions / build (ubuntu-22.04, 1.22, riscv64)

cannot use disc (variable of type int) as string value in struct literal

Check failure on line 129 in mpvplayer/player.go

View workflow job for this annotation

GitHub Actions / build (ubuntu-22.04, 1.22, riscv64)

cannot use coverArtId (variable of type string) as int value in struct literal

Check failure on line 129 in mpvplayer/player.go

View workflow job for this annotation

GitHub Actions / build (ubuntu-22.04, 1.22, amd64)

cannot use disc (variable of type int) as string value in struct literal

Check failure on line 129 in mpvplayer/player.go

View workflow job for this annotation

GitHub Actions / build (ubuntu-22.04, 1.22, amd64)

cannot use coverArtId (variable of type string) as int value in struct literal

Check failure on line 129 in mpvplayer/player.go

View workflow job for this annotation

GitHub Actions / build (ubuntu-20.04, 1.22, amd64)

cannot use disc (variable of type int) as string value in struct literal

Check failure on line 129 in mpvplayer/player.go

View workflow job for this annotation

GitHub Actions / build (ubuntu-20.04, 1.22, amd64)

cannot use coverArtId (variable of type string) as int value in struct literal

Check failure on line 129 in mpvplayer/player.go

View workflow job for this annotation

GitHub Actions / build (ubuntu-22.04, stable, arm64)

cannot use disc (variable of type int) as string value in struct literal

Check failure on line 129 in mpvplayer/player.go

View workflow job for this annotation

GitHub Actions / build (ubuntu-22.04, stable, arm64)

cannot use coverArtId (variable of type string) as int value in struct literal

Check failure on line 129 in mpvplayer/player.go

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest, 1.22, amd64)

cannot use disc (variable of type int) as string value in struct literal

Check failure on line 129 in mpvplayer/player.go

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest, 1.22, amd64)

cannot use coverArtId (variable of type string) as int value in struct literal

Check failure on line 129 in mpvplayer/player.go

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest, 1.22, arm64)

cannot use disc (variable of type int) as string value in struct literal

Check failure on line 129 in mpvplayer/player.go

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest, 1.22, arm64)

cannot use coverArtId (variable of type string) as int value in struct literal

Check failure on line 129 in mpvplayer/player.go

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest, 1.22, arm)

cannot use disc (variable of type int) as string value in struct literal

Check failure on line 129 in mpvplayer/player.go

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest, 1.22, arm)

cannot use coverArtId (variable of type string) as int value in struct literal

Check failure on line 129 in mpvplayer/player.go

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest, stable, amd64)

cannot use disc (variable of type int) as string value in struct literal

Check failure on line 129 in mpvplayer/player.go

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest, stable, amd64)

cannot use coverArtId (variable of type string) as int value in struct literal

Check failure on line 129 in mpvplayer/player.go

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest, stable, arm64)

cannot use disc (variable of type int) as string value in struct literal

Check failure on line 129 in mpvplayer/player.go

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest, stable, arm64)

cannot use coverArtId (variable of type string) as int value in struct literal

Check failure on line 129 in mpvplayer/player.go

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest, 1.22, riscv64)

cannot use disc (variable of type int) as string value in struct literal

Check failure on line 129 in mpvplayer/player.go

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest, 1.22, riscv64)

cannot use coverArtId (variable of type string) as int value in struct literal

Check failure on line 129 in mpvplayer/player.go

View workflow job for this annotation

GitHub Actions / build (ubuntu-22.04, stable, riscv64)

cannot use disc (variable of type int) as string value in struct literal

Check failure on line 129 in mpvplayer/player.go

View workflow job for this annotation

GitHub Actions / build (ubuntu-22.04, stable, riscv64)

cannot use coverArtId (variable of type string) as int value in struct literal

Check failure on line 129 in mpvplayer/player.go

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest, stable, riscv64)

cannot use disc (variable of type int) as string value in struct literal

Check failure on line 129 in mpvplayer/player.go

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest, stable, riscv64)

cannot use coverArtId (variable of type string) as int value in struct literal

Check failure on line 129 in mpvplayer/player.go

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest, stable, arm)

cannot use disc (variable of type int) as string value in struct literal

Check failure on line 129 in mpvplayer/player.go

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest, stable, arm)

cannot use coverArtId (variable of type string) as int value in struct literal
p.replaceInProgress = true
if ip, e := p.IsPaused(); ip && e == nil {
if err := p.Pause(); err != nil {
Expand Down
5 changes: 4 additions & 1 deletion mpvplayer/queue_item.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@

package mpvplayer

import "github.com/spezifisch/stmps/remote"
import (
"github.com/spezifisch/stmps/remote"
)

type QueueItem struct {
Id string
Expand All @@ -13,6 +15,7 @@ type QueueItem struct {
Duration int
Album string
TrackNumber int
CoverArtId string
DiscNumber int
}

Expand Down
48 changes: 46 additions & 2 deletions page_queue.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,13 @@
package main

import (
"bytes"
_ "embed"
"errors"
"fmt"
"image"
"image/png"
"os"
"text/template"
"time"

Expand Down Expand Up @@ -41,6 +46,7 @@ type QueuePage struct {
queueData queueData

songInfo *tview.TextView
coverArt *tview.Image

// external refs
ui *Ui
Expand All @@ -49,6 +55,18 @@ type QueuePage struct {
songInfoTemplate *template.Template
}

var STMPS_LOGO image.Image

// init sets up the default image used for songs for which the server provides
// no cover art.
func init() {
var err error
STMPS_LOGO, err = png.Decode(bytes.NewReader(_stmps_logo))
if err != nil {
fmt.Fprintf(os.Stderr, "%v", err)
}
}

func (ui *Ui) createQueuePage() *QueuePage {
tmpl := template.New("song info").Funcs(template.FuncMap{
"formatTime": func(i int) string {
Expand Down Expand Up @@ -102,14 +120,23 @@ func (ui *Ui) createQueuePage() *QueuePage {

// Song info
queuePage.songInfo = tview.NewTextView()
queuePage.songInfo.SetDynamicColors(true).SetScrollable(true).SetBorder(true).SetTitle("Song Info")
queuePage.songInfo.SetDynamicColors(true).SetScrollable(true)

queuePage.queueList.SetSelectionChangedFunc(queuePage.changeSelection)

queuePage.coverArt = tview.NewImage()
queuePage.coverArt.SetImage(STMPS_LOGO)

infoFlex := tview.NewFlex().SetDirection(tview.FlexRow).
AddItem(queuePage.songInfo, 0, 1, false).
AddItem(queuePage.coverArt, 0, 1, false)
infoFlex.SetBorder(true)
infoFlex.SetTitle(" song info ")

// flex wrapper
queuePage.Root = tview.NewFlex().SetDirection(tview.FlexColumn).
AddItem(queuePage.queueList, 0, 2, true).
AddItem(queuePage.songInfo, 0, 1, false)
AddItem(infoFlex, 0, 1, false)

// private data
queuePage.queueData = queueData{
Expand All @@ -122,9 +149,23 @@ func (ui *Ui) createQueuePage() *QueuePage {
func (q *QueuePage) changeSelection(row, column int) {
q.songInfo.Clear()
if row >= len(q.queueData.playerQueue) || row < 0 || column < 0 {
q.coverArt.SetImage(STMPS_LOGO)
return
}
currentSong := q.queueData.playerQueue[row]
art := STMPS_LOGO
if currentSong.CoverArtId != "" {
if nart, err := q.ui.connection.GetCoverArt(currentSong.CoverArtId); err == nil {
if nart != nil {
art = nart
} else {
q.logger.Printf("%q cover art %s was unexpectedly nil", currentSong.Title, currentSong.CoverArtId)
}
} else {
q.logger.Printf("error fetching cover art for %s: %v", currentSong.Title, err)
}
}
q.coverArt.SetImage(art)
_ = q.songInfoTemplate.Execute(q.songInfo, currentSong)
}

Expand Down Expand Up @@ -404,3 +445,6 @@ var songInfoTemplateString = `[blue::b]Title:[-:-:-:-] [green::i]{{.Title}}[-:-:
[blue::b]Disc:[-:-:-:-] [::i]{{.GetDiscNumber}}[-:-:-:-]
[blue::b]Track:[-:-:-:-] [::i]{{.GetTrackNumber}}[-:-:-:-]
[blue::b]Duration:[-:-:-:-] [::i]{{formatTime .Duration}}[-:-:-:-] `

//go:embed docs/stmps_logo.png
var _stmps_logo []byte
64 changes: 64 additions & 0 deletions subsonic/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,14 @@
package subsonic

import (
"bytes"
"encoding/json"
"errors"
"fmt"
"image"
"image/gif"
"image/jpeg"
"image/png"
"io"
"net/http"
"net/url"
Expand All @@ -30,6 +35,7 @@ type SubsonicConnection struct {

logger logger.LoggerInterface
directoryCache map[string]SubsonicResponse
coverArts map[string]image.Image
}

func Init(logger logger.LoggerInterface) *SubsonicConnection {
Expand All @@ -39,6 +45,7 @@ func Init(logger logger.LoggerInterface) *SubsonicConnection {

logger: logger,
directoryCache: make(map[string]SubsonicResponse),
coverArts: make(map[string]image.Image),
}
}

Expand Down Expand Up @@ -169,6 +176,7 @@ type SubsonicEntity struct {
Track int `json:"track"`
DiscNumber int `json:"discNumber"`
Path string `json:"path"`
CoverArtId string `json:"coverArt"`
}

func (s SubsonicEntity) ID() string {
Expand Down Expand Up @@ -369,6 +377,62 @@ func (connection *SubsonicConnection) GetMusicDirectory(id string) (*SubsonicRes
return resp, nil
}

// GetCoverArt fetches album art from the server, by ID. The results are cached,
// so it is safe to call this function repeatedly. If id is empty, an error
// is returned. If, for some reason, the server response can't be parsed into
// an image, an error is returned. This function can parse GIF, JPEG, and PNG
// images.
func (connection *SubsonicConnection) GetCoverArt(id string) (image.Image, error) {
if id == "" {
return nil, fmt.Errorf("GetCoverArt: no ID provided")
}
if rv, ok := connection.coverArts[id]; ok {
return rv, nil
}
query := defaultQuery(connection)
query.Set("id", id)
query.Set("f", "image/png")
caller := "GetCoverArt"
res, err := http.Get(connection.Host + "/rest/getCoverArt" + "?" + query.Encode())
if err != nil {
return nil, fmt.Errorf("[%s] failed to make GET request: %v", caller, err)
}

if res.Body != nil {
defer res.Body.Close()
} else {
return nil, fmt.Errorf("[%s] response body is nil", caller)
}

if res.StatusCode != http.StatusOK {
return nil, fmt.Errorf("[%s] unexpected status code: %d, status: %s", caller, res.StatusCode, res.Status)
}

if len(res.Header["Content-Type"]) == 0 {
return nil, fmt.Errorf("[%s] unknown image type (no content-type from server)", caller)
}
responseBody, err := io.ReadAll(res.Body)
if err != nil {
return nil, fmt.Errorf("[%s] failed to read response body: %v", caller, err)
}
var art image.Image
switch res.Header["Content-Type"][0] {
case "image/png":
art, err = png.Decode(bytes.NewReader(responseBody))
case "image/jpeg":
art, err = jpeg.Decode(bytes.NewReader(responseBody))
case "image/gif":
art, err = gif.Decode(bytes.NewReader(responseBody))
default:
return nil, fmt.Errorf("[%s] unhandled image type %s: %v", caller, res.Header["Content-Type"][0], err)
}
if art != nil {
// FIXME connection.coverArts shouldn't grow indefinitely. Add some LRU cleanup after loading a few hundred cover arts.
connection.coverArts[id] = art
}
return art, err
}

func (connection *SubsonicConnection) GetRandomSongs(Id string, randomType string) (*SubsonicResponse, error) {
query := defaultQuery(connection)

Expand Down

0 comments on commit d17acb0

Please sign in to comment.