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

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 99e8e299e4
Signed by: sagi
GPG key ID: FAB96BFC63B46458
23 changed files with 540 additions and 258 deletions

View file

@ -1,4 +1,4 @@
name: build bin
name: ci
on:
push:
@ -10,6 +10,43 @@ permissions:
contents: read
jobs:
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
build:
name: Make sure build does not fail
runs-on: ubuntu-latest

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:
enable:
- megacheck
- govet
- gocyclo
- gofmt
enable-all: true
disable:
- wrapcheck
- exhaustruct
- varnamelen
- gochecknoglobals
- depguard
- gochecknoinits
- forbidigo
- revive
- gosec
- megacheck
- unconvert
- gci
- goimports
- exportloopref
linters-settings:
govet:
check-shadowing: true
settings:
printf:
funcs:
- Infof
- Warnf
- Errorf
- Fatalf
- dogsled
- mnd
- funlen
- cyclop
- gocognit
- exhaustive
- maintidx
- gocritic
- nestif
- ireturn
- stylecheck

View file

@ -1,12 +1,16 @@
BINARY_NAME=subsonic-tui
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
build: dep
GOARCH=amd64 GOOS=darwin go build -o ${BUILD_FOLDER}/${BINARY_NAME}-darwin main.go
GOARCH=amd64 GOOS=linux go build -o ${BUILD_FOLDER}/${BINARY_NAME}-linux main.go
GOARCH=amd64 GOOS=windows go build -o ${BUILD_FOLDER}/${BINARY_NAME}-windows 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 ${GO_BUILD_LD_FLAGS} -o ${BUILD_FOLDER}/${BINARY_NAME}-${VERSION}-amd64-linux main.go
GOARCH=amd64 GOOS=windows go build ${GO_BUILD_LD_FLAGS} -o ${BUILD_FOLDER}/${BINARY_NAME}-${VERSION}-amd64-windows main.go
run: build
./${BINARY_NAME}
@ -30,3 +34,17 @@ vet:
lint:
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"
"image"
"image/jpeg"
"io/fs"
"os"
"path"
)
@ -25,6 +26,7 @@ func (c *artcache) saveArt(id string, img image.Image) *string {
if path != nil {
c.artPaths[id] = *path
}
return path
}
@ -39,10 +41,12 @@ func (c *artcache) saveImage(id string, img image.Image) *string {
if err != nil {
return nil
}
err = jpeg.Encode(f, img, nil)
if err != nil {
return nil
}
return &filePath
}
@ -50,6 +54,7 @@ func (c *artcache) GetPath(id string) *string {
if path, ok := c.artPaths[id]; ok {
return &path
}
return nil
}
@ -72,11 +77,12 @@ func (c *artcache) GetImage(id string) *image.Image {
if err != nil {
return nil
}
return &img
}
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() {
@ -86,10 +92,14 @@ func (c *artcache) Destroy() {
func init() {
tmpDir := os.TempDir()
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 {
panic("Failed to create cacheDir")
}
ArtCache = &artcache{
cacheDir: cacheDir,
artPaths: make(map[string]string),

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -23,13 +23,16 @@ type layout struct {
mainView *main
}
//gocyclo:ignore
func NewLayout(client *client.Client, playbackCtl *playback.Controller, refreshUI func()) *layout {
layout := &layout{}
largeView := tview.NewGrid().SetRows(0, 4, 1)
largeView.SetBackgroundColor(config.ColorBackground)
smallView := tview.NewFlex().SetDirection(tview.FlexRow)
smallView.SetBackgroundColor(config.ColorBackground)
layout.largeView = largeView
layout.smallView = smallView
pages := tview.NewPages()
@ -73,6 +76,7 @@ func NewLayout(client *client.Client, playbackCtl *playback.Controller, refreshU
artists.SetSelectArtistFunc(func(artistId string) {
artists.view.Blur()
statusLine.Log("Fetching artist's albums...")
go func() {
a, _ := client.GetArtist(artistId)
albums.SetAlbums(a.Album)
@ -83,8 +87,10 @@ func NewLayout(client *client.Client, playbackCtl *playback.Controller, refreshU
artists.SetOpenArtistFunc(func(artistId string) {
artists.view.Blur()
statusLine.Log("Fetching artists...")
layout.currentFocusedView = main.GetView()
layout.rebuildSmallView()
go func() {
a, _ := client.GetArtist(artistId)
main.SetArtist(a)
@ -96,8 +102,11 @@ func NewLayout(client *client.Client, playbackCtl *playback.Controller, refreshU
albums.SetCallback(func(albumID string) {
albums.view.Blur()
statusLine.Log("Fetching album...")
layout.currentFocusedView = main.GetView()
layout.rebuildSmallView()
go func() {
a, _ := client.GetAlbum(albumID)
main.SetAlbum(a)
@ -109,8 +118,10 @@ func NewLayout(client *client.Client, playbackCtl *playback.Controller, refreshU
playlists.SetCallback(func(p *subsonic.Playlist) {
playlists.view.Blur()
statusLine.Log("Fetching playlist...")
layout.currentFocusedView = main.GetView()
layout.rebuildSmallView()
go func() {
playlist, _ := client.GetPlaylist(p.ID)
main.SetPlaylist(playlist)
@ -140,6 +151,7 @@ func NewLayout(client *client.Client, playbackCtl *playback.Controller, refreshU
playbackCtl.SetSongEndedFunc(func(song *subsonic.Child) {
statusLine.Log("")
queue.Update(playbackCtl.GetQueue(), playbackCtl.GetQueuePosition())
if config.ScrobbleEnabled() {
// Scrobble
_ = client.Scrobble(song.ID)
@ -159,13 +171,16 @@ func NewLayout(client *client.Client, playbackCtl *playback.Controller, refreshU
if len(quary) == 0 {
layout.currentFocusedView.Focus(nil)
statusLine.Log("Search canceled")
return
}
// Search...
statusLine.Log("Searching for '%s'....", quary)
statusLine.view.Blur()
go func() {
result, _ := client.Search(quary)
layout.currentFocusedView.Blur()
main.SetSearch(result, quary)
layout.currentFocusedView = main.GetView()
@ -179,6 +194,7 @@ func NewLayout(client *client.Client, playbackCtl *playback.Controller, refreshU
if statusLine.Mode() == StatusModeSearch {
return event
}
if event.Rune() == '1' {
// Focus Artists
artists.view.Blur()
@ -189,6 +205,7 @@ func NewLayout(client *client.Client, playbackCtl *playback.Controller, refreshU
artists.view.Focus(nil)
layout.currentFocusedView = artists.GetView()
layout.rebuildSmallView()
return nil
} else if event.Rune() == '2' {
// Focus Albums
@ -200,6 +217,7 @@ func NewLayout(client *client.Client, playbackCtl *playback.Controller, refreshU
albums.view.Focus(nil)
layout.currentFocusedView = albums.GetView()
layout.rebuildSmallView()
return nil
} else if event.Rune() == '3' {
// Focus Playlists
@ -211,6 +229,7 @@ func NewLayout(client *client.Client, playbackCtl *playback.Controller, refreshU
playlists.view.Focus(nil)
layout.currentFocusedView = playlists.GetView()
layout.rebuildSmallView()
return nil
} else if event.Rune() == '`' {
// Focus Songs
@ -222,6 +241,7 @@ func NewLayout(client *client.Client, playbackCtl *playback.Controller, refreshU
main.view.Focus(nil)
layout.currentFocusedView = main.GetView()
layout.rebuildSmallView()
return nil
} else if event.Rune() == '4' {
// Focus Queue
@ -233,19 +253,22 @@ func NewLayout(client *client.Client, playbackCtl *playback.Controller, refreshU
queue.view.Focus(nil)
layout.currentFocusedView = queue.GetView()
layout.rebuildSmallView()
return nil
} else if event.Rune() == 'n' {
playbackCtl.Next()
queue.Update(playbackCtl.GetQueue(), playbackCtl.GetQueuePosition())
return nil
return nil
} else if event.Rune() == 'N' {
playbackCtl.Prev()
queue.Update(playbackCtl.GetQueue(), playbackCtl.GetQueuePosition())
return nil
} else if event.Rune() == 's' {
playbackCtl.Stop()
queue.Update(playbackCtl.GetQueue(), playbackCtl.GetQueuePosition())
return nil
} else if event.Rune() == 'p' {
if playbackCtl.State() == playback.PlaybackStateStopped {
@ -253,19 +276,25 @@ func NewLayout(client *client.Client, playbackCtl *playback.Controller, refreshU
if song != nil {
playbackCtl.Play(song)
}
queue.Update(playbackCtl.GetQueue(), playbackCtl.GetQueuePosition())
return nil
}
playbackCtl.TogglePlayPause()
return nil
} else if event.Rune() == 'c' {
playbackCtl.ClearQueue()
queue.Update(playbackCtl.GetQueue(), playbackCtl.GetQueuePosition())
return nil
} else if event.Rune() == '/' {
layout.currentFocusedView.Blur()
statusLine.Search()
refreshUI()
return nil
} else if event.Rune() == 'L' {
if layout.currentFocusedView == albums.GetView() {
@ -360,8 +389,10 @@ func NewLayout(client *client.Client, playbackCtl *playback.Controller, refreshU
artists.GetView().Focus(nil)
layout.currentFocusedView = artists.GetView()
layout.rebuildSmallView()
return layout
}
func (l *layout) rebuildSmallView() {
l.smallView.Clear()
l.smallView.AddItem(l.currentFocusedView, 0, 1, false)
@ -381,18 +412,22 @@ func (l *layout) Update() {
_, _, w, h := l.view.GetRect()
page, _ := l.view.GetFrontPage()
smallView := w < 100 || h < 30
if smallView {
if page != "small" {
l.mainView.SetMiniView(true)
go l.view.SwitchToPage("small")
return
}
} else {
if page != "large" {
l.mainView.SetMiniView(false)
go l.view.SwitchToPage("large")
return
}
}
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) {})
form.SetFocus(0)
return l
}
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.SetFocusFunc(func() {
flex.SetBorderColor(config.ColorSelectedBoarder)
if playlistAlbum.songList != nil {
// playlistAlbum.view.Blur()
playlistAlbum.songList.Focus(nil)
@ -61,6 +62,7 @@ func NewMainView(client *client.Client, log func(format string, a ...any)) *main
})
flex.SetBlurFunc(func() {
flex.SetBorderColor(config.ColorBluredBoarder)
if playlistAlbum.songList != nil {
playlistAlbum.songList.Blur()
}
@ -69,6 +71,7 @@ func NewMainView(client *client.Client, log func(format string, a ...any)) *main
playlistAlbum.view = flex
// Empty Box for starters...
playlistAlbum.view.AddItem(EmptyBox, 0, 1, false)
return playlistAlbum
}
@ -112,34 +115,43 @@ func (m *main) SetSearch(result *subsonic.SearchResult3, query string) {
}
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)
playBtn := m.drawPlaylistAlbumButtons(m.playlist.Entry)
m.populateSongs(m.playlist.Entry, playBtn)
}
func (m *main) drawAlbum() {
subtitle := fmt.Sprintf("%s\n\n%d | %s", m.album.Artist, m.album.Year, time.Duration(m.album.Duration*int(time.Second)).String())
subtitle := fmt.Sprintf("%s\n\n%d | %s", m.album.Artist, m.album.Year,
time.Duration(m.album.Duration*int(time.Second)).String())
m.populateHeader(m.album.Name, subtitle, m.album.Duration, m.album.CoverArt)
playBtn := m.drawPlaylistAlbumButtons(m.album.Song)
m.populateSongs(m.album.Song, playBtn)
playBtn := m.drawPlaylistAlbumButtons(m.album.Song)
m.populateSongs(m.album.Song, playBtn)
}
func (m *main) drawArtist() {
m.populateHeader(m.artist.Name, m.artistInfo.Biography, 0, m.artist.CoverArt)
btn := m.drawArtistButtons()
m.populateAlbums(btn)
}
func (m *main) drawSearch() {
sub := fmt.Sprintf("Query: %s", m.query)
sub := "Query: " + m.query
m.populateHeader("Search Results", sub, 0, "")
m.populateSearchResults()
}
func (m *main) Update() {
m.view.Clear()
switch m.mode {
case mainModeAlbum:
m.drawAlbum()
@ -150,12 +162,14 @@ func (m *main) Update() {
case mainModeSearch:
m.drawSearch()
}
m.songList.Focus(nil)
}
func (m *main) drawArtistButtons() *tview.Button {
// Add buttons: Radio
songs, _ := m.client.GetTopSongs(m.artist.Name, 10)
topSongs := 10
songs, _ := m.client.GetTopSongs(m.artist.Name, topSongs)
f := tview.NewFlex()
f.SetBackgroundColor(config.ColorBackground)
f.SetBorderPadding(0, 0, 2, 2)
@ -172,11 +186,13 @@ func (m *main) drawArtistButtons() *tview.Button {
radio.SetSelectedFunc(func() {
radio.Blur()
m.log("Generating %s's radio", m.artist.Name)
go func() {
radioSongs := []*subsonic.Child{}
if config.ExperimentalRadioAlgo() {
radioSongs, _ = m.client.GetExperimentalArtistRadio(m.artist, m.artistInfo, config.MaxRadioSongs())
}
if len(radioSongs) == 0 {
radioSongs, _ = m.client.GetSimilarSongs(m.artist.ID, config.MaxRadioSongs())
}
@ -191,33 +207,41 @@ func (m *main) drawArtistButtons() *tview.Button {
case tcell.KeyDown:
radio.Blur()
m.songList.Focus(nil)
return nil
case tcell.KeyRight:
radio.Blur()
top10.Focus(nil)
return nil
}
default:
return event
}
})
top10.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
switch event.Key() {
case tcell.KeyDown:
top10.Blur()
m.songList.Focus(nil)
return nil
case tcell.KeyLeft:
top10.Blur()
radio.Focus(nil)
return nil
}
default:
return event
}
})
f.AddItem(radio, 0, 1, false)
f.AddItem(EmptyBox, 0, 1, false)
if len(songs) > 0 {
f.AddItem(top10, 0, 1, false)
}
f.AddItem(EmptyBox, 0, 1, false)
// Add the buttons to the view
@ -232,8 +256,9 @@ func (m *main) populateAlbums(btn *tview.Button) {
table := tview.NewTable()
table.SetBackgroundColor(config.ColorBackground)
table.SetWrapSelection(true, false)
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)
d := time.Second * time.Duration(album.Duration)
duration := tview.NewTableCell(d.String()).SetAlign(tview.AlignRight).SetExpansion(1)
@ -262,14 +287,16 @@ func (m *main) populateAlbums(btn *tview.Button) {
table.Blur()
m.view.SetBorderColor(config.ColorSelectedBoarder)
btn.Focus(nil)
return nil
}
return event
})
m.songList = table
m.view.AddItem(table, 0, 1, false)
}
func (m *main) drawPlaylistAlbumButtons(songs []*subsonic.Child) *tview.Button {
@ -293,20 +320,25 @@ func (m *main) drawPlaylistAlbumButtons(songs []*subsonic.Child) *tview.Button {
case tcell.KeyLeft:
play.Blur()
artist.Focus(nil)
return nil
case tcell.KeyRight:
play.Blur()
shuffle.Focus(nil)
return nil
case tcell.KeyDown:
play.Blur()
m.songList.Focus(nil)
return nil
}
default:
return event
}
})
shuffle.SetSelectedFunc(func() {
shuffle.Blur()
cpy := make([]*subsonic.Child, len(songs))
copy(cpy, songs)
common.ShuffleSlice(cpy)
@ -318,17 +350,21 @@ func (m *main) drawPlaylistAlbumButtons(songs []*subsonic.Child) *tview.Button {
case tcell.KeyLeft:
shuffle.Blur()
play.Focus(nil)
return nil
case tcell.KeyRight:
shuffle.Blur()
queue.Focus(nil)
return nil
case tcell.KeyDown:
shuffle.Blur()
m.songList.Focus(nil)
return nil
}
default:
return event
}
})
queue.SetSelectedFunc(func() {
queue.Blur()
@ -340,20 +376,25 @@ func (m *main) drawPlaylistAlbumButtons(songs []*subsonic.Child) *tview.Button {
case tcell.KeyLeft:
queue.Blur()
shuffle.Focus(nil)
return nil
case tcell.KeyRight:
queue.Blur()
artist.Focus(nil)
return nil
case tcell.KeyDown:
queue.Blur()
m.songList.Focus(nil)
return nil
}
default:
return event
}
})
artist.SetSelectedFunc(func() {
artist.Blur()
ar, _ := m.client.GetArtist(m.album.ArtistID)
m.SetArtist(ar)
m.songList.Focus(nil)
@ -363,16 +404,20 @@ func (m *main) drawPlaylistAlbumButtons(songs []*subsonic.Child) *tview.Button {
case tcell.KeyLeft:
artist.Blur()
queue.Focus(nil)
return nil
case tcell.KeyRight:
artist.Blur()
play.Focus(nil)
return nil
case tcell.KeyDown:
artist.Blur()
m.songList.Focus(nil)
return nil
}
return event
})
@ -396,12 +441,14 @@ func (m *main) populateSongs(songs []*subsonic.Child, play *tview.Button) {
table := tview.NewTable()
table.SetBackgroundColor(config.ColorBackground)
table.SetWrapSelection(true, false)
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)
artist := tview.NewTableCell(song.Artist).SetMaxWidth(15).SetExpansion(1)
d := time.Second * time.Duration(song.Duration)
duration := tview.NewTableCell(d.String()).SetAlign(tview.AlignRight).SetExpansion(1)
table.SetCell(i, 0, num)
table.SetCell(i, 1, title)
table.SetCell(i, 2, artist)
@ -416,15 +463,18 @@ func (m *main) populateSongs(songs []*subsonic.Child, play *tview.Button) {
table.Blur()
m.view.SetBorderColor(config.ColorSelectedBoarder)
play.Focus(nil)
return nil
} else if event.Rune() == 'r' {
song := songs[row]
m.log("Generating song (%s) radio....", song.Title)
go func() {
radioSongs, _ := m.client.GetSimilarSongs(song.ID, config.MaxRadioSongs())
m.playAllFunc(radioSongs...)
}()
}
return event
})
table.SetFocusFunc(func() {
@ -446,6 +496,7 @@ func (m *main) populateSearchResults() {
table := tview.NewTable()
table.SetBackgroundColor(config.ColorBackground)
table.SetWrapSelection(true, false)
row := 0
lastArtist := 0
lastAlbum := 0
@ -455,19 +506,23 @@ func (m *main) populateSearchResults() {
// Header
header := tview.NewTableCell("Artists").SetSelectable(false)
table.SetCell(row, 0, header)
row++
// List
for i, artist := range m.searchResult.Artist {
index := tview.NewTableCell(fmt.Sprintf("%d", i+1)).
index := tview.NewTableCell(strconv.Itoa(i + 1)).
SetTextColor(config.ColorTextAccent)
a := tview.NewTableCell(artist.Name).SetExpansion(2).SetMaxWidth(15)
acount := tview.NewTableCell(fmt.Sprintf("%d Albums", artist.AlbumCount)).
SetExpansion(1).SetAlign(tview.AlignRight).SetMaxWidth(15)
table.SetCell(row, 0, index)
table.SetCell(row, 1, a)
table.SetCell(row, 2, acount)
row++
}
lastArtist = row
}
@ -477,19 +532,23 @@ func (m *main) populateSearchResults() {
// Header
header := tview.NewTableCell("Albums").SetSelectable(false)
table.SetCell(row, 0, header)
row++
// List
for i, album := range m.searchResult.Album {
index := tview.NewTableCell(fmt.Sprintf("%d", i+1)).
index := tview.NewTableCell(strconv.Itoa(i + 1)).
SetTextColor(config.ColorTextAccent)
title := tview.NewTableCell(album.Name).SetExpansion(2).SetMaxWidth(15)
artist := tview.NewTableCell(album.Artist).
SetExpansion(1).SetAlign(tview.AlignRight).SetMaxWidth(15)
table.SetCell(row, 0, index)
table.SetCell(row, 1, title)
table.SetCell(row, 2, artist)
row++
}
lastAlbum = row
}
// Songs
@ -497,17 +556,20 @@ func (m *main) populateSearchResults() {
// Header
header := tview.NewTableCell("Songs").SetSelectable(false)
table.SetCell(row, 0, header)
row++
// List
for i, song := range m.searchResult.Song {
index := tview.NewTableCell(fmt.Sprintf("%d", i+1)).
index := tview.NewTableCell(strconv.Itoa(i + 1)).
SetTextColor(config.ColorTextAccent)
title := tview.NewTableCell(song.Title).SetExpansion(2).SetMaxWidth(15)
artist := tview.NewTableCell(song.Artist).
SetExpansion(1).SetAlign(tview.AlignRight).SetMaxWidth(15)
table.SetCell(row, 0, index)
table.SetCell(row, 1, title)
table.SetCell(row, 2, artist)
row++
}
}
@ -526,28 +588,36 @@ func (m *main) populateSearchResults() {
if row <= lastAlbum {
return event
}
if event.Rune() != 'r' {
return event
}
cell := table.GetCell(row, 0)
index, err := strconv.Atoi(cell.Text)
if err != nil {
return nil
}
song := m.searchResult.Song[index-1]
m.log("Generating song (%s) radio....", song.Title)
go func() {
radioSongs, _ := m.client.GetSimilarSongs(song.ID, config.MaxRadioSongs())
m.playAllFunc(radioSongs...)
}()
return nil
})
m.songList.SetSelectedFunc(func(row, column int) {
cell := table.GetCell(row, 0)
index, err := strconv.Atoi(cell.Text)
if err != nil {
return
}
if row <= lastArtist {
artist, _ := m.client.GetArtist(m.searchResult.Artist[index-1].ID)
m.SetArtist(artist)
@ -563,15 +633,18 @@ func (m *main) populateSearchResults() {
}
func (m *main) populateHeader(title, subtitle string,
duration int, coverArtID string) {
duration int, coverArtID string,
) {
_ = duration
header := tview.NewFlex()
header.SetBackgroundColor(config.ColorBackground)
art := tview.NewImage()
art.SetBackgroundColor(config.ColorBackground)
img, _ := m.client.GetCoverArt(coverArtID)
art.SetImage(img)
t := tview.NewTextView().
SetTextColor(config.ColorTextAccent).
SetTextAlign(tview.AlignCenter).
@ -591,10 +664,12 @@ func (m *main) populateHeader(title, subtitle string,
header.AddItem(art, 0, 1, false)
header.AddItem(g, 0, 3, false)
size := 6
if m.miniView {
size = 4
}
m.view.AddItem(header, size, 1, false)
// Margin bottom of 1 line

View file

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

View file

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

View file

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

View file

@ -47,6 +47,7 @@ func (s *statusLine) Mode() Statusmode {
func (s *statusLine) Search() {
s.mode = StatusModeSearch
s.view.Clear()
label := "Search: "
_, _, w, _ := s.view.GetRect()
query := ""
@ -81,9 +82,13 @@ func (s *statusLine) Log(format string, a ...any) {
if s.mode != StatusModeLog {
return
}
str := fmt.Sprintf(format, a...)
s.view.Clear()
txt := tview.NewTextView().SetDynamicColors(true)
txt.SetBackgroundColor(config.ColorBackground)
txt.SetText(str)
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 {
return s.view
}
func (s *statusLine) Update() {
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()
// Create Client
subsonicClient := client.NewClient(config.URL())
err := subsonicClient.Authenticate(config.Username(), config.Password())
if err != nil {
// We need to show Login...
login := tui.NewLogin()
err := login.Run()
if err != nil {
panic(err)
}
}
fmt.Println("Trying to login...")
subsonicClient = client.NewClient(config.URL())
err = subsonicClient.Authenticate(config.Username(), config.Password())
if err != nil {
panic(err)