Added ci workflow + fixed linting issues #10
23 changed files with 567 additions and 261 deletions
|
@ -1,4 +1,4 @@
|
||||||
name: build bin
|
name: ci
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
|
@ -10,9 +10,52 @@ permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
codespell:
|
||||||
|
name: check for spelling errors
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Annotate locations with typos
|
||||||
|
uses: "https://github.com/codespell-project/codespell-problem-matcher@v1"
|
||||||
|
- name: Codespell
|
||||||
|
uses: "https://github.com/codespell-project/actions-codespell@v2"
|
||||||
|
with:
|
||||||
|
check_filenames: true
|
||||||
|
check_hidden: true
|
||||||
|
skip: ./.git
|
||||||
|
|
||||||
|
codequality:
|
||||||
|
name: code quality (lint/tests)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs:
|
||||||
|
- codespell
|
||||||
|
if: ${{ success() }}
|
||||||
|
steps:
|
||||||
|
- name: checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- 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
|
name: Make sure build does not fail
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
needs:
|
||||||
|
- codespell
|
||||||
|
- codequality
|
||||||
|
if: ${{ success() }}
|
||||||
steps:
|
steps:
|
||||||
- uses: https://code.forgejo.org/actions/checkout@v4
|
- uses: https://code.forgejo.org/actions/checkout@v4
|
||||||
name: checkout
|
name: checkout
|
||||||
|
@ -27,3 +70,21 @@ jobs:
|
||||||
- run: make build
|
- run: make build
|
||||||
name: install dependencies and build bin files
|
name: install dependencies and build bin files
|
||||||
|
|
||||||
|
notify-fail:
|
||||||
|
name: notify-fail
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs:
|
||||||
|
- codespell
|
||||||
|
- codequality
|
||||||
|
- compile
|
||||||
|
if: ${{ failure() }}
|
||||||
|
env:
|
||||||
|
SERVER_URL: ${{ secrets.GOTIFY_SERVER_URL }}
|
||||||
|
TOKEN: ${{ secrets.GOTIFY_TOKEN }}
|
||||||
|
steps:
|
||||||
|
- name: gotify
|
||||||
|
run: |-
|
||||||
|
curl "${SERVER_URL}?token=${TOKEN}" \
|
||||||
|
-F "title='CI failed'" \
|
||||||
|
-F "message='Something failed at ${GITHUB_REPOSITORY}/${GITHUB_REF} for user ${GITHUB_ACTOR}'" \
|
||||||
|
-F "priority=7" &> /dev/null
|
||||||
|
|
|
@ -1,76 +1,23 @@
|
||||||
# This file contains all available configuration options
|
|
||||||
# with their default values.
|
|
||||||
|
|
||||||
# options for analysis running
|
|
||||||
run:
|
|
||||||
# default concurrency is a available CPU number
|
|
||||||
concurrency: 4
|
|
||||||
|
|
||||||
# timeout for analysis, e.g. 30s, 5m, default is 1m
|
|
||||||
timeout: 10m
|
|
||||||
|
|
||||||
# exit code when at least one issue was found, default is 1
|
|
||||||
issues-exit-code: 1
|
|
||||||
|
|
||||||
# include test files or not, default is true
|
|
||||||
tests: true
|
|
||||||
|
|
||||||
# which dirs to skip: issues from them won't be reported;
|
|
||||||
# can use regexp here: generated.*, regexp is applied on full path;
|
|
||||||
# default value is empty list, but default dirs are skipped independently
|
|
||||||
# from this option's value (see skip-dirs-use-default).
|
|
||||||
skip-dirs:
|
|
||||||
- bin
|
|
||||||
- deploy
|
|
||||||
- docs
|
|
||||||
- examples
|
|
||||||
- hack
|
|
||||||
- packaging
|
|
||||||
- reports
|
|
||||||
|
|
||||||
# output configuration options
|
|
||||||
output:
|
|
||||||
# colored-line-number|line-number|json|tab|checkstyle|code-climate, default is "colored-line-number"
|
|
||||||
format: colored-line-number
|
|
||||||
|
|
||||||
# print lines of code with issue, default is true
|
|
||||||
print-issued-lines: true
|
|
||||||
|
|
||||||
# print linter name in the end of issue text, default is true
|
|
||||||
print-linter-name: true
|
|
||||||
|
|
||||||
# make issues output unique by line, default is true
|
|
||||||
uniq-by-line: true
|
|
||||||
|
|
||||||
issues:
|
|
||||||
# List of regexps of issue texts to exclude, empty list by default.
|
|
||||||
# But independently from this option we use default exclude patterns,
|
|
||||||
# it can be disabled by `exclude-use-default: false`. To list all
|
|
||||||
# excluded by default patterns execute `golangci-lint run --help`
|
|
||||||
exclude:
|
|
||||||
- 'declaration of "err" shadows declaration at'
|
|
||||||
|
|
||||||
linters:
|
linters:
|
||||||
enable:
|
enable-all: true
|
||||||
- megacheck
|
disable:
|
||||||
- govet
|
- wrapcheck
|
||||||
- gocyclo
|
- exhaustruct
|
||||||
- gofmt
|
- varnamelen
|
||||||
|
- gochecknoglobals
|
||||||
|
- depguard
|
||||||
|
- gochecknoinits
|
||||||
|
- forbidigo
|
||||||
|
- revive
|
||||||
- gosec
|
- gosec
|
||||||
- megacheck
|
- dogsled
|
||||||
- unconvert
|
- mnd
|
||||||
- gci
|
- funlen
|
||||||
- goimports
|
- cyclop
|
||||||
- exportloopref
|
- gocognit
|
||||||
|
- exhaustive
|
||||||
linters-settings:
|
- maintidx
|
||||||
govet:
|
- gocritic
|
||||||
check-shadowing: true
|
- nestif
|
||||||
|
- ireturn
|
||||||
settings:
|
- stylecheck
|
||||||
printf:
|
|
||||||
funcs:
|
|
||||||
- Infof
|
|
||||||
- Warnf
|
|
||||||
- Errorf
|
|
||||||
- Fatalf
|
|
||||||
|
|
24
Makefile
24
Makefile
|
@ -1,12 +1,16 @@
|
||||||
BINARY_NAME=subsonic-tui
|
BINARY_NAME=subsonic-tui
|
||||||
BUILD_FOLDER=build
|
BUILD_FOLDER=build
|
||||||
|
VERSION=0.0.1
|
||||||
|
|
||||||
|
GO_BUILD_LD_FLAGS=-ldflags="-s -w -X 'git.dayanhub.com/sagi/subsonic-tui/internal/variables.Commit=$(shell git rev-parse --short HEAD)' \
|
||||||
|
-X 'git.dayanhub.com/sagi/subsonic-tui/internal/variables.Version=${VERSION}'"
|
||||||
|
|
||||||
|
|
||||||
.PHONY: build
|
.PHONY: build
|
||||||
build: dep
|
build: dep
|
||||||
GOARCH=amd64 GOOS=darwin go build -o ${BUILD_FOLDER}/${BINARY_NAME}-darwin main.go
|
GOARCH=amd64 GOOS=darwin go build ${GO_BUILD_LD_FLAGS} -o ${BUILD_FOLDER}/${BINARY_NAME}-${VERSION}-amd64-darwin main.go
|
||||||
GOARCH=amd64 GOOS=linux go build -o ${BUILD_FOLDER}/${BINARY_NAME}-linux main.go
|
GOARCH=amd64 GOOS=linux go build ${GO_BUILD_LD_FLAGS} -o ${BUILD_FOLDER}/${BINARY_NAME}-${VERSION}-amd64-linux main.go
|
||||||
GOARCH=amd64 GOOS=windows go build -o ${BUILD_FOLDER}/${BINARY_NAME}-windows main.go
|
GOARCH=amd64 GOOS=windows go build ${GO_BUILD_LD_FLAGS} -o ${BUILD_FOLDER}/${BINARY_NAME}-${VERSION}-amd64-windows main.go
|
||||||
|
|
||||||
run: build
|
run: build
|
||||||
./${BINARY_NAME}
|
./${BINARY_NAME}
|
||||||
|
@ -30,3 +34,17 @@ vet:
|
||||||
|
|
||||||
lint:
|
lint:
|
||||||
golangci-lint run --enable-all
|
golangci-lint run --enable-all
|
||||||
|
|
||||||
|
.PHONY: install
|
||||||
|
install: build
|
||||||
|
ifeq ($(OS),Windows_NT) # is Windows_NT on XP, 2000, 7, Vista, 10...
|
||||||
|
$(error Unable to install on windows)
|
||||||
|
else
|
||||||
|
ifeq ($(shell uname), Linux)
|
||||||
|
sudo cp ${BUILD_FOLDER}/${BINARY_NAME}-${VERSION}-amd64-linux /usr/local/bin/${BINARY_NAME}
|
||||||
|
else
|
||||||
|
@echo $(shell uname)
|
||||||
|
$(error Unable to install on systems that are not linux)
|
||||||
|
endif
|
||||||
|
endif
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"image"
|
"image"
|
||||||
"image/jpeg"
|
"image/jpeg"
|
||||||
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
)
|
)
|
||||||
|
@ -25,6 +26,7 @@ func (c *artcache) saveArt(id string, img image.Image) *string {
|
||||||
if path != nil {
|
if path != nil {
|
||||||
c.artPaths[id] = *path
|
c.artPaths[id] = *path
|
||||||
}
|
}
|
||||||
|
|
||||||
return path
|
return path
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,10 +41,12 @@ func (c *artcache) saveImage(id string, img image.Image) *string {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
err = jpeg.Encode(f, img, nil)
|
err = jpeg.Encode(f, img, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return &filePath
|
return &filePath
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,6 +54,7 @@ func (c *artcache) GetPath(id string) *string {
|
||||||
if path, ok := c.artPaths[id]; ok {
|
if path, ok := c.artPaths[id]; ok {
|
||||||
return &path
|
return &path
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -72,11 +77,12 @@ func (c *artcache) GetImage(id string) *image.Image {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return &img
|
return &img
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *artcache) filepath(id string) string {
|
func (c *artcache) filepath(id string) string {
|
||||||
return path.Join(c.cacheDir, fmt.Sprintf("%s.jpg", id))
|
return path.Join(c.cacheDir, id+".jpg")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *artcache) Destroy() {
|
func (c *artcache) Destroy() {
|
||||||
|
@ -86,10 +92,14 @@ func (c *artcache) Destroy() {
|
||||||
func init() {
|
func init() {
|
||||||
tmpDir := os.TempDir()
|
tmpDir := os.TempDir()
|
||||||
cacheDir := path.Join(tmpDir, fmt.Sprintf("subsonic-tui-%d", os.Getpid()))
|
cacheDir := path.Join(tmpDir, fmt.Sprintf("subsonic-tui-%d", os.Getpid()))
|
||||||
err := os.Mkdir(cacheDir, 0777)
|
|
||||||
|
var permissions fs.FileMode = 0o777
|
||||||
|
|
||||||
|
err := os.Mkdir(cacheDir, permissions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic("Failed to create cacheDir")
|
panic("Failed to create cacheDir")
|
||||||
}
|
}
|
||||||
|
|
||||||
ArtCache = &artcache{
|
ArtCache = &artcache{
|
||||||
cacheDir: cacheDir,
|
cacheDir: cacheDir,
|
||||||
artPaths: make(map[string]string),
|
artPaths: make(map[string]string),
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
package client
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"image"
|
"image"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"git.dayanhub.com/sagi/subsonic-tui/internal/common"
|
"git.dayanhub.com/sagi/subsonic-tui/internal/common"
|
||||||
|
@ -16,7 +16,7 @@ type Client struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewClient(baseURL string) *Client {
|
func NewClient(baseURL string) *Client {
|
||||||
var client subsonic.Client = subsonic.Client{
|
client := subsonic.Client{
|
||||||
Client: &http.Client{},
|
Client: &http.Client{},
|
||||||
ClientName: "subsonic-tui",
|
ClientName: "subsonic-tui",
|
||||||
BaseUrl: baseURL,
|
BaseUrl: baseURL,
|
||||||
|
@ -30,6 +30,7 @@ func NewClient(baseURL string) *Client {
|
||||||
|
|
||||||
func (c *Client) Authenticate(username, password string) error {
|
func (c *Client) Authenticate(username, password string) error {
|
||||||
c.client.User = username
|
c.client.User = username
|
||||||
|
|
||||||
return c.client.Authenticate(password)
|
return c.client.Authenticate(password)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,8 +42,8 @@ func (c *Client) GetPlaylists() ([]*subsonic.Playlist, error) {
|
||||||
return c.client.GetPlaylists(map[string]string{})
|
return c.client.GetPlaylists(map[string]string{})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) GetPlaylist(ID string) (*subsonic.Playlist, error) {
|
func (c *Client) GetPlaylist(id string) (*subsonic.Playlist, error) {
|
||||||
return c.client.GetPlaylist(ID)
|
return c.client.GetPlaylist(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) GetArtists() ([]*subsonic.ArtistID3, error) {
|
func (c *Client) GetArtists() ([]*subsonic.ArtistID3, error) {
|
||||||
|
@ -50,7 +51,9 @@ func (c *Client) GetArtists() ([]*subsonic.ArtistID3, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
artists := []*subsonic.ArtistID3{}
|
artists := []*subsonic.ArtistID3{}
|
||||||
|
|
||||||
for _, i := range indexes.Index {
|
for _, i := range indexes.Index {
|
||||||
artists = append(artists, i.Artist...)
|
artists = append(artists, i.Artist...)
|
||||||
}
|
}
|
||||||
|
@ -64,53 +67,58 @@ func (c *Client) GetAlbums() ([]*subsonic.AlbumID3, error) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) GetArtist(ID string) (*subsonic.ArtistID3, error) {
|
func (c *Client) GetArtist(id string) (*subsonic.ArtistID3, error) {
|
||||||
return c.client.GetArtist(ID)
|
return c.client.GetArtist(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) GetArtistInfo(ID string) (*subsonic.ArtistInfo2, error) {
|
func (c *Client) GetArtistInfo(id string) (*subsonic.ArtistInfo2, error) {
|
||||||
return c.client.GetArtistInfo2(ID, map[string]string{
|
return c.client.GetArtistInfo2(id, map[string]string{
|
||||||
"count": "20",
|
"count": "20",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) GetAlbum(ID string) (*subsonic.AlbumID3, error) {
|
func (c *Client) GetAlbum(id string) (*subsonic.AlbumID3, error) {
|
||||||
return c.client.GetAlbum(ID)
|
return c.client.GetAlbum(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) GetCoverArt(ID string) (image.Image, error) {
|
func (c *Client) GetCoverArt(id string) (image.Image, error) {
|
||||||
if img := ArtCache.GetImage(ID); img != nil {
|
if img := ArtCache.GetImage(id); img != nil {
|
||||||
return *img, nil
|
return *img, nil
|
||||||
}
|
}
|
||||||
img, err := c.client.GetCoverArt(ID, map[string]string{
|
|
||||||
//"size": "64",
|
img, err := c.client.GetCoverArt(id, map[string]string{
|
||||||
|
// "size": "64",
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
ArtCache.saveArt(ID, img)
|
|
||||||
|
ArtCache.saveArt(id, img)
|
||||||
|
|
||||||
return img, err
|
return img, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) GetSimilarSongs(artistID string, maxSongs int) ([]*subsonic.Child, error) {
|
func (c *Client) GetSimilarSongs(artistID string, maxSongs int) ([]*subsonic.Child, error) {
|
||||||
max := fmt.Sprintf("%d", maxSongs)
|
count := strconv.Itoa(maxSongs)
|
||||||
|
|
||||||
return c.client.GetSimilarSongs2(artistID, map[string]string{
|
return c.client.GetSimilarSongs2(artistID, map[string]string{
|
||||||
"count": max,
|
"count": count,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) Stream(ID string) (io.Reader, error) {
|
func (c *Client) Stream(id string) (io.Reader, error) {
|
||||||
return c.client.Stream(ID, map[string]string{
|
return c.client.Stream(id, map[string]string{
|
||||||
"format": "mp3",
|
"format": "mp3",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) Scrobble(ID string) error {
|
func (c *Client) Scrobble(id string) error {
|
||||||
return c.client.Scrobble(ID, map[string]string{})
|
return c.client.Scrobble(id, map[string]string{})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) GetTopSongs(name string, max int) ([]*subsonic.Child, error) {
|
func (c *Client) GetTopSongs(name string, maxSongs int) ([]*subsonic.Child, error) {
|
||||||
count := fmt.Sprintf("%d", max)
|
count := strconv.Itoa(maxSongs)
|
||||||
|
|
||||||
return c.client.GetTopSongs(name, map[string]string{
|
return c.client.GetTopSongs(name, map[string]string{
|
||||||
"count": count,
|
"count": count,
|
||||||
})
|
})
|
||||||
|
@ -124,40 +132,59 @@ func (c *Client) Search(query string) (*subsonic.SearchResult3, error) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) GetExperimentalArtistRadio(artistId3 *subsonic.ArtistID3, info *subsonic.ArtistInfo2, max int) ([]*subsonic.Child, error) {
|
func (c *Client) GetExperimentalArtistRadio(artistID3 *subsonic.ArtistID3,
|
||||||
|
info *subsonic.ArtistInfo2, maxSongs int,
|
||||||
|
) ([]*subsonic.Child, error) {
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
ID := artistId3.ID
|
|
||||||
|
ID := artistID3.ID
|
||||||
similarArtists := info.SimilarArtist
|
similarArtists := info.SimilarArtist
|
||||||
songs := []*subsonic.Child{}
|
songs := []*subsonic.Child{}
|
||||||
similarArtistsSongs := 10
|
similarArtistsSongs := 10
|
||||||
thisArtistFactor := 3
|
thisArtistFactor := 3
|
||||||
portion := len(info.SimilarArtist) * similarArtistsSongs * thisArtistFactor
|
portion := len(info.SimilarArtist) * similarArtistsSongs * thisArtistFactor
|
||||||
wg.Add(2)
|
|
||||||
|
wg.Add(1)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
s, _ := c.GetSimilarSongs(ID, portion)
|
s, _ := c.GetSimilarSongs(ID, portion)
|
||||||
songs = append(songs, s...)
|
songs = append(songs, s...)
|
||||||
|
|
||||||
wg.Done()
|
wg.Done()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
wg.Add(1)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
s, _ := c.GetTopSongs(artistId3.Name, similarArtistsSongs)
|
s, _ := c.GetTopSongs(artistID3.Name, similarArtistsSongs)
|
||||||
songs = append(songs, s...)
|
songs = append(songs, s...)
|
||||||
|
|
||||||
wg.Done()
|
wg.Done()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
common.ShuffleSlice(similarArtists)
|
common.ShuffleSlice(similarArtists)
|
||||||
|
|
||||||
for _, a := range similarArtists {
|
for _, a := range similarArtists {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
|
|
||||||
artist := a
|
artist := a
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
s, _ := c.GetSimilarSongs(artist.ID, similarArtistsSongs)
|
s, _ := c.GetSimilarSongs(artist.ID, similarArtistsSongs)
|
||||||
songs = append(songs, s...)
|
songs = append(songs, s...)
|
||||||
|
|
||||||
wg.Done()
|
wg.Done()
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
if max > len(songs) {
|
|
||||||
max = len(songs)
|
if maxSongs > len(songs) {
|
||||||
|
maxSongs = len(songs)
|
||||||
}
|
}
|
||||||
songs = songs[:max]
|
|
||||||
|
songs = songs[:maxSongs]
|
||||||
common.ShuffleSlice(songs)
|
common.ShuffleSlice(songs)
|
||||||
|
|
||||||
return songs, nil
|
return songs, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,11 +11,13 @@ func ShuffleSlice(slice interface{}) {
|
||||||
swap := reflect.Swapper(slice)
|
swap := reflect.Swapper(slice)
|
||||||
|
|
||||||
length := rv.Len()
|
length := rv.Len()
|
||||||
|
|
||||||
for i := length - 1; i > 0; i-- {
|
for i := length - 1; i > 0; i-- {
|
||||||
j, err := rand.Int(rand.Reader, big.NewInt(int64(i+1)))
|
j, err := rand.Int(rand.Reader, big.NewInt(int64(i+1)))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic("Shuffle error")
|
panic("Shuffle error")
|
||||||
}
|
}
|
||||||
|
|
||||||
swap(i, int(j.Int64()))
|
swap(i, int(j.Int64()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ package config
|
||||||
import (
|
import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -17,9 +18,9 @@ type _config struct {
|
||||||
Username string `yaml:"username"`
|
Username string `yaml:"username"`
|
||||||
Password string `yaml:"password"`
|
Password string `yaml:"password"`
|
||||||
URL string `yaml:"url"`
|
URL string `yaml:"url"`
|
||||||
EnableScrobble bool `yaml:"enable_scrobble" default:"false"`
|
EnableScrobble bool `default:"false" yaml:"enableScrobble"`
|
||||||
MaxRadioSongs int `yaml:"max_radio_songs" default:"50"`
|
MaxRadioSongs int `default:"50" yaml:"maxRadioSongs"`
|
||||||
ExperimentalRadioAlgo bool `yaml:"experimental_radio_algo" default:"false"`
|
ExperimentalRadioAlgo bool `default:"false" yaml:"experimentalRadioAlgo"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var configStruct *_config
|
var configStruct *_config
|
||||||
|
@ -33,13 +34,18 @@ func init() {
|
||||||
fmt.Printf("[ERROR] Failed to fetch user config directory. %e\n", err)
|
fmt.Printf("[ERROR] Failed to fetch user config directory. %e\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := os.Stat(configDir); os.IsNotExist(err) {
|
if _, err := os.Stat(configDir); os.IsNotExist(err) {
|
||||||
err := os.MkdirAll(configDir, 0700)
|
var permissions fs.FileMode = 0o700
|
||||||
|
|
||||||
|
err := os.MkdirAll(configDir, permissions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var configFile *os.File
|
var configFile *os.File
|
||||||
|
|
||||||
if _, err := os.Stat(configPath); os.IsNotExist(err) {
|
if _, err := os.Stat(configPath); os.IsNotExist(err) {
|
||||||
configFile, err = os.Create(configPath)
|
configFile, err = os.Create(configPath)
|
||||||
defer func() {
|
defer func() {
|
||||||
|
@ -48,35 +54,44 @@ func init() {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("[ERROR] Failed to create config file @ %s. %e\n", configPath, err)
|
fmt.Printf("[ERROR] Failed to create config file @ %s. %e\n", configPath, err)
|
||||||
os.Exit(1)
|
panic("unable to create config file")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
configStruct, err = loadConfig()
|
configStruct, err = loadConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("[ERROR] Failed to load config file @ %s. %e\n", configPath, err)
|
fmt.Printf("[ERROR] Failed to load config file @ %s. %e\n", configPath, err)
|
||||||
os.Exit(1)
|
panic("unable to load config file")
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("Init Config %s\n", configPath)
|
fmt.Printf("Init Config %s\n", configPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
func URL() string {
|
func URL() string {
|
||||||
return configStruct.URL
|
return configStruct.URL
|
||||||
}
|
}
|
||||||
|
|
||||||
func Username() string {
|
func Username() string {
|
||||||
return configStruct.Username
|
return configStruct.Username
|
||||||
}
|
}
|
||||||
|
|
||||||
func Password() string {
|
func Password() string {
|
||||||
p, _ := base64.StdEncoding.DecodeString(configStruct.Password)
|
p, _ := base64.StdEncoding.DecodeString(configStruct.Password)
|
||||||
|
|
||||||
return strings.TrimSpace(string(p))
|
return strings.TrimSpace(string(p))
|
||||||
}
|
}
|
||||||
|
|
||||||
func ScrobbleEnabled() bool {
|
func ScrobbleEnabled() bool {
|
||||||
return configStruct.EnableScrobble
|
return configStruct.EnableScrobble
|
||||||
}
|
}
|
||||||
|
|
||||||
func MaxRadioSongs() int {
|
func MaxRadioSongs() int {
|
||||||
return configStruct.MaxRadioSongs
|
return configStruct.MaxRadioSongs
|
||||||
}
|
}
|
||||||
|
|
||||||
func ExperimentalRadioAlgo() bool {
|
func ExperimentalRadioAlgo() bool {
|
||||||
return configStruct.ExperimentalRadioAlgo
|
return configStruct.ExperimentalRadioAlgo
|
||||||
}
|
}
|
||||||
|
@ -95,17 +110,21 @@ func SetURL(u string) {
|
||||||
|
|
||||||
func loadConfig() (*_config, error) {
|
func loadConfig() (*_config, error) {
|
||||||
c := &_config{}
|
c := &_config{}
|
||||||
|
|
||||||
err := defaults.Set(c)
|
err := defaults.Set(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
file, err := os.ReadFile(configPath)
|
file, err := os.ReadFile(configPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = yaml.Unmarshal(file, c); err != nil {
|
if err = yaml.Unmarshal(file, c); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return c, nil
|
return c, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -115,7 +134,10 @@ func SaveConfig() {
|
||||||
fmt.Printf("[ERROR] Failed to convert config to yaml. %e\n", err)
|
fmt.Printf("[ERROR] Failed to convert config to yaml. %e\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
err = os.WriteFile(configPath, yml, 0600)
|
|
||||||
|
var permissions fs.FileMode = 0o600
|
||||||
|
|
||||||
|
err = os.WriteFile(configPath, yml, permissions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("[ERROR] Failed to save config file @ %s. %e\n", configPath, err)
|
fmt.Printf("[ERROR] Failed to save config file @ %s. %e\n", configPath, err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
|
|
@ -46,7 +46,9 @@ func NewController(client *client.Client) *Controller {
|
||||||
}
|
}
|
||||||
controller.desktopPlayback = desktopPlayer(controller)
|
controller.desktopPlayback = desktopPlayer(controller)
|
||||||
controller.desktopPlayback.Start()
|
controller.desktopPlayback.Start()
|
||||||
|
|
||||||
go controller.playbackTicker()
|
go controller.playbackTicker()
|
||||||
|
|
||||||
return controller
|
return controller
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -58,14 +60,17 @@ func (c *Controller) Play(song *subsonic.Child) {
|
||||||
if song == nil {
|
if song == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
r, err := c.client.Stream(song.ID)
|
r, err := c.client.Stream(song.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
//TODO: Log error
|
|
||||||
c.Stop()
|
c.Stop()
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Stream(r, song)
|
c.Stream(r, song)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Controller) Next() {
|
func (c *Controller) Next() {
|
||||||
song := c.queue.Next()
|
song := c.queue.Next()
|
||||||
if song != nil {
|
if song != nil {
|
||||||
|
@ -78,6 +83,7 @@ func (c *Controller) AddToQueue(songs []*subsonic.Child) {
|
||||||
if shouldPlay {
|
if shouldPlay {
|
||||||
c.Play(c.queue.GetCurrentSong())
|
c.Play(c.queue.GetCurrentSong())
|
||||||
}
|
}
|
||||||
|
|
||||||
c.desktopPlayback.OnPlaylistChanged()
|
c.desktopPlayback.OnPlaylistChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -108,6 +114,7 @@ func (c *Controller) Prev() {
|
||||||
c.Play(song)
|
c.Play(song)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Controller) GetQueue() []*subsonic.Child {
|
func (c *Controller) GetQueue() []*subsonic.Child {
|
||||||
return c.queue.Get()
|
return c.queue.Get()
|
||||||
}
|
}
|
||||||
|
@ -131,6 +138,7 @@ func (c *Controller) SetSongEndedFunc(f func(song *subsonic.Child)) {
|
||||||
func (c *Controller) Close() error {
|
func (c *Controller) Close() error {
|
||||||
c.Stop()
|
c.Stop()
|
||||||
c.closeChan <- true
|
c.closeChan <- true
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -143,6 +151,7 @@ func (c *Controller) TogglePlayPause() {
|
||||||
c.playbackState = PlaybackStatePlaying
|
c.playbackState = PlaybackStatePlaying
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
c.desktopPlayback.OnPlayPause()
|
c.desktopPlayback.OnPlayPause()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -150,12 +159,16 @@ func (c *Controller) Stop() {
|
||||||
if c.ctrl == nil {
|
if c.ctrl == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
speaker.Clear()
|
speaker.Clear()
|
||||||
|
|
||||||
c.ctrl.Paused = true
|
c.ctrl.Paused = true
|
||||||
c.playbackState = PlaybackStateStopped
|
c.playbackState = PlaybackStateStopped
|
||||||
c.ctrl = nil
|
c.ctrl = nil
|
||||||
c.stream = nil
|
c.stream = nil
|
||||||
|
|
||||||
c.songElapsedFunc(c.song, time.Duration(0))
|
c.songElapsedFunc(c.song, time.Duration(0))
|
||||||
|
|
||||||
c.song = nil
|
c.song = nil
|
||||||
c.position = 0
|
c.position = 0
|
||||||
c.desktopPlayback.OnPlayPause()
|
c.desktopPlayback.OnPlayPause()
|
||||||
|
@ -193,6 +206,7 @@ func (c *Controller) Stream(reader io.Reader, song *subsonic.Child) {
|
||||||
readerCloser := io.NopCloser(reader)
|
readerCloser := io.NopCloser(reader)
|
||||||
decodedMp3, format, err := mp3.Decode(readerCloser)
|
decodedMp3, format, err := mp3.Decode(readerCloser)
|
||||||
decodedMp3.Position()
|
decodedMp3.Position()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic("mp3.NewDecoder failed: " + err.Error())
|
panic("mp3.NewDecoder failed: " + err.Error())
|
||||||
}
|
}
|
||||||
|
@ -214,6 +228,7 @@ func (c *Controller) Stream(reader io.Reader, song *subsonic.Child) {
|
||||||
ctrl := &beep.Ctrl{Streamer: stream}
|
ctrl := &beep.Ctrl{Streamer: stream}
|
||||||
c.ctrl = ctrl
|
c.ctrl = ctrl
|
||||||
c.playbackState = PlaybackStatePlaying
|
c.playbackState = PlaybackStatePlaying
|
||||||
|
|
||||||
speaker.Play(beep.Seq(ctrl, beep.Callback(func() {
|
speaker.Play(beep.Seq(ctrl, beep.Callback(func() {
|
||||||
c.songEndedFunc(song)
|
c.songEndedFunc(song)
|
||||||
c.songEndedChan <- true
|
c.songEndedChan <- true
|
||||||
|
|
|
@ -9,11 +9,11 @@ import (
|
||||||
"github.com/godbus/dbus/v5"
|
"github.com/godbus/dbus/v5"
|
||||||
"github.com/quarckster/go-mpris-server/pkg/events"
|
"github.com/quarckster/go-mpris-server/pkg/events"
|
||||||
"github.com/quarckster/go-mpris-server/pkg/server"
|
"github.com/quarckster/go-mpris-server/pkg/server"
|
||||||
. "github.com/quarckster/go-mpris-server/pkg/types"
|
mprisTypes "github.com/quarckster/go-mpris-server/pkg/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
mprisPlayerNmae = "MehSonic"
|
mprisPlayerNmae = "SubsonicTUI"
|
||||||
mprisNoTrack = "/org/mpris/MediaPlayer2/TrackList/NoTrack"
|
mprisNoTrack = "/org/mpris/MediaPlayer2/TrackList/NoTrack"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -22,24 +22,31 @@ type mprisRoot struct{}
|
||||||
func (r mprisRoot) Raise() error {
|
func (r mprisRoot) Raise() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r mprisRoot) Quit() error {
|
func (r mprisRoot) Quit() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r mprisRoot) CanQuit() (bool, error) {
|
func (r mprisRoot) CanQuit() (bool, error) {
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r mprisRoot) CanRaise() (bool, error) {
|
func (r mprisRoot) CanRaise() (bool, error) {
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r mprisRoot) HasTrackList() (bool, error) {
|
func (r mprisRoot) HasTrackList() (bool, error) {
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r mprisRoot) Identity() (string, error) {
|
func (r mprisRoot) Identity() (string, error) {
|
||||||
return mprisPlayerNmae, nil
|
return mprisPlayerNmae, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r mprisRoot) SupportedUriSchemes() ([]string, error) {
|
func (r mprisRoot) SupportedUriSchemes() ([]string, error) {
|
||||||
return []string{}, nil
|
return []string{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r mprisRoot) SupportedMimeTypes() ([]string, error) {
|
func (r mprisRoot) SupportedMimeTypes() ([]string, error) {
|
||||||
return []string{}, nil
|
return []string{}, nil
|
||||||
}
|
}
|
||||||
|
@ -48,21 +55,27 @@ type mprisPlayer struct {
|
||||||
ctrl *Controller
|
ctrl *Controller
|
||||||
}
|
}
|
||||||
|
|
||||||
// Implement other methods of `pkg.types.OrgMprisMediaPlayer2PlayerAdapter`
|
// Implement other methods of `pkg.types.OrgMprisMediaPlayer2PlayerAdapter`.
|
||||||
func (p mprisPlayer) Next() error {
|
func (p mprisPlayer) Next() error {
|
||||||
p.ctrl.Next()
|
p.ctrl.Next()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p mprisPlayer) Previous() error {
|
func (p mprisPlayer) Previous() error {
|
||||||
p.ctrl.Prev()
|
p.ctrl.Prev()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p mprisPlayer) Pause() error {
|
func (p mprisPlayer) Pause() error {
|
||||||
if p.ctrl.State() == PlaybackStatePlaying {
|
if p.ctrl.State() == PlaybackStatePlaying {
|
||||||
p.ctrl.TogglePlayPause()
|
p.ctrl.TogglePlayPause()
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p mprisPlayer) PlayPause() error {
|
func (p mprisPlayer) PlayPause() error {
|
||||||
switch p.ctrl.State() {
|
switch p.ctrl.State() {
|
||||||
case PlaybackStatePaused, PlaybackStatePlaying:
|
case PlaybackStatePaused, PlaybackStatePlaying:
|
||||||
|
@ -70,12 +83,16 @@ func (p mprisPlayer) PlayPause() error {
|
||||||
case PlaybackStateStopped:
|
case PlaybackStateStopped:
|
||||||
p.ctrl.Play(p.ctrl.GetCurrentSong())
|
p.ctrl.Play(p.ctrl.GetCurrentSong())
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p mprisPlayer) Stop() error {
|
func (p mprisPlayer) Stop() error {
|
||||||
p.ctrl.Stop()
|
p.ctrl.Stop()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p mprisPlayer) Play() error {
|
func (p mprisPlayer) Play() error {
|
||||||
switch p.ctrl.State() {
|
switch p.ctrl.State() {
|
||||||
case PlaybackStatePaused:
|
case PlaybackStatePaused:
|
||||||
|
@ -83,45 +100,54 @@ func (p mprisPlayer) Play() error {
|
||||||
case PlaybackStateStopped:
|
case PlaybackStateStopped:
|
||||||
p.ctrl.Play(p.ctrl.GetCurrentSong())
|
p.ctrl.Play(p.ctrl.GetCurrentSong())
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
func (p mprisPlayer) Seek(offset Microseconds) error {
|
|
||||||
|
func (p mprisPlayer) Seek(offset mprisTypes.Microseconds) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
func (p mprisPlayer) SetPosition(trackId string, position Microseconds) error {
|
|
||||||
|
func (p mprisPlayer) SetPosition(trackId string, position mprisTypes.Microseconds) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p mprisPlayer) OpenUri(uri string) error {
|
func (p mprisPlayer) OpenUri(uri string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
func (p mprisPlayer) PlaybackStatus() (PlaybackStatus, error) {
|
|
||||||
|
func (p mprisPlayer) PlaybackStatus() (mprisTypes.PlaybackStatus, error) {
|
||||||
switch p.ctrl.State() {
|
switch p.ctrl.State() {
|
||||||
case PlaybackStatePlaying:
|
case PlaybackStatePlaying:
|
||||||
return PlaybackStatusPlaying, nil
|
return mprisTypes.PlaybackStatusPlaying, nil
|
||||||
case PlaybackStatePaused:
|
case PlaybackStatePaused:
|
||||||
return PlaybackStatusPaused, nil
|
return mprisTypes.PlaybackStatusPaused, nil
|
||||||
case PlaybackStateStopped:
|
case PlaybackStateStopped:
|
||||||
return PlaybackStatusStopped, nil
|
return mprisTypes.PlaybackStatusStopped, nil
|
||||||
}
|
}
|
||||||
// Should not get here
|
// Should not get here
|
||||||
return PlaybackStatusStopped, nil
|
return mprisTypes.PlaybackStatusStopped, nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p mprisPlayer) Rate() (float64, error) {
|
func (p mprisPlayer) Rate() (float64, error) {
|
||||||
return 1, nil
|
return 1, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p mprisPlayer) SetRate(float64) error {
|
func (p mprisPlayer) SetRate(float64) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
func (p mprisPlayer) Metadata() (Metadata, error) {
|
|
||||||
|
func (p mprisPlayer) Metadata() (mprisTypes.Metadata, error) {
|
||||||
s := p.ctrl.GetCurrentSong()
|
s := p.ctrl.GetCurrentSong()
|
||||||
objPath := mprisNoTrack
|
objPath := mprisNoTrack
|
||||||
|
|
||||||
if s != nil {
|
if s != nil {
|
||||||
objPath = encodeTrackId(s.ID)
|
objPath = encodeTrackID(s.ID)
|
||||||
} else {
|
} else {
|
||||||
s = &subsonic.Child{}
|
s = &subsonic.Child{}
|
||||||
}
|
}
|
||||||
md := Metadata{
|
|
||||||
|
md := mprisTypes.Metadata{
|
||||||
TrackId: dbus.ObjectPath(objPath),
|
TrackId: dbus.ObjectPath(objPath),
|
||||||
Length: secondsToMicroseconds(s.Duration),
|
Length: secondsToMicroseconds(s.Duration),
|
||||||
Title: s.Title,
|
Title: s.Title,
|
||||||
|
@ -134,41 +160,54 @@ func (p mprisPlayer) Metadata() (Metadata, error) {
|
||||||
UseCount: int(s.PlayCount),
|
UseCount: int(s.PlayCount),
|
||||||
}
|
}
|
||||||
artw := client.ArtCache.GetPath(s.CoverArt)
|
artw := client.ArtCache.GetPath(s.CoverArt)
|
||||||
|
|
||||||
if artw != nil {
|
if artw != nil {
|
||||||
md.ArtUrl = fmt.Sprintf("file://%s", *artw)
|
md.ArtUrl = "file://" + *artw
|
||||||
}
|
}
|
||||||
|
|
||||||
return md, nil
|
return md, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p mprisPlayer) Volume() (float64, error) {
|
func (p mprisPlayer) Volume() (float64, error) {
|
||||||
return 1, nil
|
return 1, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p mprisPlayer) SetVolume(float64) error {
|
func (p mprisPlayer) SetVolume(float64) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p mprisPlayer) Position() (int64, error) {
|
func (p mprisPlayer) Position() (int64, error) {
|
||||||
return int64(secondsToMicroseconds(int(p.ctrl.position))), nil
|
return int64(secondsToMicroseconds(int(p.ctrl.position))), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p mprisPlayer) MinimumRate() (float64, error) {
|
func (p mprisPlayer) MinimumRate() (float64, error) {
|
||||||
return 1, nil
|
return 1, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p mprisPlayer) MaximumRate() (float64, error) {
|
func (p mprisPlayer) MaximumRate() (float64, error) {
|
||||||
return 1, nil
|
return 1, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p mprisPlayer) CanGoNext() (bool, error) {
|
func (p mprisPlayer) CanGoNext() (bool, error) {
|
||||||
return p.ctrl.queue.HasNext(), nil
|
return p.ctrl.queue.HasNext(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p mprisPlayer) CanGoPrevious() (bool, error) {
|
func (p mprisPlayer) CanGoPrevious() (bool, error) {
|
||||||
return p.ctrl.queue.HasPrev(), nil
|
return p.ctrl.queue.HasPrev(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p mprisPlayer) CanPlay() (bool, error) {
|
func (p mprisPlayer) CanPlay() (bool, error) {
|
||||||
return p.ctrl.GetCurrentSong() != nil, nil
|
return p.ctrl.GetCurrentSong() != nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p mprisPlayer) CanPause() (bool, error) {
|
func (p mprisPlayer) CanPause() (bool, error) {
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p mprisPlayer) CanSeek() (bool, error) {
|
func (p mprisPlayer) CanSeek() (bool, error) {
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p mprisPlayer) CanControl() (bool, error) {
|
func (p mprisPlayer) CanControl() (bool, error) {
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
@ -197,18 +236,23 @@ func (p *mprisPlayback) OnPlayPause() {
|
||||||
if p.err != nil {
|
if p.err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
p.err = p.eventHandler.Player.OnPlayPause()
|
p.err = p.eventHandler.Player.OnPlayPause()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *mprisPlayback) OnPlaylistChanged() {
|
func (p *mprisPlayback) OnPlaylistChanged() {
|
||||||
if p.err != nil {
|
if p.err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
p.err = p.eventHandler.Player.OnOptions()
|
p.err = p.eventHandler.Player.OnOptions()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *mprisPlayback) OnSongChanged() {
|
func (p *mprisPlayback) OnSongChanged() {
|
||||||
if p.err != nil {
|
if p.err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
p.err = p.eventHandler.Player.OnTitle()
|
p.err = p.eventHandler.Player.OnTitle()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -216,10 +260,12 @@ func (p *mprisPlayback) OnPositionChanged(position int) {
|
||||||
if p.err != nil {
|
if p.err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
p.err = p.eventHandler.Player.OnSeek(secondsToMicroseconds(position))
|
p.err = p.eventHandler.Player.OnSeek(secondsToMicroseconds(position))
|
||||||
if p.err != nil {
|
if p.err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
p.err = p.eventHandler.Player.OnOptions()
|
p.err = p.eventHandler.Player.OnOptions()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -239,11 +285,12 @@ func desktopPlayer(c *Controller) DesktopPlayback {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func secondsToMicroseconds(s int) Microseconds {
|
func secondsToMicroseconds(s int) mprisTypes.Microseconds {
|
||||||
return Microseconds(s * 1_000_000)
|
return mprisTypes.Microseconds(s * 1_000_000)
|
||||||
}
|
}
|
||||||
|
|
||||||
func encodeTrackId(id string) string {
|
func encodeTrackID(id string) string {
|
||||||
data := []byte(id)
|
data := []byte(id)
|
||||||
|
|
||||||
return fmt.Sprintf("/%s/Track/%s", mprisPlayerNmae, base32.StdEncoding.WithPadding('0').EncodeToString(data))
|
return fmt.Sprintf("/%s/Track/%s", mprisPlayerNmae, base32.StdEncoding.WithPadding('0').EncodeToString(data))
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,23 +24,28 @@ func (q *queue) Clear() {
|
||||||
q.songQueue = []*subsonic.Child{}
|
q.songQueue = []*subsonic.Child{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// returns true if queue was empty before addition
|
// returns true if queue was empty before addition.
|
||||||
func (q *queue) Add(songs ...*subsonic.Child) bool {
|
func (q *queue) Add(songs ...*subsonic.Child) bool {
|
||||||
shouldStartPlaying := len(q.songQueue) == 0
|
shouldStartPlaying := len(q.songQueue) == 0
|
||||||
q.songQueue = append(q.songQueue, songs...)
|
q.songQueue = append(q.songQueue, songs...)
|
||||||
|
|
||||||
if shouldStartPlaying {
|
if shouldStartPlaying {
|
||||||
q.currentSong = 0
|
q.currentSong = 0
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// returns a song if position has changed
|
// returns a song if position has changed.
|
||||||
func (q *queue) SetPosition(position int) *subsonic.Child {
|
func (q *queue) SetPosition(position int) *subsonic.Child {
|
||||||
if position == q.currentSong || position < 0 || len(q.songQueue) < position {
|
if position == q.currentSong || position < 0 || len(q.songQueue) < position {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
q.currentSong = position
|
q.currentSong = position
|
||||||
|
|
||||||
return q.GetCurrentSong()
|
return q.GetCurrentSong()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,6 +57,7 @@ func (q *queue) GetCurrentSong() *subsonic.Child {
|
||||||
if len(q.songQueue) == 0 {
|
if len(q.songQueue) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return q.songQueue[q.currentSong]
|
return q.songQueue[q.currentSong]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -65,17 +71,21 @@ func (q *queue) HasNext() bool {
|
||||||
|
|
||||||
func (q *queue) Next() *subsonic.Child {
|
func (q *queue) Next() *subsonic.Child {
|
||||||
if len(q.songQueue) > q.currentSong+1 {
|
if len(q.songQueue) > q.currentSong+1 {
|
||||||
q.currentSong = q.currentSong + 1
|
q.currentSong++
|
||||||
|
|
||||||
return q.GetCurrentSong()
|
return q.GetCurrentSong()
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *queue) Prev() *subsonic.Child {
|
func (q *queue) Prev() *subsonic.Child {
|
||||||
if q.currentSong > 0 {
|
if q.currentSong > 0 {
|
||||||
q.currentSong = q.currentSong - 1
|
q.currentSong--
|
||||||
|
|
||||||
return q.GetCurrentSong()
|
return q.GetCurrentSong()
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -24,12 +24,14 @@ func NewLogin() *TUI {
|
||||||
app := tview.NewApplication()
|
app := tview.NewApplication()
|
||||||
layout := views.NewLoginView(func(u, p, url string) {
|
layout := views.NewLoginView(func(u, p, url string) {
|
||||||
c := client.NewClient(url)
|
c := client.NewClient(url)
|
||||||
|
|
||||||
err := c.Authenticate(u, p)
|
err := c.Authenticate(u, p)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.Stop()
|
app.Stop()
|
||||||
fmt.Printf("[Error] Failed to login. Aborting %e", err)
|
fmt.Printf("[Error] Failed to login. Aborting %e", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
config.SetURL(url)
|
config.SetURL(url)
|
||||||
config.SetUsername(u)
|
config.SetUsername(u)
|
||||||
config.SetPassword(p)
|
config.SetPassword(p)
|
||||||
|
@ -47,6 +49,7 @@ func NewLogin() *TUI {
|
||||||
app.Stop()
|
app.Stop()
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
return event
|
return event
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -79,36 +82,46 @@ func NewPlayer(client *client.Client, playbackCtl *playback.Controller) *TUI {
|
||||||
pages.SwitchToPage("app")
|
pages.SwitchToPage("app")
|
||||||
help.GetView().Blur()
|
help.GetView().Blur()
|
||||||
layout.GetView().Focus(nil)
|
layout.GetView().Focus(nil)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if layout.Mode() == views.StatusModeSearch {
|
if layout.Mode() == views.StatusModeSearch {
|
||||||
return event
|
return event
|
||||||
}
|
}
|
||||||
if event.Rune() == 'q' {
|
|
||||||
|
switch event.Rune() {
|
||||||
|
case 'q':
|
||||||
app.Stop()
|
app.Stop()
|
||||||
fmt.Println("Exiting..")
|
fmt.Println("Exiting..")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
} else if event.Rune() == 'h' {
|
case 'h':
|
||||||
return tcell.NewEventKey(tcell.KeyLeft, rune(tcell.KeyLeft), event.Modifiers())
|
return tcell.NewEventKey(tcell.KeyLeft, rune(tcell.KeyLeft), event.Modifiers())
|
||||||
} else if event.Rune() == 'j' {
|
case 'j':
|
||||||
return tcell.NewEventKey(tcell.KeyDown, rune(tcell.KeyDown), event.Modifiers())
|
return tcell.NewEventKey(tcell.KeyDown, rune(tcell.KeyDown), event.Modifiers())
|
||||||
} else if event.Rune() == 'k' {
|
case 'k':
|
||||||
return tcell.NewEventKey(tcell.KeyUp, rune(tcell.KeyUp), event.Modifiers())
|
return tcell.NewEventKey(tcell.KeyUp, rune(tcell.KeyUp), event.Modifiers())
|
||||||
} else if event.Rune() == 'l' {
|
case 'l':
|
||||||
return tcell.NewEventKey(tcell.KeyRight, rune(tcell.KeyRight), event.Modifiers())
|
return tcell.NewEventKey(tcell.KeyRight, rune(tcell.KeyRight), event.Modifiers())
|
||||||
} else if event.Rune() == '?' {
|
case '?':
|
||||||
pages.SwitchToPage("help")
|
pages.SwitchToPage("help")
|
||||||
layout.GetView().Blur()
|
layout.GetView().Blur()
|
||||||
|
|
||||||
go app.Draw()
|
go app.Draw()
|
||||||
|
default:
|
||||||
|
return event
|
||||||
}
|
}
|
||||||
|
|
||||||
return event
|
return event
|
||||||
})
|
})
|
||||||
|
|
||||||
app.SetAfterDrawFunc(func(screen tcell.Screen) {
|
app.SetAfterDrawFunc(func(screen tcell.Screen) {
|
||||||
layout.Update()
|
layout.Update()
|
||||||
})
|
})
|
||||||
app.GetFocus().Blur()
|
app.GetFocus().Blur()
|
||||||
|
|
||||||
//app.SetFocus(layout.GetView())
|
// app.SetFocus(layout.GetView())
|
||||||
|
|
||||||
return &TUI{
|
return &TUI{
|
||||||
app: app,
|
app: app,
|
||||||
|
@ -116,7 +129,6 @@ func NewPlayer(client *client.Client, playbackCtl *playback.Controller) *TUI {
|
||||||
client: client,
|
client: client,
|
||||||
playbackCtl: playbackCtl,
|
playbackCtl: playbackCtl,
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *TUI) Run() error {
|
func (t *TUI) Run() error {
|
||||||
|
|
|
@ -17,7 +17,6 @@ type albums struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAlbums(client *client.Client) *albums {
|
func NewAlbums(client *client.Client) *albums {
|
||||||
|
|
||||||
list := tview.NewTable()
|
list := tview.NewTable()
|
||||||
|
|
||||||
list.SetBackgroundColor(config.ColorBackground)
|
list.SetBackgroundColor(config.ColorBackground)
|
||||||
|
@ -46,6 +45,7 @@ func NewAlbums(client *client.Client) *albums {
|
||||||
})
|
})
|
||||||
|
|
||||||
obj.Update()
|
obj.Update()
|
||||||
|
|
||||||
return obj
|
return obj
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,9 +56,11 @@ func (a *albums) SetAlbums(al []*subsonic.AlbumID3) {
|
||||||
|
|
||||||
func (a *albums) Update() {
|
func (a *albums) Update() {
|
||||||
a.view.Clear()
|
a.view.Clear()
|
||||||
|
|
||||||
for i, pl := range a.albums {
|
for i, pl := range a.albums {
|
||||||
title := tview.NewTableCell(pl.Name).SetExpansion(1).SetMaxWidth(15)
|
title := tview.NewTableCell(pl.Name).SetExpansion(1).SetMaxWidth(15)
|
||||||
artist := tview.NewTableCell(pl.Artist).SetExpansion(1).SetAlign(tview.AlignRight)
|
artist := tview.NewTableCell(pl.Artist).SetExpansion(1).SetAlign(tview.AlignRight)
|
||||||
|
|
||||||
a.view.SetCell(i, 0, title)
|
a.view.SetCell(i, 0, title)
|
||||||
a.view.SetCell(i, 1, artist)
|
a.view.SetCell(i, 1, artist)
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,6 @@ type artists struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewArtists(client *client.Client) *artists {
|
func NewArtists(client *client.Client) *artists {
|
||||||
|
|
||||||
list := tview.NewTable()
|
list := tview.NewTable()
|
||||||
|
|
||||||
list.SetBackgroundColor(config.ColorBackground)
|
list.SetBackgroundColor(config.ColorBackground)
|
||||||
|
@ -38,7 +37,6 @@ func NewArtists(client *client.Client) *artists {
|
||||||
for i, artist := range arts {
|
for i, artist := range arts {
|
||||||
cell := tview.NewTableCell(artist.Name).SetExpansion(1)
|
cell := tview.NewTableCell(artist.Name).SetExpansion(1)
|
||||||
list.SetCell(i, 0, cell)
|
list.SetCell(i, 0, cell)
|
||||||
// list.AddItem(artist.Name, fmt.Sprintf("%s", artist.Name), '0', nil)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resp := &artists{
|
resp := &artists{
|
||||||
|
@ -57,12 +55,12 @@ func NewArtists(client *client.Client) *artists {
|
||||||
func (a *artists) SetSelectArtistFunc(f func(artistId string)) {
|
func (a *artists) SetSelectArtistFunc(f func(artistId string)) {
|
||||||
a.selectArtistFunc = f
|
a.selectArtistFunc = f
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *artists) SetOpenArtistFunc(f func(artistId string)) {
|
func (a *artists) SetOpenArtistFunc(f func(artistId string)) {
|
||||||
a.openArtistFunc = f
|
a.openArtistFunc = f
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *artists) Update() {
|
func (a *artists) Update() {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *artists) GetView() tview.Primitive {
|
func (a *artists) GetView() tview.Primitive {
|
||||||
|
|
|
@ -60,8 +60,10 @@ func NewHelp() *help {
|
||||||
h := &help{}
|
h := &help{}
|
||||||
view := tview.NewTable()
|
view := tview.NewTable()
|
||||||
view.SetBackgroundColor(config.ColorBackground)
|
view.SetBackgroundColor(config.ColorBackground)
|
||||||
|
|
||||||
height := 16
|
height := 16
|
||||||
width := 100
|
width := 100
|
||||||
|
|
||||||
view.SetBorder(true)
|
view.SetBorder(true)
|
||||||
view.SetTitle(" Keybindings ")
|
view.SetTitle(" Keybindings ")
|
||||||
view.SetTitleAlign(tview.AlignCenter)
|
view.SetTitleAlign(tview.AlignCenter)
|
||||||
|
@ -69,9 +71,11 @@ func NewHelp() *help {
|
||||||
for i, km := range keyMaps {
|
for i, km := range keyMaps {
|
||||||
odd := i%2 != 0
|
odd := i%2 != 0
|
||||||
txtColor := config.ColorText
|
txtColor := config.ColorText
|
||||||
|
|
||||||
if odd {
|
if odd {
|
||||||
txtColor = config.ColorTextAccent
|
txtColor = config.ColorTextAccent
|
||||||
}
|
}
|
||||||
|
|
||||||
keyCell := tview.NewTableCell(km.key).
|
keyCell := tview.NewTableCell(km.key).
|
||||||
SetExpansion(1).
|
SetExpansion(1).
|
||||||
SetAlign(tview.AlignCenter).
|
SetAlign(tview.AlignCenter).
|
||||||
|
@ -80,6 +84,7 @@ func NewHelp() *help {
|
||||||
SetExpansion(1).
|
SetExpansion(1).
|
||||||
SetAlign(tview.AlignCenter).
|
SetAlign(tview.AlignCenter).
|
||||||
SetTextColor(txtColor)
|
SetTextColor(txtColor)
|
||||||
|
|
||||||
view.SetCell(i, 0, keyCell)
|
view.SetCell(i, 0, keyCell)
|
||||||
view.SetCell(i, 1, descCell)
|
view.SetCell(i, 1, descCell)
|
||||||
}
|
}
|
||||||
|
@ -94,7 +99,6 @@ func NewHelp() *help {
|
||||||
AddItem(innerFlex, width, 1, true).
|
AddItem(innerFlex, width, 1, true).
|
||||||
AddItem(EmptyBox, 0, 1, false)
|
AddItem(EmptyBox, 0, 1, false)
|
||||||
wrapper.SetBackgroundColor((config.ColorBackground))
|
wrapper.SetBackgroundColor((config.ColorBackground))
|
||||||
|
|
||||||
h.view = wrapper
|
h.view = wrapper
|
||||||
|
|
||||||
return h
|
return h
|
||||||
|
@ -105,7 +109,6 @@ func (h *help) GetView() tview.Primitive {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *help) Update() {
|
func (h *help) Update() {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *help) SetKeyPressedFunc(f func()) {
|
func (h *help) SetKeyPressedFunc(f func()) {
|
||||||
|
|
|
@ -23,13 +23,16 @@ type layout struct {
|
||||||
mainView *main
|
mainView *main
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//gocyclo:ignore
|
||||||
func NewLayout(client *client.Client, playbackCtl *playback.Controller, refreshUI func()) *layout {
|
func NewLayout(client *client.Client, playbackCtl *playback.Controller, refreshUI func()) *layout {
|
||||||
layout := &layout{}
|
layout := &layout{}
|
||||||
|
|
||||||
largeView := tview.NewGrid().SetRows(0, 4, 1)
|
largeView := tview.NewGrid().SetRows(0, 4, 1)
|
||||||
largeView.SetBackgroundColor(config.ColorBackground)
|
largeView.SetBackgroundColor(config.ColorBackground)
|
||||||
|
|
||||||
smallView := tview.NewFlex().SetDirection(tview.FlexRow)
|
smallView := tview.NewFlex().SetDirection(tview.FlexRow)
|
||||||
smallView.SetBackgroundColor(config.ColorBackground)
|
smallView.SetBackgroundColor(config.ColorBackground)
|
||||||
|
|
||||||
layout.largeView = largeView
|
layout.largeView = largeView
|
||||||
layout.smallView = smallView
|
layout.smallView = smallView
|
||||||
pages := tview.NewPages()
|
pages := tview.NewPages()
|
||||||
|
@ -73,6 +76,7 @@ func NewLayout(client *client.Client, playbackCtl *playback.Controller, refreshU
|
||||||
artists.SetSelectArtistFunc(func(artistId string) {
|
artists.SetSelectArtistFunc(func(artistId string) {
|
||||||
artists.view.Blur()
|
artists.view.Blur()
|
||||||
statusLine.Log("Fetching artist's albums...")
|
statusLine.Log("Fetching artist's albums...")
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
a, _ := client.GetArtist(artistId)
|
a, _ := client.GetArtist(artistId)
|
||||||
albums.SetAlbums(a.Album)
|
albums.SetAlbums(a.Album)
|
||||||
|
@ -83,8 +87,10 @@ func NewLayout(client *client.Client, playbackCtl *playback.Controller, refreshU
|
||||||
artists.SetOpenArtistFunc(func(artistId string) {
|
artists.SetOpenArtistFunc(func(artistId string) {
|
||||||
artists.view.Blur()
|
artists.view.Blur()
|
||||||
statusLine.Log("Fetching artists...")
|
statusLine.Log("Fetching artists...")
|
||||||
|
|
||||||
layout.currentFocusedView = main.GetView()
|
layout.currentFocusedView = main.GetView()
|
||||||
layout.rebuildSmallView()
|
layout.rebuildSmallView()
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
a, _ := client.GetArtist(artistId)
|
a, _ := client.GetArtist(artistId)
|
||||||
main.SetArtist(a)
|
main.SetArtist(a)
|
||||||
|
@ -96,8 +102,11 @@ func NewLayout(client *client.Client, playbackCtl *playback.Controller, refreshU
|
||||||
albums.SetCallback(func(albumID string) {
|
albums.SetCallback(func(albumID string) {
|
||||||
albums.view.Blur()
|
albums.view.Blur()
|
||||||
statusLine.Log("Fetching album...")
|
statusLine.Log("Fetching album...")
|
||||||
|
|
||||||
layout.currentFocusedView = main.GetView()
|
layout.currentFocusedView = main.GetView()
|
||||||
|
|
||||||
layout.rebuildSmallView()
|
layout.rebuildSmallView()
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
a, _ := client.GetAlbum(albumID)
|
a, _ := client.GetAlbum(albumID)
|
||||||
main.SetAlbum(a)
|
main.SetAlbum(a)
|
||||||
|
@ -109,8 +118,10 @@ func NewLayout(client *client.Client, playbackCtl *playback.Controller, refreshU
|
||||||
playlists.SetCallback(func(p *subsonic.Playlist) {
|
playlists.SetCallback(func(p *subsonic.Playlist) {
|
||||||
playlists.view.Blur()
|
playlists.view.Blur()
|
||||||
statusLine.Log("Fetching playlist...")
|
statusLine.Log("Fetching playlist...")
|
||||||
|
|
||||||
layout.currentFocusedView = main.GetView()
|
layout.currentFocusedView = main.GetView()
|
||||||
layout.rebuildSmallView()
|
layout.rebuildSmallView()
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
playlist, _ := client.GetPlaylist(p.ID)
|
playlist, _ := client.GetPlaylist(p.ID)
|
||||||
main.SetPlaylist(playlist)
|
main.SetPlaylist(playlist)
|
||||||
|
@ -140,6 +151,7 @@ func NewLayout(client *client.Client, playbackCtl *playback.Controller, refreshU
|
||||||
playbackCtl.SetSongEndedFunc(func(song *subsonic.Child) {
|
playbackCtl.SetSongEndedFunc(func(song *subsonic.Child) {
|
||||||
statusLine.Log("")
|
statusLine.Log("")
|
||||||
queue.Update(playbackCtl.GetQueue(), playbackCtl.GetQueuePosition())
|
queue.Update(playbackCtl.GetQueue(), playbackCtl.GetQueuePosition())
|
||||||
|
|
||||||
if config.ScrobbleEnabled() {
|
if config.ScrobbleEnabled() {
|
||||||
// Scrobble
|
// Scrobble
|
||||||
_ = client.Scrobble(song.ID)
|
_ = client.Scrobble(song.ID)
|
||||||
|
@ -159,13 +171,16 @@ func NewLayout(client *client.Client, playbackCtl *playback.Controller, refreshU
|
||||||
if len(quary) == 0 {
|
if len(quary) == 0 {
|
||||||
layout.currentFocusedView.Focus(nil)
|
layout.currentFocusedView.Focus(nil)
|
||||||
statusLine.Log("Search canceled")
|
statusLine.Log("Search canceled")
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Search...
|
// Search...
|
||||||
statusLine.Log("Searching for '%s'....", quary)
|
statusLine.Log("Searching for '%s'....", quary)
|
||||||
statusLine.view.Blur()
|
statusLine.view.Blur()
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
result, _ := client.Search(quary)
|
result, _ := client.Search(quary)
|
||||||
|
|
||||||
layout.currentFocusedView.Blur()
|
layout.currentFocusedView.Blur()
|
||||||
main.SetSearch(result, quary)
|
main.SetSearch(result, quary)
|
||||||
layout.currentFocusedView = main.GetView()
|
layout.currentFocusedView = main.GetView()
|
||||||
|
@ -179,6 +194,7 @@ func NewLayout(client *client.Client, playbackCtl *playback.Controller, refreshU
|
||||||
if statusLine.Mode() == StatusModeSearch {
|
if statusLine.Mode() == StatusModeSearch {
|
||||||
return event
|
return event
|
||||||
}
|
}
|
||||||
|
|
||||||
if event.Rune() == '1' {
|
if event.Rune() == '1' {
|
||||||
// Focus Artists
|
// Focus Artists
|
||||||
artists.view.Blur()
|
artists.view.Blur()
|
||||||
|
@ -189,6 +205,7 @@ func NewLayout(client *client.Client, playbackCtl *playback.Controller, refreshU
|
||||||
artists.view.Focus(nil)
|
artists.view.Focus(nil)
|
||||||
layout.currentFocusedView = artists.GetView()
|
layout.currentFocusedView = artists.GetView()
|
||||||
layout.rebuildSmallView()
|
layout.rebuildSmallView()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
} else if event.Rune() == '2' {
|
} else if event.Rune() == '2' {
|
||||||
// Focus Albums
|
// Focus Albums
|
||||||
|
@ -200,6 +217,7 @@ func NewLayout(client *client.Client, playbackCtl *playback.Controller, refreshU
|
||||||
albums.view.Focus(nil)
|
albums.view.Focus(nil)
|
||||||
layout.currentFocusedView = albums.GetView()
|
layout.currentFocusedView = albums.GetView()
|
||||||
layout.rebuildSmallView()
|
layout.rebuildSmallView()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
} else if event.Rune() == '3' {
|
} else if event.Rune() == '3' {
|
||||||
// Focus Playlists
|
// Focus Playlists
|
||||||
|
@ -211,6 +229,7 @@ func NewLayout(client *client.Client, playbackCtl *playback.Controller, refreshU
|
||||||
playlists.view.Focus(nil)
|
playlists.view.Focus(nil)
|
||||||
layout.currentFocusedView = playlists.GetView()
|
layout.currentFocusedView = playlists.GetView()
|
||||||
layout.rebuildSmallView()
|
layout.rebuildSmallView()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
} else if event.Rune() == '`' {
|
} else if event.Rune() == '`' {
|
||||||
// Focus Songs
|
// Focus Songs
|
||||||
|
@ -222,6 +241,7 @@ func NewLayout(client *client.Client, playbackCtl *playback.Controller, refreshU
|
||||||
main.view.Focus(nil)
|
main.view.Focus(nil)
|
||||||
layout.currentFocusedView = main.GetView()
|
layout.currentFocusedView = main.GetView()
|
||||||
layout.rebuildSmallView()
|
layout.rebuildSmallView()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
} else if event.Rune() == '4' {
|
} else if event.Rune() == '4' {
|
||||||
// Focus Queue
|
// Focus Queue
|
||||||
|
@ -233,19 +253,22 @@ func NewLayout(client *client.Client, playbackCtl *playback.Controller, refreshU
|
||||||
queue.view.Focus(nil)
|
queue.view.Focus(nil)
|
||||||
layout.currentFocusedView = queue.GetView()
|
layout.currentFocusedView = queue.GetView()
|
||||||
layout.rebuildSmallView()
|
layout.rebuildSmallView()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
} else if event.Rune() == 'n' {
|
} else if event.Rune() == 'n' {
|
||||||
playbackCtl.Next()
|
playbackCtl.Next()
|
||||||
queue.Update(playbackCtl.GetQueue(), playbackCtl.GetQueuePosition())
|
queue.Update(playbackCtl.GetQueue(), playbackCtl.GetQueuePosition())
|
||||||
return nil
|
|
||||||
|
|
||||||
|
return nil
|
||||||
} else if event.Rune() == 'N' {
|
} else if event.Rune() == 'N' {
|
||||||
playbackCtl.Prev()
|
playbackCtl.Prev()
|
||||||
queue.Update(playbackCtl.GetQueue(), playbackCtl.GetQueuePosition())
|
queue.Update(playbackCtl.GetQueue(), playbackCtl.GetQueuePosition())
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
} else if event.Rune() == 's' {
|
} else if event.Rune() == 's' {
|
||||||
playbackCtl.Stop()
|
playbackCtl.Stop()
|
||||||
queue.Update(playbackCtl.GetQueue(), playbackCtl.GetQueuePosition())
|
queue.Update(playbackCtl.GetQueue(), playbackCtl.GetQueuePosition())
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
} else if event.Rune() == 'p' {
|
} else if event.Rune() == 'p' {
|
||||||
if playbackCtl.State() == playback.PlaybackStateStopped {
|
if playbackCtl.State() == playback.PlaybackStateStopped {
|
||||||
|
@ -253,19 +276,25 @@ func NewLayout(client *client.Client, playbackCtl *playback.Controller, refreshU
|
||||||
if song != nil {
|
if song != nil {
|
||||||
playbackCtl.Play(song)
|
playbackCtl.Play(song)
|
||||||
}
|
}
|
||||||
|
|
||||||
queue.Update(playbackCtl.GetQueue(), playbackCtl.GetQueuePosition())
|
queue.Update(playbackCtl.GetQueue(), playbackCtl.GetQueuePosition())
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
playbackCtl.TogglePlayPause()
|
playbackCtl.TogglePlayPause()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
} else if event.Rune() == 'c' {
|
} else if event.Rune() == 'c' {
|
||||||
playbackCtl.ClearQueue()
|
playbackCtl.ClearQueue()
|
||||||
queue.Update(playbackCtl.GetQueue(), playbackCtl.GetQueuePosition())
|
queue.Update(playbackCtl.GetQueue(), playbackCtl.GetQueuePosition())
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
} else if event.Rune() == '/' {
|
} else if event.Rune() == '/' {
|
||||||
layout.currentFocusedView.Blur()
|
layout.currentFocusedView.Blur()
|
||||||
statusLine.Search()
|
statusLine.Search()
|
||||||
refreshUI()
|
refreshUI()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
} else if event.Rune() == 'L' {
|
} else if event.Rune() == 'L' {
|
||||||
if layout.currentFocusedView == albums.GetView() {
|
if layout.currentFocusedView == albums.GetView() {
|
||||||
|
@ -356,12 +385,14 @@ func NewLayout(client *client.Client, playbackCtl *playback.Controller, refreshU
|
||||||
|
|
||||||
layout.view = pages
|
layout.view = pages
|
||||||
layout.player = player
|
layout.player = player
|
||||||
//Auto focus on artists
|
// Auto focus on artists
|
||||||
artists.GetView().Focus(nil)
|
artists.GetView().Focus(nil)
|
||||||
layout.currentFocusedView = artists.GetView()
|
layout.currentFocusedView = artists.GetView()
|
||||||
layout.rebuildSmallView()
|
layout.rebuildSmallView()
|
||||||
|
|
||||||
return layout
|
return layout
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *layout) rebuildSmallView() {
|
func (l *layout) rebuildSmallView() {
|
||||||
l.smallView.Clear()
|
l.smallView.Clear()
|
||||||
l.smallView.AddItem(l.currentFocusedView, 0, 1, false)
|
l.smallView.AddItem(l.currentFocusedView, 0, 1, false)
|
||||||
|
@ -381,18 +412,22 @@ func (l *layout) Update() {
|
||||||
_, _, w, h := l.view.GetRect()
|
_, _, w, h := l.view.GetRect()
|
||||||
page, _ := l.view.GetFrontPage()
|
page, _ := l.view.GetFrontPage()
|
||||||
smallView := w < 100 || h < 30
|
smallView := w < 100 || h < 30
|
||||||
|
|
||||||
if smallView {
|
if smallView {
|
||||||
if page != "small" {
|
if page != "small" {
|
||||||
l.mainView.SetMiniView(true)
|
l.mainView.SetMiniView(true)
|
||||||
go l.view.SwitchToPage("small")
|
go l.view.SwitchToPage("small")
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if page != "large" {
|
if page != "large" {
|
||||||
l.mainView.SetMiniView(false)
|
l.mainView.SetMiniView(false)
|
||||||
go l.view.SwitchToPage("large")
|
go l.view.SwitchToPage("large")
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
l.player.Update()
|
l.player.Update()
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,9 +65,9 @@ func NewLoginView(loginFunc func(u, p, url string), exitFunc func()) View {
|
||||||
|
|
||||||
l.GetView().Focus(func(p tview.Primitive) {})
|
l.GetView().Focus(func(p tview.Primitive) {})
|
||||||
form.SetFocus(0)
|
form.SetFocus(0)
|
||||||
|
|
||||||
return l
|
return l
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *login) Update() {
|
func (l *login) Update() {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,13 +54,15 @@ func NewMainView(client *client.Client, log func(format string, a ...any)) *main
|
||||||
flex.SetBorder(true)
|
flex.SetBorder(true)
|
||||||
flex.SetFocusFunc(func() {
|
flex.SetFocusFunc(func() {
|
||||||
flex.SetBorderColor(config.ColorSelectedBoarder)
|
flex.SetBorderColor(config.ColorSelectedBoarder)
|
||||||
|
|
||||||
if playlistAlbum.songList != nil {
|
if playlistAlbum.songList != nil {
|
||||||
//playlistAlbum.view.Blur()
|
// playlistAlbum.view.Blur()
|
||||||
playlistAlbum.songList.Focus(nil)
|
playlistAlbum.songList.Focus(nil)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
flex.SetBlurFunc(func() {
|
flex.SetBlurFunc(func() {
|
||||||
flex.SetBorderColor(config.ColorBluredBoarder)
|
flex.SetBorderColor(config.ColorBluredBoarder)
|
||||||
|
|
||||||
if playlistAlbum.songList != nil {
|
if playlistAlbum.songList != nil {
|
||||||
playlistAlbum.songList.Blur()
|
playlistAlbum.songList.Blur()
|
||||||
}
|
}
|
||||||
|
@ -69,6 +71,7 @@ func NewMainView(client *client.Client, log func(format string, a ...any)) *main
|
||||||
playlistAlbum.view = flex
|
playlistAlbum.view = flex
|
||||||
// Empty Box for starters...
|
// Empty Box for starters...
|
||||||
playlistAlbum.view.AddItem(EmptyBox, 0, 1, false)
|
playlistAlbum.view.AddItem(EmptyBox, 0, 1, false)
|
||||||
|
|
||||||
return playlistAlbum
|
return playlistAlbum
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -112,34 +115,43 @@ func (m *main) SetSearch(result *subsonic.SearchResult3, query string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *main) drawPlaylist() {
|
func (m *main) drawPlaylist() {
|
||||||
subtitle := fmt.Sprintf("%s\n\nCreated by: %s | %s", m.playlist.Comment, m.playlist.Owner, time.Duration(m.playlist.Duration*int(time.Second)).String())
|
subtitle := fmt.Sprintf("%s\n\nCreated by: %s | %s", m.playlist.Comment,
|
||||||
|
m.playlist.Owner, time.Duration(m.playlist.Duration*int(time.Second)).String())
|
||||||
m.populateHeader(m.playlist.Name, subtitle, m.playlist.Duration, m.playlist.CoverArt)
|
m.populateHeader(m.playlist.Name, subtitle, m.playlist.Duration, m.playlist.CoverArt)
|
||||||
|
|
||||||
playBtn := m.drawPlaylistAlbumButtons(m.playlist.Entry)
|
playBtn := m.drawPlaylistAlbumButtons(m.playlist.Entry)
|
||||||
|
|
||||||
m.populateSongs(m.playlist.Entry, playBtn)
|
m.populateSongs(m.playlist.Entry, playBtn)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *main) drawAlbum() {
|
func (m *main) drawAlbum() {
|
||||||
subtitle := fmt.Sprintf("%s\n\n%d | %s", m.album.Artist, m.album.Year, time.Duration(m.album.Duration*int(time.Second)).String())
|
subtitle := fmt.Sprintf("%s\n\n%d | %s", m.album.Artist, m.album.Year,
|
||||||
|
time.Duration(m.album.Duration*int(time.Second)).String())
|
||||||
m.populateHeader(m.album.Name, subtitle, m.album.Duration, m.album.CoverArt)
|
m.populateHeader(m.album.Name, subtitle, m.album.Duration, m.album.CoverArt)
|
||||||
playBtn := m.drawPlaylistAlbumButtons(m.album.Song)
|
|
||||||
m.populateSongs(m.album.Song, playBtn)
|
|
||||||
|
|
||||||
|
playBtn := m.drawPlaylistAlbumButtons(m.album.Song)
|
||||||
|
|
||||||
|
m.populateSongs(m.album.Song, playBtn)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *main) drawArtist() {
|
func (m *main) drawArtist() {
|
||||||
m.populateHeader(m.artist.Name, m.artistInfo.Biography, 0, m.artist.CoverArt)
|
m.populateHeader(m.artist.Name, m.artistInfo.Biography, 0, m.artist.CoverArt)
|
||||||
|
|
||||||
btn := m.drawArtistButtons()
|
btn := m.drawArtistButtons()
|
||||||
|
|
||||||
m.populateAlbums(btn)
|
m.populateAlbums(btn)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *main) drawSearch() {
|
func (m *main) drawSearch() {
|
||||||
sub := fmt.Sprintf("Query: %s", m.query)
|
sub := "Query: " + m.query
|
||||||
|
|
||||||
m.populateHeader("Search Results", sub, 0, "")
|
m.populateHeader("Search Results", sub, 0, "")
|
||||||
m.populateSearchResults()
|
m.populateSearchResults()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *main) Update() {
|
func (m *main) Update() {
|
||||||
m.view.Clear()
|
m.view.Clear()
|
||||||
|
|
||||||
switch m.mode {
|
switch m.mode {
|
||||||
case mainModeAlbum:
|
case mainModeAlbum:
|
||||||
m.drawAlbum()
|
m.drawAlbum()
|
||||||
|
@ -150,12 +162,14 @@ func (m *main) Update() {
|
||||||
case mainModeSearch:
|
case mainModeSearch:
|
||||||
m.drawSearch()
|
m.drawSearch()
|
||||||
}
|
}
|
||||||
|
|
||||||
m.songList.Focus(nil)
|
m.songList.Focus(nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *main) drawArtistButtons() *tview.Button {
|
func (m *main) drawArtistButtons() *tview.Button {
|
||||||
// Add buttons: Radio
|
// Add buttons: Radio
|
||||||
songs, _ := m.client.GetTopSongs(m.artist.Name, 10)
|
topSongs := 10
|
||||||
|
songs, _ := m.client.GetTopSongs(m.artist.Name, topSongs)
|
||||||
f := tview.NewFlex()
|
f := tview.NewFlex()
|
||||||
f.SetBackgroundColor(config.ColorBackground)
|
f.SetBackgroundColor(config.ColorBackground)
|
||||||
f.SetBorderPadding(0, 0, 2, 2)
|
f.SetBorderPadding(0, 0, 2, 2)
|
||||||
|
@ -172,11 +186,13 @@ func (m *main) drawArtistButtons() *tview.Button {
|
||||||
radio.SetSelectedFunc(func() {
|
radio.SetSelectedFunc(func() {
|
||||||
radio.Blur()
|
radio.Blur()
|
||||||
m.log("Generating %s's radio", m.artist.Name)
|
m.log("Generating %s's radio", m.artist.Name)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
radioSongs := []*subsonic.Child{}
|
radioSongs := []*subsonic.Child{}
|
||||||
if config.ExperimentalRadioAlgo() {
|
if config.ExperimentalRadioAlgo() {
|
||||||
radioSongs, _ = m.client.GetExperimentalArtistRadio(m.artist, m.artistInfo, config.MaxRadioSongs())
|
radioSongs, _ = m.client.GetExperimentalArtistRadio(m.artist, m.artistInfo, config.MaxRadioSongs())
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(radioSongs) == 0 {
|
if len(radioSongs) == 0 {
|
||||||
radioSongs, _ = m.client.GetSimilarSongs(m.artist.ID, config.MaxRadioSongs())
|
radioSongs, _ = m.client.GetSimilarSongs(m.artist.ID, config.MaxRadioSongs())
|
||||||
}
|
}
|
||||||
|
@ -191,33 +207,41 @@ func (m *main) drawArtistButtons() *tview.Button {
|
||||||
case tcell.KeyDown:
|
case tcell.KeyDown:
|
||||||
radio.Blur()
|
radio.Blur()
|
||||||
m.songList.Focus(nil)
|
m.songList.Focus(nil)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
case tcell.KeyRight:
|
case tcell.KeyRight:
|
||||||
radio.Blur()
|
radio.Blur()
|
||||||
top10.Focus(nil)
|
top10.Focus(nil)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
default:
|
||||||
return event
|
return event
|
||||||
|
}
|
||||||
})
|
})
|
||||||
top10.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
top10.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||||
switch event.Key() {
|
switch event.Key() {
|
||||||
case tcell.KeyDown:
|
case tcell.KeyDown:
|
||||||
top10.Blur()
|
top10.Blur()
|
||||||
m.songList.Focus(nil)
|
m.songList.Focus(nil)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
case tcell.KeyLeft:
|
case tcell.KeyLeft:
|
||||||
top10.Blur()
|
top10.Blur()
|
||||||
radio.Focus(nil)
|
radio.Focus(nil)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
default:
|
||||||
return event
|
return event
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
f.AddItem(radio, 0, 1, false)
|
f.AddItem(radio, 0, 1, false)
|
||||||
f.AddItem(EmptyBox, 0, 1, false)
|
f.AddItem(EmptyBox, 0, 1, false)
|
||||||
|
|
||||||
if len(songs) > 0 {
|
if len(songs) > 0 {
|
||||||
f.AddItem(top10, 0, 1, false)
|
f.AddItem(top10, 0, 1, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
f.AddItem(EmptyBox, 0, 1, false)
|
f.AddItem(EmptyBox, 0, 1, false)
|
||||||
|
|
||||||
// Add the buttons to the view
|
// Add the buttons to the view
|
||||||
|
@ -232,8 +256,9 @@ func (m *main) populateAlbums(btn *tview.Button) {
|
||||||
table := tview.NewTable()
|
table := tview.NewTable()
|
||||||
table.SetBackgroundColor(config.ColorBackground)
|
table.SetBackgroundColor(config.ColorBackground)
|
||||||
table.SetWrapSelection(true, false)
|
table.SetWrapSelection(true, false)
|
||||||
|
|
||||||
for i, album := range m.artist.Album {
|
for i, album := range m.artist.Album {
|
||||||
year := tview.NewTableCell(fmt.Sprintf("%d", album.Year)).SetTextColor(config.ColorTextAccent)
|
year := tview.NewTableCell(strconv.Itoa(album.Year)).SetTextColor(config.ColorTextAccent)
|
||||||
name := tview.NewTableCell(album.Name).SetExpansion(2).SetAlign(tview.AlignCenter)
|
name := tview.NewTableCell(album.Name).SetExpansion(2).SetAlign(tview.AlignCenter)
|
||||||
d := time.Second * time.Duration(album.Duration)
|
d := time.Second * time.Duration(album.Duration)
|
||||||
duration := tview.NewTableCell(d.String()).SetAlign(tview.AlignRight).SetExpansion(1)
|
duration := tview.NewTableCell(d.String()).SetAlign(tview.AlignRight).SetExpansion(1)
|
||||||
|
@ -262,14 +287,16 @@ func (m *main) populateAlbums(btn *tview.Button) {
|
||||||
table.Blur()
|
table.Blur()
|
||||||
m.view.SetBorderColor(config.ColorSelectedBoarder)
|
m.view.SetBorderColor(config.ColorSelectedBoarder)
|
||||||
btn.Focus(nil)
|
btn.Focus(nil)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return event
|
return event
|
||||||
})
|
})
|
||||||
|
|
||||||
m.songList = table
|
m.songList = table
|
||||||
|
|
||||||
m.view.AddItem(table, 0, 1, false)
|
m.view.AddItem(table, 0, 1, false)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *main) drawPlaylistAlbumButtons(songs []*subsonic.Child) *tview.Button {
|
func (m *main) drawPlaylistAlbumButtons(songs []*subsonic.Child) *tview.Button {
|
||||||
|
@ -293,20 +320,25 @@ func (m *main) drawPlaylistAlbumButtons(songs []*subsonic.Child) *tview.Button {
|
||||||
case tcell.KeyLeft:
|
case tcell.KeyLeft:
|
||||||
play.Blur()
|
play.Blur()
|
||||||
artist.Focus(nil)
|
artist.Focus(nil)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
case tcell.KeyRight:
|
case tcell.KeyRight:
|
||||||
play.Blur()
|
play.Blur()
|
||||||
shuffle.Focus(nil)
|
shuffle.Focus(nil)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
case tcell.KeyDown:
|
case tcell.KeyDown:
|
||||||
play.Blur()
|
play.Blur()
|
||||||
m.songList.Focus(nil)
|
m.songList.Focus(nil)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
default:
|
||||||
return event
|
return event
|
||||||
|
}
|
||||||
})
|
})
|
||||||
shuffle.SetSelectedFunc(func() {
|
shuffle.SetSelectedFunc(func() {
|
||||||
shuffle.Blur()
|
shuffle.Blur()
|
||||||
|
|
||||||
cpy := make([]*subsonic.Child, len(songs))
|
cpy := make([]*subsonic.Child, len(songs))
|
||||||
copy(cpy, songs)
|
copy(cpy, songs)
|
||||||
common.ShuffleSlice(cpy)
|
common.ShuffleSlice(cpy)
|
||||||
|
@ -318,17 +350,21 @@ func (m *main) drawPlaylistAlbumButtons(songs []*subsonic.Child) *tview.Button {
|
||||||
case tcell.KeyLeft:
|
case tcell.KeyLeft:
|
||||||
shuffle.Blur()
|
shuffle.Blur()
|
||||||
play.Focus(nil)
|
play.Focus(nil)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
case tcell.KeyRight:
|
case tcell.KeyRight:
|
||||||
shuffle.Blur()
|
shuffle.Blur()
|
||||||
queue.Focus(nil)
|
queue.Focus(nil)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
case tcell.KeyDown:
|
case tcell.KeyDown:
|
||||||
shuffle.Blur()
|
shuffle.Blur()
|
||||||
m.songList.Focus(nil)
|
m.songList.Focus(nil)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
default:
|
||||||
return event
|
return event
|
||||||
|
}
|
||||||
})
|
})
|
||||||
queue.SetSelectedFunc(func() {
|
queue.SetSelectedFunc(func() {
|
||||||
queue.Blur()
|
queue.Blur()
|
||||||
|
@ -340,20 +376,25 @@ func (m *main) drawPlaylistAlbumButtons(songs []*subsonic.Child) *tview.Button {
|
||||||
case tcell.KeyLeft:
|
case tcell.KeyLeft:
|
||||||
queue.Blur()
|
queue.Blur()
|
||||||
shuffle.Focus(nil)
|
shuffle.Focus(nil)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
case tcell.KeyRight:
|
case tcell.KeyRight:
|
||||||
queue.Blur()
|
queue.Blur()
|
||||||
artist.Focus(nil)
|
artist.Focus(nil)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
case tcell.KeyDown:
|
case tcell.KeyDown:
|
||||||
queue.Blur()
|
queue.Blur()
|
||||||
m.songList.Focus(nil)
|
m.songList.Focus(nil)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
default:
|
||||||
return event
|
return event
|
||||||
|
}
|
||||||
})
|
})
|
||||||
artist.SetSelectedFunc(func() {
|
artist.SetSelectedFunc(func() {
|
||||||
artist.Blur()
|
artist.Blur()
|
||||||
|
|
||||||
ar, _ := m.client.GetArtist(m.album.ArtistID)
|
ar, _ := m.client.GetArtist(m.album.ArtistID)
|
||||||
m.SetArtist(ar)
|
m.SetArtist(ar)
|
||||||
m.songList.Focus(nil)
|
m.songList.Focus(nil)
|
||||||
|
@ -363,16 +404,20 @@ func (m *main) drawPlaylistAlbumButtons(songs []*subsonic.Child) *tview.Button {
|
||||||
case tcell.KeyLeft:
|
case tcell.KeyLeft:
|
||||||
artist.Blur()
|
artist.Blur()
|
||||||
queue.Focus(nil)
|
queue.Focus(nil)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
case tcell.KeyRight:
|
case tcell.KeyRight:
|
||||||
artist.Blur()
|
artist.Blur()
|
||||||
play.Focus(nil)
|
play.Focus(nil)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
case tcell.KeyDown:
|
case tcell.KeyDown:
|
||||||
artist.Blur()
|
artist.Blur()
|
||||||
m.songList.Focus(nil)
|
m.songList.Focus(nil)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return event
|
return event
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -396,12 +441,14 @@ func (m *main) populateSongs(songs []*subsonic.Child, play *tview.Button) {
|
||||||
table := tview.NewTable()
|
table := tview.NewTable()
|
||||||
table.SetBackgroundColor(config.ColorBackground)
|
table.SetBackgroundColor(config.ColorBackground)
|
||||||
table.SetWrapSelection(true, false)
|
table.SetWrapSelection(true, false)
|
||||||
|
|
||||||
for i, song := range songs {
|
for i, song := range songs {
|
||||||
num := tview.NewTableCell(fmt.Sprintf("%d", i+1)).SetTextColor(config.ColorTextAccent)
|
num := tview.NewTableCell(strconv.Itoa(i + 1)).SetTextColor(config.ColorTextAccent)
|
||||||
title := tview.NewTableCell(song.Title).SetMaxWidth(15).SetExpansion(2)
|
title := tview.NewTableCell(song.Title).SetMaxWidth(15).SetExpansion(2)
|
||||||
artist := tview.NewTableCell(song.Artist).SetMaxWidth(15).SetExpansion(1)
|
artist := tview.NewTableCell(song.Artist).SetMaxWidth(15).SetExpansion(1)
|
||||||
d := time.Second * time.Duration(song.Duration)
|
d := time.Second * time.Duration(song.Duration)
|
||||||
duration := tview.NewTableCell(d.String()).SetAlign(tview.AlignRight).SetExpansion(1)
|
duration := tview.NewTableCell(d.String()).SetAlign(tview.AlignRight).SetExpansion(1)
|
||||||
|
|
||||||
table.SetCell(i, 0, num)
|
table.SetCell(i, 0, num)
|
||||||
table.SetCell(i, 1, title)
|
table.SetCell(i, 1, title)
|
||||||
table.SetCell(i, 2, artist)
|
table.SetCell(i, 2, artist)
|
||||||
|
@ -416,15 +463,18 @@ func (m *main) populateSongs(songs []*subsonic.Child, play *tview.Button) {
|
||||||
table.Blur()
|
table.Blur()
|
||||||
m.view.SetBorderColor(config.ColorSelectedBoarder)
|
m.view.SetBorderColor(config.ColorSelectedBoarder)
|
||||||
play.Focus(nil)
|
play.Focus(nil)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
} else if event.Rune() == 'r' {
|
} else if event.Rune() == 'r' {
|
||||||
song := songs[row]
|
song := songs[row]
|
||||||
m.log("Generating song (%s) radio....", song.Title)
|
m.log("Generating song (%s) radio....", song.Title)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
radioSongs, _ := m.client.GetSimilarSongs(song.ID, config.MaxRadioSongs())
|
radioSongs, _ := m.client.GetSimilarSongs(song.ID, config.MaxRadioSongs())
|
||||||
m.playAllFunc(radioSongs...)
|
m.playAllFunc(radioSongs...)
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
return event
|
return event
|
||||||
})
|
})
|
||||||
table.SetFocusFunc(func() {
|
table.SetFocusFunc(func() {
|
||||||
|
@ -446,6 +496,7 @@ func (m *main) populateSearchResults() {
|
||||||
table := tview.NewTable()
|
table := tview.NewTable()
|
||||||
table.SetBackgroundColor(config.ColorBackground)
|
table.SetBackgroundColor(config.ColorBackground)
|
||||||
table.SetWrapSelection(true, false)
|
table.SetWrapSelection(true, false)
|
||||||
|
|
||||||
row := 0
|
row := 0
|
||||||
lastArtist := 0
|
lastArtist := 0
|
||||||
lastAlbum := 0
|
lastAlbum := 0
|
||||||
|
@ -455,19 +506,23 @@ func (m *main) populateSearchResults() {
|
||||||
// Header
|
// Header
|
||||||
header := tview.NewTableCell("Artists").SetSelectable(false)
|
header := tview.NewTableCell("Artists").SetSelectable(false)
|
||||||
table.SetCell(row, 0, header)
|
table.SetCell(row, 0, header)
|
||||||
|
|
||||||
row++
|
row++
|
||||||
//List
|
// List
|
||||||
for i, artist := range m.searchResult.Artist {
|
for i, artist := range m.searchResult.Artist {
|
||||||
index := tview.NewTableCell(fmt.Sprintf("%d", i+1)).
|
index := tview.NewTableCell(strconv.Itoa(i + 1)).
|
||||||
SetTextColor(config.ColorTextAccent)
|
SetTextColor(config.ColorTextAccent)
|
||||||
a := tview.NewTableCell(artist.Name).SetExpansion(2).SetMaxWidth(15)
|
a := tview.NewTableCell(artist.Name).SetExpansion(2).SetMaxWidth(15)
|
||||||
acount := tview.NewTableCell(fmt.Sprintf("%d Albums", artist.AlbumCount)).
|
count := tview.NewTableCell(fmt.Sprintf("%d Albums", artist.AlbumCount)).
|
||||||
SetExpansion(1).SetAlign(tview.AlignRight).SetMaxWidth(15)
|
SetExpansion(1).SetAlign(tview.AlignRight).SetMaxWidth(15)
|
||||||
|
|
||||||
table.SetCell(row, 0, index)
|
table.SetCell(row, 0, index)
|
||||||
table.SetCell(row, 1, a)
|
table.SetCell(row, 1, a)
|
||||||
table.SetCell(row, 2, acount)
|
table.SetCell(row, 2, count)
|
||||||
|
|
||||||
row++
|
row++
|
||||||
}
|
}
|
||||||
|
|
||||||
lastArtist = row
|
lastArtist = row
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -477,19 +532,23 @@ func (m *main) populateSearchResults() {
|
||||||
// Header
|
// Header
|
||||||
header := tview.NewTableCell("Albums").SetSelectable(false)
|
header := tview.NewTableCell("Albums").SetSelectable(false)
|
||||||
table.SetCell(row, 0, header)
|
table.SetCell(row, 0, header)
|
||||||
|
|
||||||
row++
|
row++
|
||||||
//List
|
// List
|
||||||
for i, album := range m.searchResult.Album {
|
for i, album := range m.searchResult.Album {
|
||||||
index := tview.NewTableCell(fmt.Sprintf("%d", i+1)).
|
index := tview.NewTableCell(strconv.Itoa(i + 1)).
|
||||||
SetTextColor(config.ColorTextAccent)
|
SetTextColor(config.ColorTextAccent)
|
||||||
title := tview.NewTableCell(album.Name).SetExpansion(2).SetMaxWidth(15)
|
title := tview.NewTableCell(album.Name).SetExpansion(2).SetMaxWidth(15)
|
||||||
artist := tview.NewTableCell(album.Artist).
|
artist := tview.NewTableCell(album.Artist).
|
||||||
SetExpansion(1).SetAlign(tview.AlignRight).SetMaxWidth(15)
|
SetExpansion(1).SetAlign(tview.AlignRight).SetMaxWidth(15)
|
||||||
|
|
||||||
table.SetCell(row, 0, index)
|
table.SetCell(row, 0, index)
|
||||||
table.SetCell(row, 1, title)
|
table.SetCell(row, 1, title)
|
||||||
table.SetCell(row, 2, artist)
|
table.SetCell(row, 2, artist)
|
||||||
|
|
||||||
row++
|
row++
|
||||||
}
|
}
|
||||||
|
|
||||||
lastAlbum = row
|
lastAlbum = row
|
||||||
}
|
}
|
||||||
// Songs
|
// Songs
|
||||||
|
@ -497,17 +556,20 @@ func (m *main) populateSearchResults() {
|
||||||
// Header
|
// Header
|
||||||
header := tview.NewTableCell("Songs").SetSelectable(false)
|
header := tview.NewTableCell("Songs").SetSelectable(false)
|
||||||
table.SetCell(row, 0, header)
|
table.SetCell(row, 0, header)
|
||||||
|
|
||||||
row++
|
row++
|
||||||
//List
|
// List
|
||||||
for i, song := range m.searchResult.Song {
|
for i, song := range m.searchResult.Song {
|
||||||
index := tview.NewTableCell(fmt.Sprintf("%d", i+1)).
|
index := tview.NewTableCell(strconv.Itoa(i + 1)).
|
||||||
SetTextColor(config.ColorTextAccent)
|
SetTextColor(config.ColorTextAccent)
|
||||||
title := tview.NewTableCell(song.Title).SetExpansion(2).SetMaxWidth(15)
|
title := tview.NewTableCell(song.Title).SetExpansion(2).SetMaxWidth(15)
|
||||||
artist := tview.NewTableCell(song.Artist).
|
artist := tview.NewTableCell(song.Artist).
|
||||||
SetExpansion(1).SetAlign(tview.AlignRight).SetMaxWidth(15)
|
SetExpansion(1).SetAlign(tview.AlignRight).SetMaxWidth(15)
|
||||||
|
|
||||||
table.SetCell(row, 0, index)
|
table.SetCell(row, 0, index)
|
||||||
table.SetCell(row, 1, title)
|
table.SetCell(row, 1, title)
|
||||||
table.SetCell(row, 2, artist)
|
table.SetCell(row, 2, artist)
|
||||||
|
|
||||||
row++
|
row++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -526,28 +588,36 @@ func (m *main) populateSearchResults() {
|
||||||
if row <= lastAlbum {
|
if row <= lastAlbum {
|
||||||
return event
|
return event
|
||||||
}
|
}
|
||||||
|
|
||||||
if event.Rune() != 'r' {
|
if event.Rune() != 'r' {
|
||||||
return event
|
return event
|
||||||
}
|
}
|
||||||
|
|
||||||
cell := table.GetCell(row, 0)
|
cell := table.GetCell(row, 0)
|
||||||
|
|
||||||
index, err := strconv.Atoi(cell.Text)
|
index, err := strconv.Atoi(cell.Text)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
song := m.searchResult.Song[index-1]
|
song := m.searchResult.Song[index-1]
|
||||||
m.log("Generating song (%s) radio....", song.Title)
|
m.log("Generating song (%s) radio....", song.Title)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
radioSongs, _ := m.client.GetSimilarSongs(song.ID, config.MaxRadioSongs())
|
radioSongs, _ := m.client.GetSimilarSongs(song.ID, config.MaxRadioSongs())
|
||||||
m.playAllFunc(radioSongs...)
|
m.playAllFunc(radioSongs...)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
m.songList.SetSelectedFunc(func(row, column int) {
|
m.songList.SetSelectedFunc(func(row, column int) {
|
||||||
cell := table.GetCell(row, 0)
|
cell := table.GetCell(row, 0)
|
||||||
|
|
||||||
index, err := strconv.Atoi(cell.Text)
|
index, err := strconv.Atoi(cell.Text)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if row <= lastArtist {
|
if row <= lastArtist {
|
||||||
artist, _ := m.client.GetArtist(m.searchResult.Artist[index-1].ID)
|
artist, _ := m.client.GetArtist(m.searchResult.Artist[index-1].ID)
|
||||||
m.SetArtist(artist)
|
m.SetArtist(artist)
|
||||||
|
@ -563,15 +633,18 @@ func (m *main) populateSearchResults() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *main) populateHeader(title, subtitle string,
|
func (m *main) populateHeader(title, subtitle string,
|
||||||
duration int, coverArtID string) {
|
duration int, coverArtID string,
|
||||||
|
) {
|
||||||
|
_ = duration
|
||||||
header := tview.NewFlex()
|
header := tview.NewFlex()
|
||||||
header.SetBackgroundColor(config.ColorBackground)
|
header.SetBackgroundColor(config.ColorBackground)
|
||||||
|
|
||||||
art := tview.NewImage()
|
art := tview.NewImage()
|
||||||
art.SetBackgroundColor(config.ColorBackground)
|
art.SetBackgroundColor(config.ColorBackground)
|
||||||
|
|
||||||
img, _ := m.client.GetCoverArt(coverArtID)
|
img, _ := m.client.GetCoverArt(coverArtID)
|
||||||
art.SetImage(img)
|
art.SetImage(img)
|
||||||
|
|
||||||
t := tview.NewTextView().
|
t := tview.NewTextView().
|
||||||
SetTextColor(config.ColorTextAccent).
|
SetTextColor(config.ColorTextAccent).
|
||||||
SetTextAlign(tview.AlignCenter).
|
SetTextAlign(tview.AlignCenter).
|
||||||
|
@ -591,10 +664,12 @@ func (m *main) populateHeader(title, subtitle string,
|
||||||
|
|
||||||
header.AddItem(art, 0, 1, false)
|
header.AddItem(art, 0, 1, false)
|
||||||
header.AddItem(g, 0, 3, false)
|
header.AddItem(g, 0, 3, false)
|
||||||
|
|
||||||
size := 6
|
size := 6
|
||||||
if m.miniView {
|
if m.miniView {
|
||||||
size = 4
|
size = 4
|
||||||
}
|
}
|
||||||
|
|
||||||
m.view.AddItem(header, size, 1, false)
|
m.view.AddItem(header, size, 1, false)
|
||||||
|
|
||||||
// Margin bottom of 1 line
|
// Margin bottom of 1 line
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
|
|
||||||
"git.dayanhub.com/sagi/subsonic-tui/internal/client"
|
"git.dayanhub.com/sagi/subsonic-tui/internal/client"
|
||||||
"git.dayanhub.com/sagi/subsonic-tui/internal/config"
|
"git.dayanhub.com/sagi/subsonic-tui/internal/config"
|
||||||
|
"git.dayanhub.com/sagi/subsonic-tui/internal/variables"
|
||||||
"github.com/delucks/go-subsonic"
|
"github.com/delucks/go-subsonic"
|
||||||
"github.com/rivo/tview"
|
"github.com/rivo/tview"
|
||||||
)
|
)
|
||||||
|
@ -26,7 +27,7 @@ func NewPlayer(client *client.Client) *player {
|
||||||
grid := tview.NewGrid().SetColumns(9, 0)
|
grid := tview.NewGrid().SetColumns(9, 0)
|
||||||
grid.SetBackgroundColor(config.ColorBackground)
|
grid.SetBackgroundColor(config.ColorBackground)
|
||||||
|
|
||||||
//album art
|
// album art
|
||||||
art := tview.NewImage()
|
art := tview.NewImage()
|
||||||
art.SetBackgroundColor(config.ColorBackground)
|
art.SetBackgroundColor(config.ColorBackground)
|
||||||
|
|
||||||
|
@ -57,7 +58,7 @@ func NewPlayer(client *client.Client) *player {
|
||||||
|
|
||||||
player.SetSongInfo(&subsonic.Child{
|
player.SetSongInfo(&subsonic.Child{
|
||||||
Title: "Subsonic TUI",
|
Title: "Subsonic TUI",
|
||||||
Album: "MaVeZe",
|
Album: "Made with love ❤️",
|
||||||
Artist: "ZeGoomba",
|
Artist: "ZeGoomba",
|
||||||
CoverArt: "",
|
CoverArt: "",
|
||||||
Duration: 0,
|
Duration: 0,
|
||||||
|
@ -81,13 +82,14 @@ func (p *player) LoadAlbumArt(ID string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *player) UpdateProgress(elapsed time.Duration) {
|
func (p *player) UpdateProgress(elapsed time.Duration) {
|
||||||
|
|
||||||
if p.song.Duration == 0 {
|
if p.song.Duration == 0 {
|
||||||
// Startup... Show version number
|
// Startup... Show version number
|
||||||
versionInfo := tview.NewTextView().SetText("Version: 0.1")
|
versionInfo := tview.NewTextView().SetText(fmt.Sprintf("Version: %s | Commit: %s",
|
||||||
|
variables.Version, variables.Commit))
|
||||||
versionInfo.SetBackgroundColor(config.ColorPlaybackProgressRemaining)
|
versionInfo.SetBackgroundColor(config.ColorPlaybackProgressRemaining)
|
||||||
versionInfo.SetTextColor(config.ColorPlaybackProgressElapsed)
|
versionInfo.SetTextColor(config.ColorPlaybackProgressElapsed)
|
||||||
p.progress.AddItem(versionInfo, 0, 1, false)
|
p.progress.AddItem(versionInfo, 0, 1, false)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,12 +98,15 @@ func (p *player) UpdateProgress(elapsed time.Duration) {
|
||||||
overlappedBox.SetBackgroundColor(config.ColorPlaybackProgressElapsed)
|
overlappedBox.SetBackgroundColor(config.ColorPlaybackProgressElapsed)
|
||||||
overlappedBox.SetTextColor(config.ColorPlaybackProgressRemaining)
|
overlappedBox.SetTextColor(config.ColorPlaybackProgressRemaining)
|
||||||
overlappedBox.SetText(songDuration.String())
|
overlappedBox.SetText(songDuration.String())
|
||||||
|
|
||||||
remainingBox := tview.NewTextView()
|
remainingBox := tview.NewTextView()
|
||||||
remainingBox.SetBackgroundColor(config.ColorPlaybackProgressRemaining)
|
remainingBox.SetBackgroundColor(config.ColorPlaybackProgressRemaining)
|
||||||
remainingBox.SetTextColor(config.ColorPlaybackProgressElapsed)
|
remainingBox.SetTextColor(config.ColorPlaybackProgressElapsed)
|
||||||
remainingBox.SetTextAlign(tview.AlignRight)
|
remainingBox.SetTextAlign(tview.AlignRight)
|
||||||
|
|
||||||
rm := time.Duration(songDuration.Seconds()-elapsed.Seconds()) * time.Second
|
rm := time.Duration(songDuration.Seconds()-elapsed.Seconds()) * time.Second
|
||||||
remaining := fmt.Sprintf("-%s", rm.String())
|
remaining := "-" + rm.String()
|
||||||
|
|
||||||
remainingBox.SetText(remaining)
|
remainingBox.SetText(remaining)
|
||||||
p.progress.Clear()
|
p.progress.Clear()
|
||||||
p.progress.AddItem(overlappedBox, 0, int(elapsed.Seconds()), false)
|
p.progress.AddItem(overlappedBox, 0, int(elapsed.Seconds()), false)
|
||||||
|
@ -113,5 +118,5 @@ func (p *player) GetView() tview.Primitive {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *player) Update() {
|
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 {
|
func NewPlaylists(client *client.Client) *playlists {
|
||||||
|
|
||||||
obj := &playlists{
|
obj := &playlists{
|
||||||
client: client,
|
client: client,
|
||||||
}
|
}
|
||||||
|
@ -55,7 +54,6 @@ func (p *playlists) SetCallback(f func(playlist *subsonic.Playlist)) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *playlists) Update() {
|
func (p *playlists) Update() {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *playlists) GetView() tview.Primitive {
|
func (p *playlists) GetView() tview.Primitive {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
package views
|
package views
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.dayanhub.com/sagi/subsonic-tui/internal/config"
|
"git.dayanhub.com/sagi/subsonic-tui/internal/config"
|
||||||
|
@ -43,20 +43,24 @@ func (q *queue) drawQueue() {
|
||||||
list := q.view
|
list := q.view
|
||||||
list.SetWrapSelection(true, false)
|
list.SetWrapSelection(true, false)
|
||||||
list.SetSelectable(true, false)
|
list.SetSelectable(true, false)
|
||||||
|
|
||||||
for i, song := range q.songQueue {
|
for i, song := range q.songQueue {
|
||||||
isCurrentSong := q.currentSong == i
|
isCurrentSong := q.currentSong == i
|
||||||
isPlayed := i < q.currentSong
|
isPlayed := i < q.currentSong
|
||||||
bgColor := config.ColorBackground
|
bgColor := config.ColorBackground
|
||||||
|
|
||||||
if isCurrentSong {
|
if isCurrentSong {
|
||||||
bgColor = config.ColorQueuePlayingBg
|
bgColor = config.ColorQueuePlayingBg
|
||||||
} else if isPlayed {
|
} else if isPlayed {
|
||||||
bgColor = config.ColorQueuePlayedBg
|
bgColor = config.ColorQueuePlayedBg
|
||||||
}
|
}
|
||||||
num := tview.NewTableCell(fmt.Sprintf("%d", i+1)).SetTextColor(config.ColorTextAccent).SetBackgroundColor(bgColor)
|
|
||||||
|
num := tview.NewTableCell(strconv.Itoa(i + 1)).SetTextColor(config.ColorTextAccent).SetBackgroundColor(bgColor)
|
||||||
title := tview.NewTableCell(song.Title).SetMaxWidth(15).SetExpansion(2).SetBackgroundColor(bgColor)
|
title := tview.NewTableCell(song.Title).SetMaxWidth(15).SetExpansion(2).SetBackgroundColor(bgColor)
|
||||||
artist := tview.NewTableCell(song.Artist).SetMaxWidth(15).SetExpansion(1).SetBackgroundColor(bgColor)
|
artist := tview.NewTableCell(song.Artist).SetMaxWidth(15).SetExpansion(1).SetBackgroundColor(bgColor)
|
||||||
d := time.Second * time.Duration(song.Duration)
|
d := time.Second * time.Duration(song.Duration)
|
||||||
duration := tview.NewTableCell(d.String()).SetAlign(tview.AlignRight).SetExpansion(1).SetBackgroundColor(bgColor)
|
duration := tview.NewTableCell(d.String()).SetAlign(tview.AlignRight).SetExpansion(1).SetBackgroundColor(bgColor)
|
||||||
|
|
||||||
list.SetCell(i, 0, num)
|
list.SetCell(i, 0, num)
|
||||||
list.SetCell(i, 1, title)
|
list.SetCell(i, 1, title)
|
||||||
list.SetCell(i, 2, artist)
|
list.SetCell(i, 2, artist)
|
||||||
|
@ -73,7 +77,6 @@ func (q *queue) Update(songs []*subsonic.Child, currentSong int) {
|
||||||
q.songQueue = songs
|
q.songQueue = songs
|
||||||
q.currentSong = currentSong
|
q.currentSong = currentSong
|
||||||
q.drawQueue()
|
q.drawQueue()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *queue) GetView() tview.Primitive {
|
func (q *queue) GetView() tview.Primitive {
|
||||||
|
|
|
@ -47,6 +47,7 @@ func (s *statusLine) Mode() Statusmode {
|
||||||
func (s *statusLine) Search() {
|
func (s *statusLine) Search() {
|
||||||
s.mode = StatusModeSearch
|
s.mode = StatusModeSearch
|
||||||
s.view.Clear()
|
s.view.Clear()
|
||||||
|
|
||||||
label := "Search: "
|
label := "Search: "
|
||||||
_, _, w, _ := s.view.GetRect()
|
_, _, w, _ := s.view.GetRect()
|
||||||
query := ""
|
query := ""
|
||||||
|
@ -81,9 +82,13 @@ func (s *statusLine) Log(format string, a ...any) {
|
||||||
if s.mode != StatusModeLog {
|
if s.mode != StatusModeLog {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
str := fmt.Sprintf(format, a...)
|
str := fmt.Sprintf(format, a...)
|
||||||
|
|
||||||
s.view.Clear()
|
s.view.Clear()
|
||||||
|
|
||||||
txt := tview.NewTextView().SetDynamicColors(true)
|
txt := tview.NewTextView().SetDynamicColors(true)
|
||||||
|
|
||||||
txt.SetBackgroundColor(config.ColorBackground)
|
txt.SetBackgroundColor(config.ColorBackground)
|
||||||
txt.SetText(str)
|
txt.SetText(str)
|
||||||
s.view.AddItem(txt, 0, 1, false)
|
s.view.AddItem(txt, 0, 1, false)
|
||||||
|
@ -93,6 +98,7 @@ func (s *statusLine) Log(format string, a ...any) {
|
||||||
func (s *statusLine) GetView() tview.Primitive {
|
func (s *statusLine) GetView() tview.Primitive {
|
||||||
return s.view
|
return s.view
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *statusLine) Update() {
|
func (s *statusLine) Update() {
|
||||||
s.onUpdateFunc()
|
s.onUpdateFunc()
|
||||||
}
|
}
|
||||||
|
|
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()
|
defer client.ArtCache.Destroy()
|
||||||
// Create Client
|
// Create Client
|
||||||
subsonicClient := client.NewClient(config.URL())
|
subsonicClient := client.NewClient(config.URL())
|
||||||
|
|
||||||
err := subsonicClient.Authenticate(config.Username(), config.Password())
|
err := subsonicClient.Authenticate(config.Username(), config.Password())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// We need to show Login...
|
// We need to show Login...
|
||||||
login := tui.NewLogin()
|
login := tui.NewLogin()
|
||||||
|
|
||||||
err := login.Run()
|
err := login.Run()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("Trying to login...")
|
fmt.Println("Trying to login...")
|
||||||
|
|
||||||
subsonicClient = client.NewClient(config.URL())
|
subsonicClient = client.NewClient(config.URL())
|
||||||
|
|
||||||
err = subsonicClient.Authenticate(config.Username(), config.Password())
|
err = subsonicClient.Authenticate(config.Username(), config.Password())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
|
|
Loading…
Reference in a new issue