Added ci workflow + fixed linting issues
Some checks failed
ci / check for spelling errors (pull_request) Successful in 20s
ci / code quality (lint/tests) (pull_request) Failing after 1m30s
ci / Make sure build does not fail (pull_request) Has been skipped
ci / notify-fail (pull_request) Failing after 13s

Signed-off-by: Sagi Dayan <sagidayan@gmail.com>
This commit is contained in:
Sagi Dayan 2024-12-18 17:29:16 +02:00
parent 174b4ae0ea
commit 864d56954a
Signed by: sagi
GPG key ID: FAB96BFC63B46458
23 changed files with 565 additions and 261 deletions

View file

@ -1,4 +1,4 @@
name: build bin name: ci
on: on:
push: push:
@ -10,9 +10,50 @@ permissions:
contents: read contents: read
jobs: jobs:
build: codespell:
name: check for spelling errors
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Annotate locations with typos
uses: "https://github.com/codespell-project/codespell-problem-matcher@v1"
- name: Codespell
uses: "https://github.com/codespell-project/actions-codespell@v2"
with:
check_filenames: true
check_hidden: true
skip: ./.git
codequality:
name: code quality (lint/tests)
runs-on: ubuntu-latest
needs:
- codespell
if: ${{ success() }}
steps:
- name: checkout
uses: actions/checkout@v4
- uses: https://code.forgejo.org/actions/setup-go@v5
name: install go
with:
go-version-file: './go.mod'
- name: install dependencies
run: make dep
- name: download golangci-lint
run: wget https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh
- name: install golangci-lint
run: sh ./install.sh v1.62.2
- name: lint
run: ./bin/golangci-lint run
compile:
name: Make sure build does not fail name: Make sure build does not fail
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs:
- codespell
- codequality
if: ${{ success() }}
steps: steps:
- uses: https://code.forgejo.org/actions/checkout@v4 - uses: https://code.forgejo.org/actions/checkout@v4
name: checkout name: checkout
@ -27,3 +68,21 @@ jobs:
- run: make build - run: make build
name: install dependencies and build bin files name: install dependencies and build bin files
notify-fail:
name: notify-fail
runs-on: ubuntu-latest
needs:
- codespell
- codequality
- compile
if: ${{ failure() }}
env:
SERVER_URL: ${{ secrets.GOTIFY_SERVER_URL }}
TOKEN: ${{ secrets.GOTIFY_TOKEN }}
steps:
- name: gotify
run: |-
curl "${SERVER_URL}?token=${TOKEN}" \
-F "title='CI failed'" \
-F "message='Something failed at ${GITHUB_REPOSITORY}/${GITHUB_REF} for user ${GITHUB_ACTOR}'" \
-F "priority=7" &> /dev/null

View file

@ -1,76 +1,23 @@
# This file contains all available configuration options
# with their default values.
# options for analysis running
run:
# default concurrency is a available CPU number
concurrency: 4
# timeout for analysis, e.g. 30s, 5m, default is 1m
timeout: 10m
# exit code when at least one issue was found, default is 1
issues-exit-code: 1
# include test files or not, default is true
tests: true
# which dirs to skip: issues from them won't be reported;
# can use regexp here: generated.*, regexp is applied on full path;
# default value is empty list, but default dirs are skipped independently
# from this option's value (see skip-dirs-use-default).
skip-dirs:
- bin
- deploy
- docs
- examples
- hack
- packaging
- reports
# output configuration options
output:
# colored-line-number|line-number|json|tab|checkstyle|code-climate, default is "colored-line-number"
format: colored-line-number
# print lines of code with issue, default is true
print-issued-lines: true
# print linter name in the end of issue text, default is true
print-linter-name: true
# make issues output unique by line, default is true
uniq-by-line: true
issues:
# List of regexps of issue texts to exclude, empty list by default.
# But independently from this option we use default exclude patterns,
# it can be disabled by `exclude-use-default: false`. To list all
# excluded by default patterns execute `golangci-lint run --help`
exclude:
- 'declaration of "err" shadows declaration at'
linters: linters:
enable: enable-all: true
- megacheck disable:
- govet - wrapcheck
- gocyclo - exhaustruct
- gofmt - varnamelen
- gochecknoglobals
- depguard
- gochecknoinits
- forbidigo
- revive
- gosec - gosec
- megacheck - dogsled
- unconvert - mnd
- gci - funlen
- goimports - cyclop
- exportloopref - gocognit
- exhaustive
linters-settings: - maintidx
govet: - gocritic
check-shadowing: true - nestif
- ireturn
settings: - stylecheck
printf:
funcs:
- Infof
- Warnf
- Errorf
- Fatalf

View file

@ -1,12 +1,16 @@
BINARY_NAME=subsonic-tui BINARY_NAME=subsonic-tui
BUILD_FOLDER=build BUILD_FOLDER=build
VERSION=0.0.1
GO_BUILD_LD_FLAGS=-ldflags="-s -w -X 'git.dayanhub.com/sagi/subsonic-tui/internal/variables.Commit=$(shell git rev-parse --short HEAD)' \
-X 'git.dayanhub.com/sagi/subsonic-tui/internal/variables.Version=${VERSION}'"
.PHONY: build .PHONY: build
build: dep build: dep
GOARCH=amd64 GOOS=darwin go build -o ${BUILD_FOLDER}/${BINARY_NAME}-darwin main.go GOARCH=amd64 GOOS=darwin go build ${GO_BUILD_LD_FLAGS} -o ${BUILD_FOLDER}/${BINARY_NAME}-${VERSION}-amd64-darwin main.go
GOARCH=amd64 GOOS=linux go build -o ${BUILD_FOLDER}/${BINARY_NAME}-linux main.go GOARCH=amd64 GOOS=linux go build ${GO_BUILD_LD_FLAGS} -o ${BUILD_FOLDER}/${BINARY_NAME}-${VERSION}-amd64-linux main.go
GOARCH=amd64 GOOS=windows go build -o ${BUILD_FOLDER}/${BINARY_NAME}-windows main.go GOARCH=amd64 GOOS=windows go build ${GO_BUILD_LD_FLAGS} -o ${BUILD_FOLDER}/${BINARY_NAME}-${VERSION}-amd64-windows main.go
run: build run: build
./${BINARY_NAME} ./${BINARY_NAME}
@ -30,3 +34,17 @@ vet:
lint: lint:
golangci-lint run --enable-all golangci-lint run --enable-all
.PHONY: install
install: build
ifeq ($(OS),Windows_NT) # is Windows_NT on XP, 2000, 7, Vista, 10...
$(error Unable to install on windows)
else
ifeq ($(shell uname), Linux)
sudo cp ${BUILD_FOLDER}/${BINARY_NAME}-${VERSION}-amd64-linux /usr/local/bin/${BINARY_NAME}
else
@echo $(shell uname)
$(error Unable to install on systems that are not linux)
endif
endif

View file

@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"image" "image"
"image/jpeg" "image/jpeg"
"io/fs"
"os" "os"
"path" "path"
) )
@ -25,6 +26,7 @@ func (c *artcache) saveArt(id string, img image.Image) *string {
if path != nil { if path != nil {
c.artPaths[id] = *path c.artPaths[id] = *path
} }
return path return path
} }
@ -39,10 +41,12 @@ func (c *artcache) saveImage(id string, img image.Image) *string {
if err != nil { if err != nil {
return nil return nil
} }
err = jpeg.Encode(f, img, nil) err = jpeg.Encode(f, img, nil)
if err != nil { if err != nil {
return nil return nil
} }
return &filePath return &filePath
} }
@ -50,6 +54,7 @@ func (c *artcache) GetPath(id string) *string {
if path, ok := c.artPaths[id]; ok { if path, ok := c.artPaths[id]; ok {
return &path return &path
} }
return nil return nil
} }
@ -72,11 +77,12 @@ func (c *artcache) GetImage(id string) *image.Image {
if err != nil { if err != nil {
return nil return nil
} }
return &img return &img
} }
func (c *artcache) filepath(id string) string { func (c *artcache) filepath(id string) string {
return path.Join(c.cacheDir, fmt.Sprintf("%s.jpg", id)) return path.Join(c.cacheDir, id+".jpg")
} }
func (c *artcache) Destroy() { func (c *artcache) Destroy() {
@ -86,10 +92,14 @@ func (c *artcache) Destroy() {
func init() { func init() {
tmpDir := os.TempDir() tmpDir := os.TempDir()
cacheDir := path.Join(tmpDir, fmt.Sprintf("subsonic-tui-%d", os.Getpid())) cacheDir := path.Join(tmpDir, fmt.Sprintf("subsonic-tui-%d", os.Getpid()))
err := os.Mkdir(cacheDir, 0777)
var permissions fs.FileMode = 0o777
err := os.Mkdir(cacheDir, permissions)
if err != nil { if err != nil {
panic("Failed to create cacheDir") panic("Failed to create cacheDir")
} }
ArtCache = &artcache{ ArtCache = &artcache{
cacheDir: cacheDir, cacheDir: cacheDir,
artPaths: make(map[string]string), artPaths: make(map[string]string),

View file

@ -1,10 +1,10 @@
package client package client
import ( import (
"fmt"
"image" "image"
"io" "io"
"net/http" "net/http"
"strconv"
"sync" "sync"
"git.dayanhub.com/sagi/subsonic-tui/internal/common" "git.dayanhub.com/sagi/subsonic-tui/internal/common"
@ -16,7 +16,7 @@ type Client struct {
} }
func NewClient(baseURL string) *Client { func NewClient(baseURL string) *Client {
var client subsonic.Client = subsonic.Client{ client := subsonic.Client{
Client: &http.Client{}, Client: &http.Client{},
ClientName: "subsonic-tui", ClientName: "subsonic-tui",
BaseUrl: baseURL, BaseUrl: baseURL,
@ -30,6 +30,7 @@ func NewClient(baseURL string) *Client {
func (c *Client) Authenticate(username, password string) error { func (c *Client) Authenticate(username, password string) error {
c.client.User = username c.client.User = username
return c.client.Authenticate(password) return c.client.Authenticate(password)
} }
@ -41,8 +42,8 @@ func (c *Client) GetPlaylists() ([]*subsonic.Playlist, error) {
return c.client.GetPlaylists(map[string]string{}) return c.client.GetPlaylists(map[string]string{})
} }
func (c *Client) GetPlaylist(ID string) (*subsonic.Playlist, error) { func (c *Client) GetPlaylist(id string) (*subsonic.Playlist, error) {
return c.client.GetPlaylist(ID) return c.client.GetPlaylist(id)
} }
func (c *Client) GetArtists() ([]*subsonic.ArtistID3, error) { func (c *Client) GetArtists() ([]*subsonic.ArtistID3, error) {
@ -50,7 +51,9 @@ func (c *Client) GetArtists() ([]*subsonic.ArtistID3, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
artists := []*subsonic.ArtistID3{} artists := []*subsonic.ArtistID3{}
for _, i := range indexes.Index { for _, i := range indexes.Index {
artists = append(artists, i.Artist...) artists = append(artists, i.Artist...)
} }
@ -64,53 +67,58 @@ func (c *Client) GetAlbums() ([]*subsonic.AlbumID3, error) {
}) })
} }
func (c *Client) GetArtist(ID string) (*subsonic.ArtistID3, error) { func (c *Client) GetArtist(id string) (*subsonic.ArtistID3, error) {
return c.client.GetArtist(ID) return c.client.GetArtist(id)
} }
func (c *Client) GetArtistInfo(ID string) (*subsonic.ArtistInfo2, error) { func (c *Client) GetArtistInfo(id string) (*subsonic.ArtistInfo2, error) {
return c.client.GetArtistInfo2(ID, map[string]string{ return c.client.GetArtistInfo2(id, map[string]string{
"count": "20", "count": "20",
}) })
} }
func (c *Client) GetAlbum(ID string) (*subsonic.AlbumID3, error) { func (c *Client) GetAlbum(id string) (*subsonic.AlbumID3, error) {
return c.client.GetAlbum(ID) return c.client.GetAlbum(id)
} }
func (c *Client) GetCoverArt(ID string) (image.Image, error) { func (c *Client) GetCoverArt(id string) (image.Image, error) {
if img := ArtCache.GetImage(ID); img != nil { if img := ArtCache.GetImage(id); img != nil {
return *img, nil return *img, nil
} }
img, err := c.client.GetCoverArt(ID, map[string]string{
img, err := c.client.GetCoverArt(id, map[string]string{
// "size": "64", // "size": "64",
}) })
if err != nil { if err != nil {
return nil, err return nil, err
} }
ArtCache.saveArt(ID, img)
ArtCache.saveArt(id, img)
return img, err return img, err
} }
func (c *Client) GetSimilarSongs(artistID string, maxSongs int) ([]*subsonic.Child, error) { func (c *Client) GetSimilarSongs(artistID string, maxSongs int) ([]*subsonic.Child, error) {
max := fmt.Sprintf("%d", maxSongs) count := strconv.Itoa(maxSongs)
return c.client.GetSimilarSongs2(artistID, map[string]string{ return c.client.GetSimilarSongs2(artistID, map[string]string{
"count": max, "count": count,
}) })
} }
func (c *Client) Stream(ID string) (io.Reader, error) { func (c *Client) Stream(id string) (io.Reader, error) {
return c.client.Stream(ID, map[string]string{ return c.client.Stream(id, map[string]string{
"format": "mp3", "format": "mp3",
}) })
} }
func (c *Client) Scrobble(ID string) error { func (c *Client) Scrobble(id string) error {
return c.client.Scrobble(ID, map[string]string{}) return c.client.Scrobble(id, map[string]string{})
} }
func (c *Client) GetTopSongs(name string, max int) ([]*subsonic.Child, error) { func (c *Client) GetTopSongs(name string, maxSongs int) ([]*subsonic.Child, error) {
count := fmt.Sprintf("%d", max) count := strconv.Itoa(maxSongs)
return c.client.GetTopSongs(name, map[string]string{ return c.client.GetTopSongs(name, map[string]string{
"count": count, "count": count,
}) })
@ -124,40 +132,59 @@ func (c *Client) Search(query string) (*subsonic.SearchResult3, error) {
}) })
} }
func (c *Client) GetExperimentalArtistRadio(artistId3 *subsonic.ArtistID3, info *subsonic.ArtistInfo2, max int) ([]*subsonic.Child, error) { func (c *Client) GetExperimentalArtistRadio(artistID3 *subsonic.ArtistID3,
info *subsonic.ArtistInfo2, maxSongs int,
) ([]*subsonic.Child, error) {
var wg sync.WaitGroup var wg sync.WaitGroup
ID := artistId3.ID
ID := artistID3.ID
similarArtists := info.SimilarArtist similarArtists := info.SimilarArtist
songs := []*subsonic.Child{} songs := []*subsonic.Child{}
similarArtistsSongs := 10 similarArtistsSongs := 10
thisArtistFactor := 3 thisArtistFactor := 3
portion := len(info.SimilarArtist) * similarArtistsSongs * thisArtistFactor portion := len(info.SimilarArtist) * similarArtistsSongs * thisArtistFactor
wg.Add(2)
wg.Add(1)
go func() { go func() {
s, _ := c.GetSimilarSongs(ID, portion) s, _ := c.GetSimilarSongs(ID, portion)
songs = append(songs, s...) songs = append(songs, s...)
wg.Done() wg.Done()
}() }()
wg.Add(1)
go func() { go func() {
s, _ := c.GetTopSongs(artistId3.Name, similarArtistsSongs) s, _ := c.GetTopSongs(artistID3.Name, similarArtistsSongs)
songs = append(songs, s...) songs = append(songs, s...)
wg.Done() wg.Done()
}() }()
common.ShuffleSlice(similarArtists) common.ShuffleSlice(similarArtists)
for _, a := range similarArtists { for _, a := range similarArtists {
wg.Add(1) wg.Add(1)
artist := a artist := a
go func() { go func() {
s, _ := c.GetSimilarSongs(artist.ID, similarArtistsSongs) s, _ := c.GetSimilarSongs(artist.ID, similarArtistsSongs)
songs = append(songs, s...) songs = append(songs, s...)
wg.Done() wg.Done()
}() }()
} }
wg.Wait() wg.Wait()
if max > len(songs) {
max = len(songs) if maxSongs > len(songs) {
maxSongs = len(songs)
} }
songs = songs[:max]
songs = songs[:maxSongs]
common.ShuffleSlice(songs) common.ShuffleSlice(songs)
return songs, nil return songs, nil
} }

View file

@ -11,11 +11,13 @@ func ShuffleSlice(slice interface{}) {
swap := reflect.Swapper(slice) swap := reflect.Swapper(slice)
length := rv.Len() length := rv.Len()
for i := length - 1; i > 0; i-- { for i := length - 1; i > 0; i-- {
j, err := rand.Int(rand.Reader, big.NewInt(int64(i+1))) j, err := rand.Int(rand.Reader, big.NewInt(int64(i+1)))
if err != nil { if err != nil {
panic("Shuffle error") panic("Shuffle error")
} }
swap(i, int(j.Int64())) swap(i, int(j.Int64()))
} }
} }

View file

@ -3,6 +3,7 @@ package config
import ( import (
"encoding/base64" "encoding/base64"
"fmt" "fmt"
"io/fs"
"os" "os"
"path" "path"
"strings" "strings"
@ -17,9 +18,9 @@ type _config struct {
Username string `yaml:"username"` Username string `yaml:"username"`
Password string `yaml:"password"` Password string `yaml:"password"`
URL string `yaml:"url"` URL string `yaml:"url"`
EnableScrobble bool `yaml:"enable_scrobble" default:"false"` EnableScrobble bool `default:"false" yaml:"enableScrobble"`
MaxRadioSongs int `yaml:"max_radio_songs" default:"50"` MaxRadioSongs int `default:"50" yaml:"maxRadioSongs"`
ExperimentalRadioAlgo bool `yaml:"experimental_radio_algo" default:"false"` ExperimentalRadioAlgo bool `default:"false" yaml:"experimentalRadioAlgo"`
} }
var configStruct *_config var configStruct *_config
@ -33,13 +34,18 @@ func init() {
fmt.Printf("[ERROR] Failed to fetch user config directory. %e\n", err) fmt.Printf("[ERROR] Failed to fetch user config directory. %e\n", err)
os.Exit(1) os.Exit(1)
} }
if _, err := os.Stat(configDir); os.IsNotExist(err) { if _, err := os.Stat(configDir); os.IsNotExist(err) {
err := os.MkdirAll(configDir, 0700) var permissions fs.FileMode = 0o700
err := os.MkdirAll(configDir, permissions)
if err != nil { if err != nil {
panic(err) panic(err)
} }
} }
var configFile *os.File var configFile *os.File
if _, err := os.Stat(configPath); os.IsNotExist(err) { if _, err := os.Stat(configPath); os.IsNotExist(err) {
configFile, err = os.Create(configPath) configFile, err = os.Create(configPath)
defer func() { defer func() {
@ -48,35 +54,44 @@ func init() {
panic(err) panic(err)
} }
}() }()
if err != nil { if err != nil {
fmt.Printf("[ERROR] Failed to create config file @ %s. %e\n", configPath, err) fmt.Printf("[ERROR] Failed to create config file @ %s. %e\n", configPath, err)
os.Exit(1) panic("unable to create config file")
} }
} }
configStruct, err = loadConfig() configStruct, err = loadConfig()
if err != nil { if err != nil {
fmt.Printf("[ERROR] Failed to load config file @ %s. %e\n", configPath, err) fmt.Printf("[ERROR] Failed to load config file @ %s. %e\n", configPath, err)
os.Exit(1) panic("unable to load config file")
} }
fmt.Printf("Init Config %s\n", configPath) fmt.Printf("Init Config %s\n", configPath)
} }
func URL() string { func URL() string {
return configStruct.URL return configStruct.URL
} }
func Username() string { func Username() string {
return configStruct.Username return configStruct.Username
} }
func Password() string { func Password() string {
p, _ := base64.StdEncoding.DecodeString(configStruct.Password) p, _ := base64.StdEncoding.DecodeString(configStruct.Password)
return strings.TrimSpace(string(p)) return strings.TrimSpace(string(p))
} }
func ScrobbleEnabled() bool { func ScrobbleEnabled() bool {
return configStruct.EnableScrobble return configStruct.EnableScrobble
} }
func MaxRadioSongs() int { func MaxRadioSongs() int {
return configStruct.MaxRadioSongs return configStruct.MaxRadioSongs
} }
func ExperimentalRadioAlgo() bool { func ExperimentalRadioAlgo() bool {
return configStruct.ExperimentalRadioAlgo return configStruct.ExperimentalRadioAlgo
} }
@ -95,17 +110,21 @@ func SetURL(u string) {
func loadConfig() (*_config, error) { func loadConfig() (*_config, error) {
c := &_config{} c := &_config{}
err := defaults.Set(c) err := defaults.Set(c)
if err != nil { if err != nil {
panic(err) panic(err)
} }
file, err := os.ReadFile(configPath) file, err := os.ReadFile(configPath)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if err = yaml.Unmarshal(file, c); err != nil { if err = yaml.Unmarshal(file, c); err != nil {
return nil, err return nil, err
} }
return c, nil return c, nil
} }
@ -115,7 +134,10 @@ func SaveConfig() {
fmt.Printf("[ERROR] Failed to convert config to yaml. %e\n", err) fmt.Printf("[ERROR] Failed to convert config to yaml. %e\n", err)
os.Exit(1) os.Exit(1)
} }
err = os.WriteFile(configPath, yml, 0600)
var permissions fs.FileMode = 0o600
err = os.WriteFile(configPath, yml, permissions)
if err != nil { if err != nil {
fmt.Printf("[ERROR] Failed to save config file @ %s. %e\n", configPath, err) fmt.Printf("[ERROR] Failed to save config file @ %s. %e\n", configPath, err)
os.Exit(1) os.Exit(1)

View file

@ -46,7 +46,9 @@ func NewController(client *client.Client) *Controller {
} }
controller.desktopPlayback = desktopPlayer(controller) controller.desktopPlayback = desktopPlayer(controller)
controller.desktopPlayback.Start() controller.desktopPlayback.Start()
go controller.playbackTicker() go controller.playbackTicker()
return controller return controller
} }
@ -58,14 +60,17 @@ func (c *Controller) Play(song *subsonic.Child) {
if song == nil { if song == nil {
return return
} }
r, err := c.client.Stream(song.ID) r, err := c.client.Stream(song.ID)
if err != nil { if err != nil {
//TODO: Log error
c.Stop() c.Stop()
return return
} }
c.Stream(r, song) c.Stream(r, song)
} }
func (c *Controller) Next() { func (c *Controller) Next() {
song := c.queue.Next() song := c.queue.Next()
if song != nil { if song != nil {
@ -78,6 +83,7 @@ func (c *Controller) AddToQueue(songs []*subsonic.Child) {
if shouldPlay { if shouldPlay {
c.Play(c.queue.GetCurrentSong()) c.Play(c.queue.GetCurrentSong())
} }
c.desktopPlayback.OnPlaylistChanged() c.desktopPlayback.OnPlaylistChanged()
} }
@ -108,6 +114,7 @@ func (c *Controller) Prev() {
c.Play(song) c.Play(song)
} }
} }
func (c *Controller) GetQueue() []*subsonic.Child { func (c *Controller) GetQueue() []*subsonic.Child {
return c.queue.Get() return c.queue.Get()
} }
@ -131,6 +138,7 @@ func (c *Controller) SetSongEndedFunc(f func(song *subsonic.Child)) {
func (c *Controller) Close() error { func (c *Controller) Close() error {
c.Stop() c.Stop()
c.closeChan <- true c.closeChan <- true
return nil return nil
} }
@ -143,6 +151,7 @@ func (c *Controller) TogglePlayPause() {
c.playbackState = PlaybackStatePlaying c.playbackState = PlaybackStatePlaying
} }
} }
c.desktopPlayback.OnPlayPause() c.desktopPlayback.OnPlayPause()
} }
@ -150,12 +159,16 @@ func (c *Controller) Stop() {
if c.ctrl == nil { if c.ctrl == nil {
return return
} }
speaker.Clear() speaker.Clear()
c.ctrl.Paused = true c.ctrl.Paused = true
c.playbackState = PlaybackStateStopped c.playbackState = PlaybackStateStopped
c.ctrl = nil c.ctrl = nil
c.stream = nil c.stream = nil
c.songElapsedFunc(c.song, time.Duration(0)) c.songElapsedFunc(c.song, time.Duration(0))
c.song = nil c.song = nil
c.position = 0 c.position = 0
c.desktopPlayback.OnPlayPause() c.desktopPlayback.OnPlayPause()
@ -193,6 +206,7 @@ func (c *Controller) Stream(reader io.Reader, song *subsonic.Child) {
readerCloser := io.NopCloser(reader) readerCloser := io.NopCloser(reader)
decodedMp3, format, err := mp3.Decode(readerCloser) decodedMp3, format, err := mp3.Decode(readerCloser)
decodedMp3.Position() decodedMp3.Position()
if err != nil { if err != nil {
panic("mp3.NewDecoder failed: " + err.Error()) panic("mp3.NewDecoder failed: " + err.Error())
} }
@ -214,6 +228,7 @@ func (c *Controller) Stream(reader io.Reader, song *subsonic.Child) {
ctrl := &beep.Ctrl{Streamer: stream} ctrl := &beep.Ctrl{Streamer: stream}
c.ctrl = ctrl c.ctrl = ctrl
c.playbackState = PlaybackStatePlaying c.playbackState = PlaybackStatePlaying
speaker.Play(beep.Seq(ctrl, beep.Callback(func() { speaker.Play(beep.Seq(ctrl, beep.Callback(func() {
c.songEndedFunc(song) c.songEndedFunc(song)
c.songEndedChan <- true c.songEndedChan <- true

View file

@ -9,11 +9,11 @@ import (
"github.com/godbus/dbus/v5" "github.com/godbus/dbus/v5"
"github.com/quarckster/go-mpris-server/pkg/events" "github.com/quarckster/go-mpris-server/pkg/events"
"github.com/quarckster/go-mpris-server/pkg/server" "github.com/quarckster/go-mpris-server/pkg/server"
. "github.com/quarckster/go-mpris-server/pkg/types" mprisTypes "github.com/quarckster/go-mpris-server/pkg/types"
) )
const ( const (
mprisPlayerNmae = "MehSonic" mprisPlayerNmae = "SubsonicTUI"
mprisNoTrack = "/org/mpris/MediaPlayer2/TrackList/NoTrack" mprisNoTrack = "/org/mpris/MediaPlayer2/TrackList/NoTrack"
) )
@ -22,24 +22,31 @@ type mprisRoot struct{}
func (r mprisRoot) Raise() error { func (r mprisRoot) Raise() error {
return nil return nil
} }
func (r mprisRoot) Quit() error { func (r mprisRoot) Quit() error {
return nil return nil
} }
func (r mprisRoot) CanQuit() (bool, error) { func (r mprisRoot) CanQuit() (bool, error) {
return true, nil return true, nil
} }
func (r mprisRoot) CanRaise() (bool, error) { func (r mprisRoot) CanRaise() (bool, error) {
return false, nil return false, nil
} }
func (r mprisRoot) HasTrackList() (bool, error) { func (r mprisRoot) HasTrackList() (bool, error) {
return false, nil return false, nil
} }
func (r mprisRoot) Identity() (string, error) { func (r mprisRoot) Identity() (string, error) {
return mprisPlayerNmae, nil return mprisPlayerNmae, nil
} }
func (r mprisRoot) SupportedUriSchemes() ([]string, error) { func (r mprisRoot) SupportedUriSchemes() ([]string, error) {
return []string{}, nil return []string{}, nil
} }
func (r mprisRoot) SupportedMimeTypes() ([]string, error) { func (r mprisRoot) SupportedMimeTypes() ([]string, error) {
return []string{}, nil return []string{}, nil
} }
@ -48,21 +55,27 @@ type mprisPlayer struct {
ctrl *Controller ctrl *Controller
} }
// Implement other methods of `pkg.types.OrgMprisMediaPlayer2PlayerAdapter` // Implement other methods of `pkg.types.OrgMprisMediaPlayer2PlayerAdapter`.
func (p mprisPlayer) Next() error { func (p mprisPlayer) Next() error {
p.ctrl.Next() p.ctrl.Next()
return nil return nil
} }
func (p mprisPlayer) Previous() error { func (p mprisPlayer) Previous() error {
p.ctrl.Prev() p.ctrl.Prev()
return nil return nil
} }
func (p mprisPlayer) Pause() error { func (p mprisPlayer) Pause() error {
if p.ctrl.State() == PlaybackStatePlaying { if p.ctrl.State() == PlaybackStatePlaying {
p.ctrl.TogglePlayPause() p.ctrl.TogglePlayPause()
} }
return nil return nil
} }
func (p mprisPlayer) PlayPause() error { func (p mprisPlayer) PlayPause() error {
switch p.ctrl.State() { switch p.ctrl.State() {
case PlaybackStatePaused, PlaybackStatePlaying: case PlaybackStatePaused, PlaybackStatePlaying:
@ -70,12 +83,16 @@ func (p mprisPlayer) PlayPause() error {
case PlaybackStateStopped: case PlaybackStateStopped:
p.ctrl.Play(p.ctrl.GetCurrentSong()) p.ctrl.Play(p.ctrl.GetCurrentSong())
} }
return nil return nil
} }
func (p mprisPlayer) Stop() error { func (p mprisPlayer) Stop() error {
p.ctrl.Stop() p.ctrl.Stop()
return nil return nil
} }
func (p mprisPlayer) Play() error { func (p mprisPlayer) Play() error {
switch p.ctrl.State() { switch p.ctrl.State() {
case PlaybackStatePaused: case PlaybackStatePaused:
@ -83,45 +100,54 @@ func (p mprisPlayer) Play() error {
case PlaybackStateStopped: case PlaybackStateStopped:
p.ctrl.Play(p.ctrl.GetCurrentSong()) p.ctrl.Play(p.ctrl.GetCurrentSong())
} }
return nil return nil
} }
func (p mprisPlayer) Seek(offset Microseconds) error {
func (p mprisPlayer) Seek(offset mprisTypes.Microseconds) error {
return nil return nil
} }
func (p mprisPlayer) SetPosition(trackId string, position Microseconds) error {
func (p mprisPlayer) SetPosition(trackId string, position mprisTypes.Microseconds) error {
return nil return nil
} }
func (p mprisPlayer) OpenUri(uri string) error { func (p mprisPlayer) OpenUri(uri string) error {
return nil return nil
} }
func (p mprisPlayer) PlaybackStatus() (PlaybackStatus, error) {
func (p mprisPlayer) PlaybackStatus() (mprisTypes.PlaybackStatus, error) {
switch p.ctrl.State() { switch p.ctrl.State() {
case PlaybackStatePlaying: case PlaybackStatePlaying:
return PlaybackStatusPlaying, nil return mprisTypes.PlaybackStatusPlaying, nil
case PlaybackStatePaused: case PlaybackStatePaused:
return PlaybackStatusPaused, nil return mprisTypes.PlaybackStatusPaused, nil
case PlaybackStateStopped: case PlaybackStateStopped:
return PlaybackStatusStopped, nil return mprisTypes.PlaybackStatusStopped, nil
} }
// Should not get here // Should not get here
return PlaybackStatusStopped, nil return mprisTypes.PlaybackStatusStopped, nil
} }
func (p mprisPlayer) Rate() (float64, error) { func (p mprisPlayer) Rate() (float64, error) {
return 1, nil return 1, nil
} }
func (p mprisPlayer) SetRate(float64) error { func (p mprisPlayer) SetRate(float64) error {
return nil return nil
} }
func (p mprisPlayer) Metadata() (Metadata, error) {
func (p mprisPlayer) Metadata() (mprisTypes.Metadata, error) {
s := p.ctrl.GetCurrentSong() s := p.ctrl.GetCurrentSong()
objPath := mprisNoTrack objPath := mprisNoTrack
if s != nil { if s != nil {
objPath = encodeTrackId(s.ID) objPath = encodeTrackID(s.ID)
} else { } else {
s = &subsonic.Child{} s = &subsonic.Child{}
} }
md := Metadata{
md := mprisTypes.Metadata{
TrackId: dbus.ObjectPath(objPath), TrackId: dbus.ObjectPath(objPath),
Length: secondsToMicroseconds(s.Duration), Length: secondsToMicroseconds(s.Duration),
Title: s.Title, Title: s.Title,
@ -134,41 +160,54 @@ func (p mprisPlayer) Metadata() (Metadata, error) {
UseCount: int(s.PlayCount), UseCount: int(s.PlayCount),
} }
artw := client.ArtCache.GetPath(s.CoverArt) artw := client.ArtCache.GetPath(s.CoverArt)
if artw != nil { if artw != nil {
md.ArtUrl = fmt.Sprintf("file://%s", *artw) md.ArtUrl = "file://" + *artw
} }
return md, nil return md, nil
} }
func (p mprisPlayer) Volume() (float64, error) { func (p mprisPlayer) Volume() (float64, error) {
return 1, nil return 1, nil
} }
func (p mprisPlayer) SetVolume(float64) error { func (p mprisPlayer) SetVolume(float64) error {
return nil return nil
} }
func (p mprisPlayer) Position() (int64, error) { func (p mprisPlayer) Position() (int64, error) {
return int64(secondsToMicroseconds(int(p.ctrl.position))), nil return int64(secondsToMicroseconds(int(p.ctrl.position))), nil
} }
func (p mprisPlayer) MinimumRate() (float64, error) { func (p mprisPlayer) MinimumRate() (float64, error) {
return 1, nil return 1, nil
} }
func (p mprisPlayer) MaximumRate() (float64, error) { func (p mprisPlayer) MaximumRate() (float64, error) {
return 1, nil return 1, nil
} }
func (p mprisPlayer) CanGoNext() (bool, error) { func (p mprisPlayer) CanGoNext() (bool, error) {
return p.ctrl.queue.HasNext(), nil return p.ctrl.queue.HasNext(), nil
} }
func (p mprisPlayer) CanGoPrevious() (bool, error) { func (p mprisPlayer) CanGoPrevious() (bool, error) {
return p.ctrl.queue.HasPrev(), nil return p.ctrl.queue.HasPrev(), nil
} }
func (p mprisPlayer) CanPlay() (bool, error) { func (p mprisPlayer) CanPlay() (bool, error) {
return p.ctrl.GetCurrentSong() != nil, nil return p.ctrl.GetCurrentSong() != nil, nil
} }
func (p mprisPlayer) CanPause() (bool, error) { func (p mprisPlayer) CanPause() (bool, error) {
return true, nil return true, nil
} }
func (p mprisPlayer) CanSeek() (bool, error) { func (p mprisPlayer) CanSeek() (bool, error) {
return false, nil return false, nil
} }
func (p mprisPlayer) CanControl() (bool, error) { func (p mprisPlayer) CanControl() (bool, error) {
return true, nil return true, nil
} }
@ -197,18 +236,23 @@ func (p *mprisPlayback) OnPlayPause() {
if p.err != nil { if p.err != nil {
return return
} }
p.err = p.eventHandler.Player.OnPlayPause() p.err = p.eventHandler.Player.OnPlayPause()
} }
func (p *mprisPlayback) OnPlaylistChanged() { func (p *mprisPlayback) OnPlaylistChanged() {
if p.err != nil { if p.err != nil {
return return
} }
p.err = p.eventHandler.Player.OnOptions() p.err = p.eventHandler.Player.OnOptions()
} }
func (p *mprisPlayback) OnSongChanged() { func (p *mprisPlayback) OnSongChanged() {
if p.err != nil { if p.err != nil {
return return
} }
p.err = p.eventHandler.Player.OnTitle() p.err = p.eventHandler.Player.OnTitle()
} }
@ -216,10 +260,12 @@ func (p *mprisPlayback) OnPositionChanged(position int) {
if p.err != nil { if p.err != nil {
return return
} }
p.err = p.eventHandler.Player.OnSeek(secondsToMicroseconds(position)) p.err = p.eventHandler.Player.OnSeek(secondsToMicroseconds(position))
if p.err != nil { if p.err != nil {
return return
} }
p.err = p.eventHandler.Player.OnOptions() p.err = p.eventHandler.Player.OnOptions()
} }
@ -239,11 +285,12 @@ func desktopPlayer(c *Controller) DesktopPlayback {
} }
} }
func secondsToMicroseconds(s int) Microseconds { func secondsToMicroseconds(s int) mprisTypes.Microseconds {
return Microseconds(s * 1_000_000) return mprisTypes.Microseconds(s * 1_000_000)
} }
func encodeTrackId(id string) string { func encodeTrackID(id string) string {
data := []byte(id) data := []byte(id)
return fmt.Sprintf("/%s/Track/%s", mprisPlayerNmae, base32.StdEncoding.WithPadding('0').EncodeToString(data)) return fmt.Sprintf("/%s/Track/%s", mprisPlayerNmae, base32.StdEncoding.WithPadding('0').EncodeToString(data))
} }

View file

@ -24,23 +24,28 @@ func (q *queue) Clear() {
q.songQueue = []*subsonic.Child{} q.songQueue = []*subsonic.Child{}
} }
// returns true if queue was empty before addition // returns true if queue was empty before addition.
func (q *queue) Add(songs ...*subsonic.Child) bool { func (q *queue) Add(songs ...*subsonic.Child) bool {
shouldStartPlaying := len(q.songQueue) == 0 shouldStartPlaying := len(q.songQueue) == 0
q.songQueue = append(q.songQueue, songs...) q.songQueue = append(q.songQueue, songs...)
if shouldStartPlaying { if shouldStartPlaying {
q.currentSong = 0 q.currentSong = 0
return true return true
} }
return false return false
} }
// returns a song if position has changed // returns a song if position has changed.
func (q *queue) SetPosition(position int) *subsonic.Child { func (q *queue) SetPosition(position int) *subsonic.Child {
if position == q.currentSong || position < 0 || len(q.songQueue) < position { if position == q.currentSong || position < 0 || len(q.songQueue) < position {
return nil return nil
} }
q.currentSong = position q.currentSong = position
return q.GetCurrentSong() return q.GetCurrentSong()
} }
@ -52,6 +57,7 @@ func (q *queue) GetCurrentSong() *subsonic.Child {
if len(q.songQueue) == 0 { if len(q.songQueue) == 0 {
return nil return nil
} }
return q.songQueue[q.currentSong] return q.songQueue[q.currentSong]
} }
@ -65,17 +71,21 @@ func (q *queue) HasNext() bool {
func (q *queue) Next() *subsonic.Child { func (q *queue) Next() *subsonic.Child {
if len(q.songQueue) > q.currentSong+1 { if len(q.songQueue) > q.currentSong+1 {
q.currentSong = q.currentSong + 1 q.currentSong++
return q.GetCurrentSong() return q.GetCurrentSong()
} }
return nil return nil
} }
func (q *queue) Prev() *subsonic.Child { func (q *queue) Prev() *subsonic.Child {
if q.currentSong > 0 { if q.currentSong > 0 {
q.currentSong = q.currentSong - 1 q.currentSong--
return q.GetCurrentSong() return q.GetCurrentSong()
} }
return nil return nil
} }

View file

@ -24,12 +24,14 @@ func NewLogin() *TUI {
app := tview.NewApplication() app := tview.NewApplication()
layout := views.NewLoginView(func(u, p, url string) { layout := views.NewLoginView(func(u, p, url string) {
c := client.NewClient(url) c := client.NewClient(url)
err := c.Authenticate(u, p) err := c.Authenticate(u, p)
if err != nil { if err != nil {
app.Stop() app.Stop()
fmt.Printf("[Error] Failed to login. Aborting %e", err) fmt.Printf("[Error] Failed to login. Aborting %e", err)
os.Exit(1) os.Exit(1)
} }
config.SetURL(url) config.SetURL(url)
config.SetUsername(u) config.SetUsername(u)
config.SetPassword(p) config.SetPassword(p)
@ -47,6 +49,7 @@ func NewLogin() *TUI {
app.Stop() app.Stop()
os.Exit(0) os.Exit(0)
} }
return event return event
}) })
@ -79,30 +82,40 @@ func NewPlayer(client *client.Client, playbackCtl *playback.Controller) *TUI {
pages.SwitchToPage("app") pages.SwitchToPage("app")
help.GetView().Blur() help.GetView().Blur()
layout.GetView().Focus(nil) layout.GetView().Focus(nil)
return nil return nil
} }
if layout.Mode() == views.StatusModeSearch { if layout.Mode() == views.StatusModeSearch {
return event return event
} }
if event.Rune() == 'q' {
switch event.Rune() {
case 'q':
app.Stop() app.Stop()
fmt.Println("Exiting..") fmt.Println("Exiting..")
return nil return nil
} else if event.Rune() == 'h' { case 'h':
return tcell.NewEventKey(tcell.KeyLeft, rune(tcell.KeyLeft), event.Modifiers()) return tcell.NewEventKey(tcell.KeyLeft, rune(tcell.KeyLeft), event.Modifiers())
} else if event.Rune() == 'j' { case 'j':
return tcell.NewEventKey(tcell.KeyDown, rune(tcell.KeyDown), event.Modifiers()) return tcell.NewEventKey(tcell.KeyDown, rune(tcell.KeyDown), event.Modifiers())
} else if event.Rune() == 'k' { case 'k':
return tcell.NewEventKey(tcell.KeyUp, rune(tcell.KeyUp), event.Modifiers()) return tcell.NewEventKey(tcell.KeyUp, rune(tcell.KeyUp), event.Modifiers())
} else if event.Rune() == 'l' { case 'l':
return tcell.NewEventKey(tcell.KeyRight, rune(tcell.KeyRight), event.Modifiers()) return tcell.NewEventKey(tcell.KeyRight, rune(tcell.KeyRight), event.Modifiers())
} else if event.Rune() == '?' { case '?':
pages.SwitchToPage("help") pages.SwitchToPage("help")
layout.GetView().Blur() layout.GetView().Blur()
go app.Draw() go app.Draw()
default:
return event
} }
return event return event
}) })
app.SetAfterDrawFunc(func(screen tcell.Screen) { app.SetAfterDrawFunc(func(screen tcell.Screen) {
layout.Update() layout.Update()
}) })
@ -116,7 +129,6 @@ func NewPlayer(client *client.Client, playbackCtl *playback.Controller) *TUI {
client: client, client: client,
playbackCtl: playbackCtl, playbackCtl: playbackCtl,
} }
} }
func (t *TUI) Run() error { func (t *TUI) Run() error {

View file

@ -17,7 +17,6 @@ type albums struct {
} }
func NewAlbums(client *client.Client) *albums { func NewAlbums(client *client.Client) *albums {
list := tview.NewTable() list := tview.NewTable()
list.SetBackgroundColor(config.ColorBackground) list.SetBackgroundColor(config.ColorBackground)
@ -46,6 +45,7 @@ func NewAlbums(client *client.Client) *albums {
}) })
obj.Update() obj.Update()
return obj return obj
} }
@ -56,9 +56,11 @@ func (a *albums) SetAlbums(al []*subsonic.AlbumID3) {
func (a *albums) Update() { func (a *albums) Update() {
a.view.Clear() a.view.Clear()
for i, pl := range a.albums { for i, pl := range a.albums {
title := tview.NewTableCell(pl.Name).SetExpansion(1).SetMaxWidth(15) title := tview.NewTableCell(pl.Name).SetExpansion(1).SetMaxWidth(15)
artist := tview.NewTableCell(pl.Artist).SetExpansion(1).SetAlign(tview.AlignRight) artist := tview.NewTableCell(pl.Artist).SetExpansion(1).SetAlign(tview.AlignRight)
a.view.SetCell(i, 0, title) a.view.SetCell(i, 0, title)
a.view.SetCell(i, 1, artist) a.view.SetCell(i, 1, artist)
} }

View file

@ -18,7 +18,6 @@ type artists struct {
} }
func NewArtists(client *client.Client) *artists { func NewArtists(client *client.Client) *artists {
list := tview.NewTable() list := tview.NewTable()
list.SetBackgroundColor(config.ColorBackground) list.SetBackgroundColor(config.ColorBackground)
@ -38,7 +37,6 @@ func NewArtists(client *client.Client) *artists {
for i, artist := range arts { for i, artist := range arts {
cell := tview.NewTableCell(artist.Name).SetExpansion(1) cell := tview.NewTableCell(artist.Name).SetExpansion(1)
list.SetCell(i, 0, cell) list.SetCell(i, 0, cell)
// list.AddItem(artist.Name, fmt.Sprintf("%s", artist.Name), '0', nil)
} }
resp := &artists{ resp := &artists{
@ -57,12 +55,12 @@ func NewArtists(client *client.Client) *artists {
func (a *artists) SetSelectArtistFunc(f func(artistId string)) { func (a *artists) SetSelectArtistFunc(f func(artistId string)) {
a.selectArtistFunc = f a.selectArtistFunc = f
} }
func (a *artists) SetOpenArtistFunc(f func(artistId string)) { func (a *artists) SetOpenArtistFunc(f func(artistId string)) {
a.openArtistFunc = f a.openArtistFunc = f
} }
func (a *artists) Update() { func (a *artists) Update() {
} }
func (a *artists) GetView() tview.Primitive { func (a *artists) GetView() tview.Primitive {

View file

@ -60,8 +60,10 @@ func NewHelp() *help {
h := &help{} h := &help{}
view := tview.NewTable() view := tview.NewTable()
view.SetBackgroundColor(config.ColorBackground) view.SetBackgroundColor(config.ColorBackground)
height := 16 height := 16
width := 100 width := 100
view.SetBorder(true) view.SetBorder(true)
view.SetTitle(" Keybindings ") view.SetTitle(" Keybindings ")
view.SetTitleAlign(tview.AlignCenter) view.SetTitleAlign(tview.AlignCenter)
@ -69,9 +71,11 @@ func NewHelp() *help {
for i, km := range keyMaps { for i, km := range keyMaps {
odd := i%2 != 0 odd := i%2 != 0
txtColor := config.ColorText txtColor := config.ColorText
if odd { if odd {
txtColor = config.ColorTextAccent txtColor = config.ColorTextAccent
} }
keyCell := tview.NewTableCell(km.key). keyCell := tview.NewTableCell(km.key).
SetExpansion(1). SetExpansion(1).
SetAlign(tview.AlignCenter). SetAlign(tview.AlignCenter).
@ -80,6 +84,7 @@ func NewHelp() *help {
SetExpansion(1). SetExpansion(1).
SetAlign(tview.AlignCenter). SetAlign(tview.AlignCenter).
SetTextColor(txtColor) SetTextColor(txtColor)
view.SetCell(i, 0, keyCell) view.SetCell(i, 0, keyCell)
view.SetCell(i, 1, descCell) view.SetCell(i, 1, descCell)
} }
@ -94,7 +99,6 @@ func NewHelp() *help {
AddItem(innerFlex, width, 1, true). AddItem(innerFlex, width, 1, true).
AddItem(EmptyBox, 0, 1, false) AddItem(EmptyBox, 0, 1, false)
wrapper.SetBackgroundColor((config.ColorBackground)) wrapper.SetBackgroundColor((config.ColorBackground))
h.view = wrapper h.view = wrapper
return h return h
@ -105,7 +109,6 @@ func (h *help) GetView() tview.Primitive {
} }
func (h *help) Update() { func (h *help) Update() {
} }
func (h *help) SetKeyPressedFunc(f func()) { func (h *help) SetKeyPressedFunc(f func()) {

View file

@ -23,13 +23,16 @@ type layout struct {
mainView *main mainView *main
} }
//gocyclo:ignore
func NewLayout(client *client.Client, playbackCtl *playback.Controller, refreshUI func()) *layout { func NewLayout(client *client.Client, playbackCtl *playback.Controller, refreshUI func()) *layout {
layout := &layout{} layout := &layout{}
largeView := tview.NewGrid().SetRows(0, 4, 1) largeView := tview.NewGrid().SetRows(0, 4, 1)
largeView.SetBackgroundColor(config.ColorBackground) largeView.SetBackgroundColor(config.ColorBackground)
smallView := tview.NewFlex().SetDirection(tview.FlexRow) smallView := tview.NewFlex().SetDirection(tview.FlexRow)
smallView.SetBackgroundColor(config.ColorBackground) smallView.SetBackgroundColor(config.ColorBackground)
layout.largeView = largeView layout.largeView = largeView
layout.smallView = smallView layout.smallView = smallView
pages := tview.NewPages() pages := tview.NewPages()
@ -73,6 +76,7 @@ func NewLayout(client *client.Client, playbackCtl *playback.Controller, refreshU
artists.SetSelectArtistFunc(func(artistId string) { artists.SetSelectArtistFunc(func(artistId string) {
artists.view.Blur() artists.view.Blur()
statusLine.Log("Fetching artist's albums...") statusLine.Log("Fetching artist's albums...")
go func() { go func() {
a, _ := client.GetArtist(artistId) a, _ := client.GetArtist(artistId)
albums.SetAlbums(a.Album) albums.SetAlbums(a.Album)
@ -83,8 +87,10 @@ func NewLayout(client *client.Client, playbackCtl *playback.Controller, refreshU
artists.SetOpenArtistFunc(func(artistId string) { artists.SetOpenArtistFunc(func(artistId string) {
artists.view.Blur() artists.view.Blur()
statusLine.Log("Fetching artists...") statusLine.Log("Fetching artists...")
layout.currentFocusedView = main.GetView() layout.currentFocusedView = main.GetView()
layout.rebuildSmallView() layout.rebuildSmallView()
go func() { go func() {
a, _ := client.GetArtist(artistId) a, _ := client.GetArtist(artistId)
main.SetArtist(a) main.SetArtist(a)
@ -96,8 +102,11 @@ func NewLayout(client *client.Client, playbackCtl *playback.Controller, refreshU
albums.SetCallback(func(albumID string) { albums.SetCallback(func(albumID string) {
albums.view.Blur() albums.view.Blur()
statusLine.Log("Fetching album...") statusLine.Log("Fetching album...")
layout.currentFocusedView = main.GetView() layout.currentFocusedView = main.GetView()
layout.rebuildSmallView() layout.rebuildSmallView()
go func() { go func() {
a, _ := client.GetAlbum(albumID) a, _ := client.GetAlbum(albumID)
main.SetAlbum(a) main.SetAlbum(a)
@ -109,8 +118,10 @@ func NewLayout(client *client.Client, playbackCtl *playback.Controller, refreshU
playlists.SetCallback(func(p *subsonic.Playlist) { playlists.SetCallback(func(p *subsonic.Playlist) {
playlists.view.Blur() playlists.view.Blur()
statusLine.Log("Fetching playlist...") statusLine.Log("Fetching playlist...")
layout.currentFocusedView = main.GetView() layout.currentFocusedView = main.GetView()
layout.rebuildSmallView() layout.rebuildSmallView()
go func() { go func() {
playlist, _ := client.GetPlaylist(p.ID) playlist, _ := client.GetPlaylist(p.ID)
main.SetPlaylist(playlist) main.SetPlaylist(playlist)
@ -140,6 +151,7 @@ func NewLayout(client *client.Client, playbackCtl *playback.Controller, refreshU
playbackCtl.SetSongEndedFunc(func(song *subsonic.Child) { playbackCtl.SetSongEndedFunc(func(song *subsonic.Child) {
statusLine.Log("") statusLine.Log("")
queue.Update(playbackCtl.GetQueue(), playbackCtl.GetQueuePosition()) queue.Update(playbackCtl.GetQueue(), playbackCtl.GetQueuePosition())
if config.ScrobbleEnabled() { if config.ScrobbleEnabled() {
// Scrobble // Scrobble
_ = client.Scrobble(song.ID) _ = client.Scrobble(song.ID)
@ -159,13 +171,16 @@ func NewLayout(client *client.Client, playbackCtl *playback.Controller, refreshU
if len(quary) == 0 { if len(quary) == 0 {
layout.currentFocusedView.Focus(nil) layout.currentFocusedView.Focus(nil)
statusLine.Log("Search canceled") statusLine.Log("Search canceled")
return return
} }
// Search... // Search...
statusLine.Log("Searching for '%s'....", quary) statusLine.Log("Searching for '%s'....", quary)
statusLine.view.Blur() statusLine.view.Blur()
go func() { go func() {
result, _ := client.Search(quary) result, _ := client.Search(quary)
layout.currentFocusedView.Blur() layout.currentFocusedView.Blur()
main.SetSearch(result, quary) main.SetSearch(result, quary)
layout.currentFocusedView = main.GetView() layout.currentFocusedView = main.GetView()
@ -179,6 +194,7 @@ func NewLayout(client *client.Client, playbackCtl *playback.Controller, refreshU
if statusLine.Mode() == StatusModeSearch { if statusLine.Mode() == StatusModeSearch {
return event return event
} }
if event.Rune() == '1' { if event.Rune() == '1' {
// Focus Artists // Focus Artists
artists.view.Blur() artists.view.Blur()
@ -189,6 +205,7 @@ func NewLayout(client *client.Client, playbackCtl *playback.Controller, refreshU
artists.view.Focus(nil) artists.view.Focus(nil)
layout.currentFocusedView = artists.GetView() layout.currentFocusedView = artists.GetView()
layout.rebuildSmallView() layout.rebuildSmallView()
return nil return nil
} else if event.Rune() == '2' { } else if event.Rune() == '2' {
// Focus Albums // Focus Albums
@ -200,6 +217,7 @@ func NewLayout(client *client.Client, playbackCtl *playback.Controller, refreshU
albums.view.Focus(nil) albums.view.Focus(nil)
layout.currentFocusedView = albums.GetView() layout.currentFocusedView = albums.GetView()
layout.rebuildSmallView() layout.rebuildSmallView()
return nil return nil
} else if event.Rune() == '3' { } else if event.Rune() == '3' {
// Focus Playlists // Focus Playlists
@ -211,6 +229,7 @@ func NewLayout(client *client.Client, playbackCtl *playback.Controller, refreshU
playlists.view.Focus(nil) playlists.view.Focus(nil)
layout.currentFocusedView = playlists.GetView() layout.currentFocusedView = playlists.GetView()
layout.rebuildSmallView() layout.rebuildSmallView()
return nil return nil
} else if event.Rune() == '`' { } else if event.Rune() == '`' {
// Focus Songs // Focus Songs
@ -222,6 +241,7 @@ func NewLayout(client *client.Client, playbackCtl *playback.Controller, refreshU
main.view.Focus(nil) main.view.Focus(nil)
layout.currentFocusedView = main.GetView() layout.currentFocusedView = main.GetView()
layout.rebuildSmallView() layout.rebuildSmallView()
return nil return nil
} else if event.Rune() == '4' { } else if event.Rune() == '4' {
// Focus Queue // Focus Queue
@ -233,19 +253,22 @@ func NewLayout(client *client.Client, playbackCtl *playback.Controller, refreshU
queue.view.Focus(nil) queue.view.Focus(nil)
layout.currentFocusedView = queue.GetView() layout.currentFocusedView = queue.GetView()
layout.rebuildSmallView() layout.rebuildSmallView()
return nil return nil
} else if event.Rune() == 'n' { } else if event.Rune() == 'n' {
playbackCtl.Next() playbackCtl.Next()
queue.Update(playbackCtl.GetQueue(), playbackCtl.GetQueuePosition()) queue.Update(playbackCtl.GetQueue(), playbackCtl.GetQueuePosition())
return nil
return nil
} else if event.Rune() == 'N' { } else if event.Rune() == 'N' {
playbackCtl.Prev() playbackCtl.Prev()
queue.Update(playbackCtl.GetQueue(), playbackCtl.GetQueuePosition()) queue.Update(playbackCtl.GetQueue(), playbackCtl.GetQueuePosition())
return nil return nil
} else if event.Rune() == 's' { } else if event.Rune() == 's' {
playbackCtl.Stop() playbackCtl.Stop()
queue.Update(playbackCtl.GetQueue(), playbackCtl.GetQueuePosition()) queue.Update(playbackCtl.GetQueue(), playbackCtl.GetQueuePosition())
return nil return nil
} else if event.Rune() == 'p' { } else if event.Rune() == 'p' {
if playbackCtl.State() == playback.PlaybackStateStopped { if playbackCtl.State() == playback.PlaybackStateStopped {
@ -253,19 +276,25 @@ func NewLayout(client *client.Client, playbackCtl *playback.Controller, refreshU
if song != nil { if song != nil {
playbackCtl.Play(song) playbackCtl.Play(song)
} }
queue.Update(playbackCtl.GetQueue(), playbackCtl.GetQueuePosition()) queue.Update(playbackCtl.GetQueue(), playbackCtl.GetQueuePosition())
return nil return nil
} }
playbackCtl.TogglePlayPause() playbackCtl.TogglePlayPause()
return nil return nil
} else if event.Rune() == 'c' { } else if event.Rune() == 'c' {
playbackCtl.ClearQueue() playbackCtl.ClearQueue()
queue.Update(playbackCtl.GetQueue(), playbackCtl.GetQueuePosition()) queue.Update(playbackCtl.GetQueue(), playbackCtl.GetQueuePosition())
return nil return nil
} else if event.Rune() == '/' { } else if event.Rune() == '/' {
layout.currentFocusedView.Blur() layout.currentFocusedView.Blur()
statusLine.Search() statusLine.Search()
refreshUI() refreshUI()
return nil return nil
} else if event.Rune() == 'L' { } else if event.Rune() == 'L' {
if layout.currentFocusedView == albums.GetView() { if layout.currentFocusedView == albums.GetView() {
@ -360,8 +389,10 @@ func NewLayout(client *client.Client, playbackCtl *playback.Controller, refreshU
artists.GetView().Focus(nil) artists.GetView().Focus(nil)
layout.currentFocusedView = artists.GetView() layout.currentFocusedView = artists.GetView()
layout.rebuildSmallView() layout.rebuildSmallView()
return layout return layout
} }
func (l *layout) rebuildSmallView() { func (l *layout) rebuildSmallView() {
l.smallView.Clear() l.smallView.Clear()
l.smallView.AddItem(l.currentFocusedView, 0, 1, false) l.smallView.AddItem(l.currentFocusedView, 0, 1, false)
@ -381,18 +412,22 @@ func (l *layout) Update() {
_, _, w, h := l.view.GetRect() _, _, w, h := l.view.GetRect()
page, _ := l.view.GetFrontPage() page, _ := l.view.GetFrontPage()
smallView := w < 100 || h < 30 smallView := w < 100 || h < 30
if smallView { if smallView {
if page != "small" { if page != "small" {
l.mainView.SetMiniView(true) l.mainView.SetMiniView(true)
go l.view.SwitchToPage("small") go l.view.SwitchToPage("small")
return return
} }
} else { } else {
if page != "large" { if page != "large" {
l.mainView.SetMiniView(false) l.mainView.SetMiniView(false)
go l.view.SwitchToPage("large") go l.view.SwitchToPage("large")
return return
} }
} }
l.player.Update() l.player.Update()
} }

View file

@ -65,9 +65,9 @@ func NewLoginView(loginFunc func(u, p, url string), exitFunc func()) View {
l.GetView().Focus(func(p tview.Primitive) {}) l.GetView().Focus(func(p tview.Primitive) {})
form.SetFocus(0) form.SetFocus(0)
return l return l
} }
func (l *login) Update() { func (l *login) Update() {
} }

View file

@ -54,6 +54,7 @@ func NewMainView(client *client.Client, log func(format string, a ...any)) *main
flex.SetBorder(true) flex.SetBorder(true)
flex.SetFocusFunc(func() { flex.SetFocusFunc(func() {
flex.SetBorderColor(config.ColorSelectedBoarder) flex.SetBorderColor(config.ColorSelectedBoarder)
if playlistAlbum.songList != nil { if playlistAlbum.songList != nil {
// playlistAlbum.view.Blur() // playlistAlbum.view.Blur()
playlistAlbum.songList.Focus(nil) playlistAlbum.songList.Focus(nil)
@ -61,6 +62,7 @@ func NewMainView(client *client.Client, log func(format string, a ...any)) *main
}) })
flex.SetBlurFunc(func() { flex.SetBlurFunc(func() {
flex.SetBorderColor(config.ColorBluredBoarder) flex.SetBorderColor(config.ColorBluredBoarder)
if playlistAlbum.songList != nil { if playlistAlbum.songList != nil {
playlistAlbum.songList.Blur() playlistAlbum.songList.Blur()
} }
@ -69,6 +71,7 @@ func NewMainView(client *client.Client, log func(format string, a ...any)) *main
playlistAlbum.view = flex playlistAlbum.view = flex
// Empty Box for starters... // Empty Box for starters...
playlistAlbum.view.AddItem(EmptyBox, 0, 1, false) playlistAlbum.view.AddItem(EmptyBox, 0, 1, false)
return playlistAlbum return playlistAlbum
} }
@ -112,34 +115,43 @@ func (m *main) SetSearch(result *subsonic.SearchResult3, query string) {
} }
func (m *main) drawPlaylist() { 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()) 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) m.populateHeader(m.playlist.Name, subtitle, m.playlist.Duration, m.playlist.CoverArt)
playBtn := m.drawPlaylistAlbumButtons(m.playlist.Entry) playBtn := m.drawPlaylistAlbumButtons(m.playlist.Entry)
m.populateSongs(m.playlist.Entry, playBtn) m.populateSongs(m.playlist.Entry, playBtn)
} }
func (m *main) drawAlbum() { 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()) 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) m.populateHeader(m.album.Name, subtitle, m.album.Duration, m.album.CoverArt)
playBtn := m.drawPlaylistAlbumButtons(m.album.Song)
m.populateSongs(m.album.Song, playBtn)
playBtn := m.drawPlaylistAlbumButtons(m.album.Song)
m.populateSongs(m.album.Song, playBtn)
} }
func (m *main) drawArtist() { func (m *main) drawArtist() {
m.populateHeader(m.artist.Name, m.artistInfo.Biography, 0, m.artist.CoverArt) m.populateHeader(m.artist.Name, m.artistInfo.Biography, 0, m.artist.CoverArt)
btn := m.drawArtistButtons() btn := m.drawArtistButtons()
m.populateAlbums(btn) m.populateAlbums(btn)
} }
func (m *main) drawSearch() { func (m *main) drawSearch() {
sub := fmt.Sprintf("Query: %s", m.query) sub := "Query: " + m.query
m.populateHeader("Search Results", sub, 0, "") m.populateHeader("Search Results", sub, 0, "")
m.populateSearchResults() m.populateSearchResults()
} }
func (m *main) Update() { func (m *main) Update() {
m.view.Clear() m.view.Clear()
switch m.mode { switch m.mode {
case mainModeAlbum: case mainModeAlbum:
m.drawAlbum() m.drawAlbum()
@ -150,12 +162,14 @@ func (m *main) Update() {
case mainModeSearch: case mainModeSearch:
m.drawSearch() m.drawSearch()
} }
m.songList.Focus(nil) m.songList.Focus(nil)
} }
func (m *main) drawArtistButtons() *tview.Button { func (m *main) drawArtistButtons() *tview.Button {
// Add buttons: Radio // Add buttons: Radio
songs, _ := m.client.GetTopSongs(m.artist.Name, 10) topSongs := 10
songs, _ := m.client.GetTopSongs(m.artist.Name, topSongs)
f := tview.NewFlex() f := tview.NewFlex()
f.SetBackgroundColor(config.ColorBackground) f.SetBackgroundColor(config.ColorBackground)
f.SetBorderPadding(0, 0, 2, 2) f.SetBorderPadding(0, 0, 2, 2)
@ -172,11 +186,13 @@ func (m *main) drawArtistButtons() *tview.Button {
radio.SetSelectedFunc(func() { radio.SetSelectedFunc(func() {
radio.Blur() radio.Blur()
m.log("Generating %s's radio", m.artist.Name) m.log("Generating %s's radio", m.artist.Name)
go func() { go func() {
radioSongs := []*subsonic.Child{} radioSongs := []*subsonic.Child{}
if config.ExperimentalRadioAlgo() { if config.ExperimentalRadioAlgo() {
radioSongs, _ = m.client.GetExperimentalArtistRadio(m.artist, m.artistInfo, config.MaxRadioSongs()) radioSongs, _ = m.client.GetExperimentalArtistRadio(m.artist, m.artistInfo, config.MaxRadioSongs())
} }
if len(radioSongs) == 0 { if len(radioSongs) == 0 {
radioSongs, _ = m.client.GetSimilarSongs(m.artist.ID, config.MaxRadioSongs()) radioSongs, _ = m.client.GetSimilarSongs(m.artist.ID, config.MaxRadioSongs())
} }
@ -191,33 +207,41 @@ func (m *main) drawArtistButtons() *tview.Button {
case tcell.KeyDown: case tcell.KeyDown:
radio.Blur() radio.Blur()
m.songList.Focus(nil) m.songList.Focus(nil)
return nil return nil
case tcell.KeyRight: case tcell.KeyRight:
radio.Blur() radio.Blur()
top10.Focus(nil) top10.Focus(nil)
return nil return nil
} default:
return event return event
}
}) })
top10.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { top10.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
switch event.Key() { switch event.Key() {
case tcell.KeyDown: case tcell.KeyDown:
top10.Blur() top10.Blur()
m.songList.Focus(nil) m.songList.Focus(nil)
return nil return nil
case tcell.KeyLeft: case tcell.KeyLeft:
top10.Blur() top10.Blur()
radio.Focus(nil) radio.Focus(nil)
return nil return nil
} default:
return event return event
}
}) })
f.AddItem(radio, 0, 1, false) f.AddItem(radio, 0, 1, false)
f.AddItem(EmptyBox, 0, 1, false) f.AddItem(EmptyBox, 0, 1, false)
if len(songs) > 0 { if len(songs) > 0 {
f.AddItem(top10, 0, 1, false) f.AddItem(top10, 0, 1, false)
} }
f.AddItem(EmptyBox, 0, 1, false) f.AddItem(EmptyBox, 0, 1, false)
// Add the buttons to the view // Add the buttons to the view
@ -232,8 +256,9 @@ func (m *main) populateAlbums(btn *tview.Button) {
table := tview.NewTable() table := tview.NewTable()
table.SetBackgroundColor(config.ColorBackground) table.SetBackgroundColor(config.ColorBackground)
table.SetWrapSelection(true, false) table.SetWrapSelection(true, false)
for i, album := range m.artist.Album { for i, album := range m.artist.Album {
year := tview.NewTableCell(fmt.Sprintf("%d", album.Year)).SetTextColor(config.ColorTextAccent) year := tview.NewTableCell(strconv.Itoa(album.Year)).SetTextColor(config.ColorTextAccent)
name := tview.NewTableCell(album.Name).SetExpansion(2).SetAlign(tview.AlignCenter) name := tview.NewTableCell(album.Name).SetExpansion(2).SetAlign(tview.AlignCenter)
d := time.Second * time.Duration(album.Duration) d := time.Second * time.Duration(album.Duration)
duration := tview.NewTableCell(d.String()).SetAlign(tview.AlignRight).SetExpansion(1) duration := tview.NewTableCell(d.String()).SetAlign(tview.AlignRight).SetExpansion(1)
@ -262,14 +287,16 @@ func (m *main) populateAlbums(btn *tview.Button) {
table.Blur() table.Blur()
m.view.SetBorderColor(config.ColorSelectedBoarder) m.view.SetBorderColor(config.ColorSelectedBoarder)
btn.Focus(nil) btn.Focus(nil)
return nil return nil
} }
return event return event
}) })
m.songList = table m.songList = table
m.view.AddItem(table, 0, 1, false) m.view.AddItem(table, 0, 1, false)
} }
func (m *main) drawPlaylistAlbumButtons(songs []*subsonic.Child) *tview.Button { func (m *main) drawPlaylistAlbumButtons(songs []*subsonic.Child) *tview.Button {
@ -293,20 +320,25 @@ func (m *main) drawPlaylistAlbumButtons(songs []*subsonic.Child) *tview.Button {
case tcell.KeyLeft: case tcell.KeyLeft:
play.Blur() play.Blur()
artist.Focus(nil) artist.Focus(nil)
return nil return nil
case tcell.KeyRight: case tcell.KeyRight:
play.Blur() play.Blur()
shuffle.Focus(nil) shuffle.Focus(nil)
return nil return nil
case tcell.KeyDown: case tcell.KeyDown:
play.Blur() play.Blur()
m.songList.Focus(nil) m.songList.Focus(nil)
return nil return nil
} default:
return event return event
}
}) })
shuffle.SetSelectedFunc(func() { shuffle.SetSelectedFunc(func() {
shuffle.Blur() shuffle.Blur()
cpy := make([]*subsonic.Child, len(songs)) cpy := make([]*subsonic.Child, len(songs))
copy(cpy, songs) copy(cpy, songs)
common.ShuffleSlice(cpy) common.ShuffleSlice(cpy)
@ -318,17 +350,21 @@ func (m *main) drawPlaylistAlbumButtons(songs []*subsonic.Child) *tview.Button {
case tcell.KeyLeft: case tcell.KeyLeft:
shuffle.Blur() shuffle.Blur()
play.Focus(nil) play.Focus(nil)
return nil return nil
case tcell.KeyRight: case tcell.KeyRight:
shuffle.Blur() shuffle.Blur()
queue.Focus(nil) queue.Focus(nil)
return nil return nil
case tcell.KeyDown: case tcell.KeyDown:
shuffle.Blur() shuffle.Blur()
m.songList.Focus(nil) m.songList.Focus(nil)
return nil return nil
} default:
return event return event
}
}) })
queue.SetSelectedFunc(func() { queue.SetSelectedFunc(func() {
queue.Blur() queue.Blur()
@ -340,20 +376,25 @@ func (m *main) drawPlaylistAlbumButtons(songs []*subsonic.Child) *tview.Button {
case tcell.KeyLeft: case tcell.KeyLeft:
queue.Blur() queue.Blur()
shuffle.Focus(nil) shuffle.Focus(nil)
return nil return nil
case tcell.KeyRight: case tcell.KeyRight:
queue.Blur() queue.Blur()
artist.Focus(nil) artist.Focus(nil)
return nil return nil
case tcell.KeyDown: case tcell.KeyDown:
queue.Blur() queue.Blur()
m.songList.Focus(nil) m.songList.Focus(nil)
return nil return nil
} default:
return event return event
}
}) })
artist.SetSelectedFunc(func() { artist.SetSelectedFunc(func() {
artist.Blur() artist.Blur()
ar, _ := m.client.GetArtist(m.album.ArtistID) ar, _ := m.client.GetArtist(m.album.ArtistID)
m.SetArtist(ar) m.SetArtist(ar)
m.songList.Focus(nil) m.songList.Focus(nil)
@ -363,16 +404,20 @@ func (m *main) drawPlaylistAlbumButtons(songs []*subsonic.Child) *tview.Button {
case tcell.KeyLeft: case tcell.KeyLeft:
artist.Blur() artist.Blur()
queue.Focus(nil) queue.Focus(nil)
return nil return nil
case tcell.KeyRight: case tcell.KeyRight:
artist.Blur() artist.Blur()
play.Focus(nil) play.Focus(nil)
return nil return nil
case tcell.KeyDown: case tcell.KeyDown:
artist.Blur() artist.Blur()
m.songList.Focus(nil) m.songList.Focus(nil)
return nil return nil
} }
return event return event
}) })
@ -396,12 +441,14 @@ func (m *main) populateSongs(songs []*subsonic.Child, play *tview.Button) {
table := tview.NewTable() table := tview.NewTable()
table.SetBackgroundColor(config.ColorBackground) table.SetBackgroundColor(config.ColorBackground)
table.SetWrapSelection(true, false) table.SetWrapSelection(true, false)
for i, song := range songs { for i, song := range songs {
num := tview.NewTableCell(fmt.Sprintf("%d", i+1)).SetTextColor(config.ColorTextAccent) num := tview.NewTableCell(strconv.Itoa(i + 1)).SetTextColor(config.ColorTextAccent)
title := tview.NewTableCell(song.Title).SetMaxWidth(15).SetExpansion(2) title := tview.NewTableCell(song.Title).SetMaxWidth(15).SetExpansion(2)
artist := tview.NewTableCell(song.Artist).SetMaxWidth(15).SetExpansion(1) artist := tview.NewTableCell(song.Artist).SetMaxWidth(15).SetExpansion(1)
d := time.Second * time.Duration(song.Duration) d := time.Second * time.Duration(song.Duration)
duration := tview.NewTableCell(d.String()).SetAlign(tview.AlignRight).SetExpansion(1) duration := tview.NewTableCell(d.String()).SetAlign(tview.AlignRight).SetExpansion(1)
table.SetCell(i, 0, num) table.SetCell(i, 0, num)
table.SetCell(i, 1, title) table.SetCell(i, 1, title)
table.SetCell(i, 2, artist) table.SetCell(i, 2, artist)
@ -416,15 +463,18 @@ func (m *main) populateSongs(songs []*subsonic.Child, play *tview.Button) {
table.Blur() table.Blur()
m.view.SetBorderColor(config.ColorSelectedBoarder) m.view.SetBorderColor(config.ColorSelectedBoarder)
play.Focus(nil) play.Focus(nil)
return nil return nil
} else if event.Rune() == 'r' { } else if event.Rune() == 'r' {
song := songs[row] song := songs[row]
m.log("Generating song (%s) radio....", song.Title) m.log("Generating song (%s) radio....", song.Title)
go func() { go func() {
radioSongs, _ := m.client.GetSimilarSongs(song.ID, config.MaxRadioSongs()) radioSongs, _ := m.client.GetSimilarSongs(song.ID, config.MaxRadioSongs())
m.playAllFunc(radioSongs...) m.playAllFunc(radioSongs...)
}() }()
} }
return event return event
}) })
table.SetFocusFunc(func() { table.SetFocusFunc(func() {
@ -446,6 +496,7 @@ func (m *main) populateSearchResults() {
table := tview.NewTable() table := tview.NewTable()
table.SetBackgroundColor(config.ColorBackground) table.SetBackgroundColor(config.ColorBackground)
table.SetWrapSelection(true, false) table.SetWrapSelection(true, false)
row := 0 row := 0
lastArtist := 0 lastArtist := 0
lastAlbum := 0 lastAlbum := 0
@ -455,19 +506,23 @@ func (m *main) populateSearchResults() {
// Header // Header
header := tview.NewTableCell("Artists").SetSelectable(false) header := tview.NewTableCell("Artists").SetSelectable(false)
table.SetCell(row, 0, header) table.SetCell(row, 0, header)
row++ row++
// List // List
for i, artist := range m.searchResult.Artist { for i, artist := range m.searchResult.Artist {
index := tview.NewTableCell(fmt.Sprintf("%d", i+1)). index := tview.NewTableCell(strconv.Itoa(i + 1)).
SetTextColor(config.ColorTextAccent) SetTextColor(config.ColorTextAccent)
a := tview.NewTableCell(artist.Name).SetExpansion(2).SetMaxWidth(15) a := tview.NewTableCell(artist.Name).SetExpansion(2).SetMaxWidth(15)
acount := tview.NewTableCell(fmt.Sprintf("%d Albums", artist.AlbumCount)). count := tview.NewTableCell(fmt.Sprintf("%d Albums", artist.AlbumCount)).
SetExpansion(1).SetAlign(tview.AlignRight).SetMaxWidth(15) SetExpansion(1).SetAlign(tview.AlignRight).SetMaxWidth(15)
table.SetCell(row, 0, index) table.SetCell(row, 0, index)
table.SetCell(row, 1, a) table.SetCell(row, 1, a)
table.SetCell(row, 2, acount) table.SetCell(row, 2, count)
row++ row++
} }
lastArtist = row lastArtist = row
} }
@ -477,19 +532,23 @@ func (m *main) populateSearchResults() {
// Header // Header
header := tview.NewTableCell("Albums").SetSelectable(false) header := tview.NewTableCell("Albums").SetSelectable(false)
table.SetCell(row, 0, header) table.SetCell(row, 0, header)
row++ row++
// List // List
for i, album := range m.searchResult.Album { for i, album := range m.searchResult.Album {
index := tview.NewTableCell(fmt.Sprintf("%d", i+1)). index := tview.NewTableCell(strconv.Itoa(i + 1)).
SetTextColor(config.ColorTextAccent) SetTextColor(config.ColorTextAccent)
title := tview.NewTableCell(album.Name).SetExpansion(2).SetMaxWidth(15) title := tview.NewTableCell(album.Name).SetExpansion(2).SetMaxWidth(15)
artist := tview.NewTableCell(album.Artist). artist := tview.NewTableCell(album.Artist).
SetExpansion(1).SetAlign(tview.AlignRight).SetMaxWidth(15) SetExpansion(1).SetAlign(tview.AlignRight).SetMaxWidth(15)
table.SetCell(row, 0, index) table.SetCell(row, 0, index)
table.SetCell(row, 1, title) table.SetCell(row, 1, title)
table.SetCell(row, 2, artist) table.SetCell(row, 2, artist)
row++ row++
} }
lastAlbum = row lastAlbum = row
} }
// Songs // Songs
@ -497,17 +556,20 @@ func (m *main) populateSearchResults() {
// Header // Header
header := tview.NewTableCell("Songs").SetSelectable(false) header := tview.NewTableCell("Songs").SetSelectable(false)
table.SetCell(row, 0, header) table.SetCell(row, 0, header)
row++ row++
// List // List
for i, song := range m.searchResult.Song { for i, song := range m.searchResult.Song {
index := tview.NewTableCell(fmt.Sprintf("%d", i+1)). index := tview.NewTableCell(strconv.Itoa(i + 1)).
SetTextColor(config.ColorTextAccent) SetTextColor(config.ColorTextAccent)
title := tview.NewTableCell(song.Title).SetExpansion(2).SetMaxWidth(15) title := tview.NewTableCell(song.Title).SetExpansion(2).SetMaxWidth(15)
artist := tview.NewTableCell(song.Artist). artist := tview.NewTableCell(song.Artist).
SetExpansion(1).SetAlign(tview.AlignRight).SetMaxWidth(15) SetExpansion(1).SetAlign(tview.AlignRight).SetMaxWidth(15)
table.SetCell(row, 0, index) table.SetCell(row, 0, index)
table.SetCell(row, 1, title) table.SetCell(row, 1, title)
table.SetCell(row, 2, artist) table.SetCell(row, 2, artist)
row++ row++
} }
} }
@ -526,28 +588,36 @@ func (m *main) populateSearchResults() {
if row <= lastAlbum { if row <= lastAlbum {
return event return event
} }
if event.Rune() != 'r' { if event.Rune() != 'r' {
return event return event
} }
cell := table.GetCell(row, 0) cell := table.GetCell(row, 0)
index, err := strconv.Atoi(cell.Text) index, err := strconv.Atoi(cell.Text)
if err != nil { if err != nil {
return nil return nil
} }
song := m.searchResult.Song[index-1] song := m.searchResult.Song[index-1]
m.log("Generating song (%s) radio....", song.Title) m.log("Generating song (%s) radio....", song.Title)
go func() { go func() {
radioSongs, _ := m.client.GetSimilarSongs(song.ID, config.MaxRadioSongs()) radioSongs, _ := m.client.GetSimilarSongs(song.ID, config.MaxRadioSongs())
m.playAllFunc(radioSongs...) m.playAllFunc(radioSongs...)
}() }()
return nil return nil
}) })
m.songList.SetSelectedFunc(func(row, column int) { m.songList.SetSelectedFunc(func(row, column int) {
cell := table.GetCell(row, 0) cell := table.GetCell(row, 0)
index, err := strconv.Atoi(cell.Text) index, err := strconv.Atoi(cell.Text)
if err != nil { if err != nil {
return return
} }
if row <= lastArtist { if row <= lastArtist {
artist, _ := m.client.GetArtist(m.searchResult.Artist[index-1].ID) artist, _ := m.client.GetArtist(m.searchResult.Artist[index-1].ID)
m.SetArtist(artist) m.SetArtist(artist)
@ -563,15 +633,18 @@ func (m *main) populateSearchResults() {
} }
func (m *main) populateHeader(title, subtitle string, func (m *main) populateHeader(title, subtitle string,
duration int, coverArtID string) { duration int, coverArtID string,
) {
_ = duration
header := tview.NewFlex() header := tview.NewFlex()
header.SetBackgroundColor(config.ColorBackground) header.SetBackgroundColor(config.ColorBackground)
art := tview.NewImage() art := tview.NewImage()
art.SetBackgroundColor(config.ColorBackground) art.SetBackgroundColor(config.ColorBackground)
img, _ := m.client.GetCoverArt(coverArtID) img, _ := m.client.GetCoverArt(coverArtID)
art.SetImage(img) art.SetImage(img)
t := tview.NewTextView(). t := tview.NewTextView().
SetTextColor(config.ColorTextAccent). SetTextColor(config.ColorTextAccent).
SetTextAlign(tview.AlignCenter). SetTextAlign(tview.AlignCenter).
@ -591,10 +664,12 @@ func (m *main) populateHeader(title, subtitle string,
header.AddItem(art, 0, 1, false) header.AddItem(art, 0, 1, false)
header.AddItem(g, 0, 3, false) header.AddItem(g, 0, 3, false)
size := 6 size := 6
if m.miniView { if m.miniView {
size = 4 size = 4
} }
m.view.AddItem(header, size, 1, false) m.view.AddItem(header, size, 1, false)
// Margin bottom of 1 line // Margin bottom of 1 line

View file

@ -6,6 +6,7 @@ import (
"git.dayanhub.com/sagi/subsonic-tui/internal/client" "git.dayanhub.com/sagi/subsonic-tui/internal/client"
"git.dayanhub.com/sagi/subsonic-tui/internal/config" "git.dayanhub.com/sagi/subsonic-tui/internal/config"
"git.dayanhub.com/sagi/subsonic-tui/internal/variables"
"github.com/delucks/go-subsonic" "github.com/delucks/go-subsonic"
"github.com/rivo/tview" "github.com/rivo/tview"
) )
@ -57,7 +58,7 @@ func NewPlayer(client *client.Client) *player {
player.SetSongInfo(&subsonic.Child{ player.SetSongInfo(&subsonic.Child{
Title: "Subsonic TUI", Title: "Subsonic TUI",
Album: "MaVeZe", Album: "Made with love ❤️",
Artist: "ZeGoomba", Artist: "ZeGoomba",
CoverArt: "", CoverArt: "",
Duration: 0, Duration: 0,
@ -81,13 +82,14 @@ func (p *player) LoadAlbumArt(ID string) {
} }
func (p *player) UpdateProgress(elapsed time.Duration) { func (p *player) UpdateProgress(elapsed time.Duration) {
if p.song.Duration == 0 { if p.song.Duration == 0 {
// Startup... Show version number // Startup... Show version number
versionInfo := tview.NewTextView().SetText("Version: 0.1") versionInfo := tview.NewTextView().SetText(fmt.Sprintf("Version: %s | Commit: %s",
variables.Version, variables.Commit))
versionInfo.SetBackgroundColor(config.ColorPlaybackProgressRemaining) versionInfo.SetBackgroundColor(config.ColorPlaybackProgressRemaining)
versionInfo.SetTextColor(config.ColorPlaybackProgressElapsed) versionInfo.SetTextColor(config.ColorPlaybackProgressElapsed)
p.progress.AddItem(versionInfo, 0, 1, false) p.progress.AddItem(versionInfo, 0, 1, false)
return return
} }
@ -96,12 +98,15 @@ func (p *player) UpdateProgress(elapsed time.Duration) {
overlappedBox.SetBackgroundColor(config.ColorPlaybackProgressElapsed) overlappedBox.SetBackgroundColor(config.ColorPlaybackProgressElapsed)
overlappedBox.SetTextColor(config.ColorPlaybackProgressRemaining) overlappedBox.SetTextColor(config.ColorPlaybackProgressRemaining)
overlappedBox.SetText(songDuration.String()) overlappedBox.SetText(songDuration.String())
remainingBox := tview.NewTextView() remainingBox := tview.NewTextView()
remainingBox.SetBackgroundColor(config.ColorPlaybackProgressRemaining) remainingBox.SetBackgroundColor(config.ColorPlaybackProgressRemaining)
remainingBox.SetTextColor(config.ColorPlaybackProgressElapsed) remainingBox.SetTextColor(config.ColorPlaybackProgressElapsed)
remainingBox.SetTextAlign(tview.AlignRight) remainingBox.SetTextAlign(tview.AlignRight)
rm := time.Duration(songDuration.Seconds()-elapsed.Seconds()) * time.Second rm := time.Duration(songDuration.Seconds()-elapsed.Seconds()) * time.Second
remaining := fmt.Sprintf("-%s", rm.String()) remaining := "-" + rm.String()
remainingBox.SetText(remaining) remainingBox.SetText(remaining)
p.progress.Clear() p.progress.Clear()
p.progress.AddItem(overlappedBox, 0, int(elapsed.Seconds()), false) p.progress.AddItem(overlappedBox, 0, int(elapsed.Seconds()), false)

View file

@ -16,7 +16,6 @@ type playlists struct {
} }
func NewPlaylists(client *client.Client) *playlists { func NewPlaylists(client *client.Client) *playlists {
obj := &playlists{ obj := &playlists{
client: client, client: client,
} }
@ -55,7 +54,6 @@ func (p *playlists) SetCallback(f func(playlist *subsonic.Playlist)) {
} }
func (p *playlists) Update() { func (p *playlists) Update() {
} }
func (p *playlists) GetView() tview.Primitive { func (p *playlists) GetView() tview.Primitive {

View file

@ -1,7 +1,7 @@
package views package views
import ( import (
"fmt" "strconv"
"time" "time"
"git.dayanhub.com/sagi/subsonic-tui/internal/config" "git.dayanhub.com/sagi/subsonic-tui/internal/config"
@ -43,20 +43,24 @@ func (q *queue) drawQueue() {
list := q.view list := q.view
list.SetWrapSelection(true, false) list.SetWrapSelection(true, false)
list.SetSelectable(true, false) list.SetSelectable(true, false)
for i, song := range q.songQueue { for i, song := range q.songQueue {
isCurrentSong := q.currentSong == i isCurrentSong := q.currentSong == i
isPlayed := i < q.currentSong isPlayed := i < q.currentSong
bgColor := config.ColorBackground bgColor := config.ColorBackground
if isCurrentSong { if isCurrentSong {
bgColor = config.ColorQueuePlayingBg bgColor = config.ColorQueuePlayingBg
} else if isPlayed { } else if isPlayed {
bgColor = config.ColorQueuePlayedBg bgColor = config.ColorQueuePlayedBg
} }
num := tview.NewTableCell(fmt.Sprintf("%d", i+1)).SetTextColor(config.ColorTextAccent).SetBackgroundColor(bgColor)
num := tview.NewTableCell(strconv.Itoa(i + 1)).SetTextColor(config.ColorTextAccent).SetBackgroundColor(bgColor)
title := tview.NewTableCell(song.Title).SetMaxWidth(15).SetExpansion(2).SetBackgroundColor(bgColor) title := tview.NewTableCell(song.Title).SetMaxWidth(15).SetExpansion(2).SetBackgroundColor(bgColor)
artist := tview.NewTableCell(song.Artist).SetMaxWidth(15).SetExpansion(1).SetBackgroundColor(bgColor) artist := tview.NewTableCell(song.Artist).SetMaxWidth(15).SetExpansion(1).SetBackgroundColor(bgColor)
d := time.Second * time.Duration(song.Duration) d := time.Second * time.Duration(song.Duration)
duration := tview.NewTableCell(d.String()).SetAlign(tview.AlignRight).SetExpansion(1).SetBackgroundColor(bgColor) duration := tview.NewTableCell(d.String()).SetAlign(tview.AlignRight).SetExpansion(1).SetBackgroundColor(bgColor)
list.SetCell(i, 0, num) list.SetCell(i, 0, num)
list.SetCell(i, 1, title) list.SetCell(i, 1, title)
list.SetCell(i, 2, artist) list.SetCell(i, 2, artist)
@ -73,7 +77,6 @@ func (q *queue) Update(songs []*subsonic.Child, currentSong int) {
q.songQueue = songs q.songQueue = songs
q.currentSong = currentSong q.currentSong = currentSong
q.drawQueue() q.drawQueue()
} }
func (q *queue) GetView() tview.Primitive { func (q *queue) GetView() tview.Primitive {

View file

@ -47,6 +47,7 @@ func (s *statusLine) Mode() Statusmode {
func (s *statusLine) Search() { func (s *statusLine) Search() {
s.mode = StatusModeSearch s.mode = StatusModeSearch
s.view.Clear() s.view.Clear()
label := "Search: " label := "Search: "
_, _, w, _ := s.view.GetRect() _, _, w, _ := s.view.GetRect()
query := "" query := ""
@ -81,9 +82,13 @@ func (s *statusLine) Log(format string, a ...any) {
if s.mode != StatusModeLog { if s.mode != StatusModeLog {
return return
} }
str := fmt.Sprintf(format, a...) str := fmt.Sprintf(format, a...)
s.view.Clear() s.view.Clear()
txt := tview.NewTextView().SetDynamicColors(true) txt := tview.NewTextView().SetDynamicColors(true)
txt.SetBackgroundColor(config.ColorBackground) txt.SetBackgroundColor(config.ColorBackground)
txt.SetText(str) txt.SetText(str)
s.view.AddItem(txt, 0, 1, false) s.view.AddItem(txt, 0, 1, false)
@ -93,6 +98,7 @@ func (s *statusLine) Log(format string, a ...any) {
func (s *statusLine) GetView() tview.Primitive { func (s *statusLine) GetView() tview.Primitive {
return s.view return s.view
} }
func (s *statusLine) Update() { func (s *statusLine) Update() {
s.onUpdateFunc() s.onUpdateFunc()
} }

View file

@ -0,0 +1,6 @@
package variables
var (
Commit = "HEAD"
Version = "development"
)

View file

@ -13,18 +13,22 @@ func main() {
defer client.ArtCache.Destroy() defer client.ArtCache.Destroy()
// Create Client // Create Client
subsonicClient := client.NewClient(config.URL()) subsonicClient := client.NewClient(config.URL())
err := subsonicClient.Authenticate(config.Username(), config.Password()) err := subsonicClient.Authenticate(config.Username(), config.Password())
if err != nil { if err != nil {
// We need to show Login... // We need to show Login...
login := tui.NewLogin() login := tui.NewLogin()
err := login.Run() err := login.Run()
if err != nil { if err != nil {
panic(err) panic(err)
} }
} }
fmt.Println("Trying to login...") fmt.Println("Trying to login...")
subsonicClient = client.NewClient(config.URL()) subsonicClient = client.NewClient(config.URL())
err = subsonicClient.Authenticate(config.Username(), config.Password()) err = subsonicClient.Authenticate(config.Username(), config.Password())
if err != nil { if err != nil {
panic(err) panic(err)