Added ci workflow + fixed linting issues
All checks were successful
ci / check for spelling errors (pull_request) Successful in 21s
ci / code quality (lint/tests) (pull_request) Successful in 2m29s
ci / Make sure build does not fail (pull_request) Successful in 2m25s
ci / notify-fail (pull_request) Has been skipped
ci / check for spelling errors (push) Successful in 22s
ci / code quality (lint/tests) (push) Successful in 1m53s
ci / Make sure build does not fail (push) Successful in 2m55s
ci / notify-fail (push) Has been skipped
All checks were successful
ci / check for spelling errors (pull_request) Successful in 21s
ci / code quality (lint/tests) (pull_request) Successful in 2m29s
ci / Make sure build does not fail (pull_request) Successful in 2m25s
ci / notify-fail (pull_request) Has been skipped
ci / check for spelling errors (push) Successful in 22s
ci / code quality (lint/tests) (push) Successful in 1m53s
ci / Make sure build does not fail (push) Successful in 2m55s
ci / notify-fail (push) Has been skipped
Signed-off-by: Sagi Dayan <sagidayan@gmail.com>
This commit is contained in:
parent
174b4ae0ea
commit
92e3c4ea7d
23 changed files with 567 additions and 261 deletions
|
@ -1,4 +1,4 @@
|
|||
name: build bin
|
||||
name: ci
|
||||
|
||||
on:
|
||||
push:
|
||||
|
@ -10,9 +10,52 @@ permissions:
|
|||
contents: read
|
||||
|
||||
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
|
||||
- name: install alsa devel
|
||||
run: apt update && apt install libasound2-dev -y
|
||||
- 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
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- codespell
|
||||
- codequality
|
||||
if: ${{ success() }}
|
||||
steps:
|
||||
- uses: https://code.forgejo.org/actions/checkout@v4
|
||||
name: checkout
|
||||
|
@ -27,3 +70,21 @@ jobs:
|
|||
- run: make build
|
||||
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
|
||||
|
|
|
@ -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
|
||||
- gosec
|
||||
- megacheck
|
||||
- unconvert
|
||||
- gci
|
||||
- goimports
|
||||
- exportloopref
|
||||
|
||||
linters-settings:
|
||||
govet:
|
||||
check-shadowing: true
|
||||
|
||||
settings:
|
||||
printf:
|
||||
funcs:
|
||||
- Infof
|
||||
- Warnf
|
||||
- Errorf
|
||||
- Fatalf
|
||||
enable-all: true
|
||||
disable:
|
||||
- wrapcheck
|
||||
- exhaustruct
|
||||
- varnamelen
|
||||
- gochecknoglobals
|
||||
- depguard
|
||||
- gochecknoinits
|
||||
- forbidigo
|
||||
- revive
|
||||
- gosec
|
||||
- dogsled
|
||||
- mnd
|
||||
- funlen
|
||||
- cyclop
|
||||
- gocognit
|
||||
- exhaustive
|
||||
- maintidx
|
||||
- gocritic
|
||||
- nestif
|
||||
- ireturn
|
||||
- stylecheck
|
||||
|
|
24
Makefile
24
Makefile
|
@ -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
|
||||
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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{
|
||||
//"size": "64",
|
||||
|
||||
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
|
||||
}
|
||||
|
|
|
@ -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()))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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,36 +82,46 @@ 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()
|
||||
})
|
||||
app.GetFocus().Blur()
|
||||
|
||||
//app.SetFocus(layout.GetView())
|
||||
// app.SetFocus(layout.GetView())
|
||||
|
||||
return &TUI{
|
||||
app: app,
|
||||
|
@ -116,7 +129,6 @@ func NewPlayer(client *client.Client, playbackCtl *playback.Controller) *TUI {
|
|||
client: client,
|
||||
playbackCtl: playbackCtl,
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (t *TUI) Run() error {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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,89 +276,95 @@ 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() {
|
||||
layout.currentFocusedView.Blur()
|
||||
main.view.Focus(nil)
|
||||
layout.currentFocusedView = main.GetView()
|
||||
layout.rebuildSmallView()
|
||||
} else if layout.currentFocusedView == artists.GetView() {
|
||||
layout.currentFocusedView.Blur()
|
||||
main.view.Focus(nil)
|
||||
layout.currentFocusedView = main.GetView()
|
||||
layout.rebuildSmallView()
|
||||
} else if layout.currentFocusedView == playlists.GetView() {
|
||||
layout.currentFocusedView.Blur()
|
||||
main.view.Focus(nil)
|
||||
layout.currentFocusedView = main.GetView()
|
||||
layout.rebuildSmallView()
|
||||
} else if layout.currentFocusedView == main.GetView() {
|
||||
layout.currentFocusedView.Blur()
|
||||
queue.view.Focus(nil)
|
||||
layout.currentFocusedView = queue.GetView()
|
||||
layout.rebuildSmallView()
|
||||
}
|
||||
if layout.currentFocusedView == albums.GetView() {
|
||||
layout.currentFocusedView.Blur()
|
||||
main.view.Focus(nil)
|
||||
layout.currentFocusedView = main.GetView()
|
||||
layout.rebuildSmallView()
|
||||
} else if layout.currentFocusedView == artists.GetView() {
|
||||
layout.currentFocusedView.Blur()
|
||||
main.view.Focus(nil)
|
||||
layout.currentFocusedView = main.GetView()
|
||||
layout.rebuildSmallView()
|
||||
} else if layout.currentFocusedView == playlists.GetView() {
|
||||
layout.currentFocusedView.Blur()
|
||||
main.view.Focus(nil)
|
||||
layout.currentFocusedView = main.GetView()
|
||||
layout.rebuildSmallView()
|
||||
} else if layout.currentFocusedView == main.GetView() {
|
||||
layout.currentFocusedView.Blur()
|
||||
queue.view.Focus(nil)
|
||||
layout.currentFocusedView = queue.GetView()
|
||||
layout.rebuildSmallView()
|
||||
}
|
||||
} else if event.Rune() == 'H' {
|
||||
if layout.currentFocusedView == queue.GetView() {
|
||||
layout.currentFocusedView.Blur()
|
||||
main.view.Focus(nil)
|
||||
layout.currentFocusedView = main.GetView()
|
||||
layout.rebuildSmallView()
|
||||
} else if layout.currentFocusedView == main.GetView() {
|
||||
layout.currentFocusedView.Blur()
|
||||
artists.view.Focus(nil)
|
||||
layout.currentFocusedView = artists.GetView()
|
||||
layout.rebuildSmallView()
|
||||
}
|
||||
if layout.currentFocusedView == queue.GetView() {
|
||||
layout.currentFocusedView.Blur()
|
||||
main.view.Focus(nil)
|
||||
layout.currentFocusedView = main.GetView()
|
||||
layout.rebuildSmallView()
|
||||
} else if layout.currentFocusedView == main.GetView() {
|
||||
layout.currentFocusedView.Blur()
|
||||
artists.view.Focus(nil)
|
||||
layout.currentFocusedView = artists.GetView()
|
||||
layout.rebuildSmallView()
|
||||
}
|
||||
} else if event.Rune() == 'J' {
|
||||
if layout.currentFocusedView == albums.GetView() {
|
||||
layout.currentFocusedView.Blur()
|
||||
playlists.view.Focus(nil)
|
||||
layout.currentFocusedView = playlists.GetView()
|
||||
layout.rebuildSmallView()
|
||||
} else if layout.currentFocusedView == artists.GetView() {
|
||||
layout.currentFocusedView.Blur()
|
||||
albums.view.Focus(nil)
|
||||
layout.currentFocusedView = albums.GetView()
|
||||
layout.rebuildSmallView()
|
||||
} else if layout.currentFocusedView == playlists.GetView() {
|
||||
layout.currentFocusedView.Blur()
|
||||
artists.view.Focus(nil)
|
||||
layout.currentFocusedView = artists.GetView()
|
||||
layout.rebuildSmallView()
|
||||
}
|
||||
if layout.currentFocusedView == albums.GetView() {
|
||||
layout.currentFocusedView.Blur()
|
||||
playlists.view.Focus(nil)
|
||||
layout.currentFocusedView = playlists.GetView()
|
||||
layout.rebuildSmallView()
|
||||
} else if layout.currentFocusedView == artists.GetView() {
|
||||
layout.currentFocusedView.Blur()
|
||||
albums.view.Focus(nil)
|
||||
layout.currentFocusedView = albums.GetView()
|
||||
layout.rebuildSmallView()
|
||||
} else if layout.currentFocusedView == playlists.GetView() {
|
||||
layout.currentFocusedView.Blur()
|
||||
artists.view.Focus(nil)
|
||||
layout.currentFocusedView = artists.GetView()
|
||||
layout.rebuildSmallView()
|
||||
}
|
||||
} else if event.Rune() == 'K' {
|
||||
if layout.currentFocusedView == albums.GetView() {
|
||||
layout.currentFocusedView.Blur()
|
||||
artists.view.Focus(nil)
|
||||
layout.currentFocusedView = artists.GetView()
|
||||
layout.rebuildSmallView()
|
||||
} else if layout.currentFocusedView == artists.GetView() {
|
||||
layout.currentFocusedView.Blur()
|
||||
playlists.view.Focus(nil)
|
||||
layout.currentFocusedView = playlists.GetView()
|
||||
layout.rebuildSmallView()
|
||||
} else if layout.currentFocusedView == playlists.GetView() {
|
||||
layout.currentFocusedView.Blur()
|
||||
albums.view.Focus(nil)
|
||||
layout.currentFocusedView = albums.GetView()
|
||||
layout.rebuildSmallView()
|
||||
}
|
||||
}
|
||||
if layout.currentFocusedView == albums.GetView() {
|
||||
layout.currentFocusedView.Blur()
|
||||
artists.view.Focus(nil)
|
||||
layout.currentFocusedView = artists.GetView()
|
||||
layout.rebuildSmallView()
|
||||
} else if layout.currentFocusedView == artists.GetView() {
|
||||
layout.currentFocusedView.Blur()
|
||||
playlists.view.Focus(nil)
|
||||
layout.currentFocusedView = playlists.GetView()
|
||||
layout.rebuildSmallView()
|
||||
} else if layout.currentFocusedView == playlists.GetView() {
|
||||
layout.currentFocusedView.Blur()
|
||||
albums.view.Focus(nil)
|
||||
layout.currentFocusedView = albums.GetView()
|
||||
layout.rebuildSmallView()
|
||||
}
|
||||
}
|
||||
|
||||
return event
|
||||
})
|
||||
|
@ -356,12 +385,14 @@ func NewLayout(client *client.Client, playbackCtl *playback.Controller, refreshU
|
|||
|
||||
layout.view = pages
|
||||
layout.player = player
|
||||
//Auto focus on artists
|
||||
// Auto focus on artists
|
||||
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()
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
||||
}
|
||||
|
|
|
@ -54,13 +54,15 @@ 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.view.Blur()
|
||||
playlistAlbum.songList.Focus(nil)
|
||||
}
|
||||
})
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
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
|
||||
// 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)).
|
||||
count := 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)
|
||||
table.SetCell(row, 2, count)
|
||||
|
||||
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
|
||||
// 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
|
||||
// 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
|
||||
|
|
|
@ -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"
|
||||
)
|
||||
|
@ -26,7 +27,7 @@ func NewPlayer(client *client.Client) *player {
|
|||
grid := tview.NewGrid().SetColumns(9, 0)
|
||||
grid.SetBackgroundColor(config.ColorBackground)
|
||||
|
||||
//album art
|
||||
// album art
|
||||
art := tview.NewImage()
|
||||
art.SetBackgroundColor(config.ColorBackground)
|
||||
|
||||
|
@ -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)
|
||||
|
@ -113,5 +118,5 @@ func (p *player) GetView() tview.Primitive {
|
|||
}
|
||||
|
||||
func (p *player) Update() {
|
||||
//p.UpdateProgress("00:00", 50)
|
||||
// p.UpdateProgress("00:00", 50)
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
6
internal/variables/variables.go
Normal file
6
internal/variables/variables.go
Normal file
|
@ -0,0 +1,6 @@
|
|||
package variables
|
||||
|
||||
var (
|
||||
Commit = "HEAD"
|
||||
Version = "development"
|
||||
)
|
6
main.go
6
main.go
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue