Merge branch 'main' into feature/shuffle
This commit is contained in:
+12
-1
@@ -63,6 +63,7 @@ type App struct {
|
|||||||
// UI callbacks to be set in main
|
// UI callbacks to be set in main
|
||||||
OnReactivate func()
|
OnReactivate func()
|
||||||
OnExit func()
|
OnExit func()
|
||||||
|
OnReloadTheme func()
|
||||||
|
|
||||||
appName string
|
appName string
|
||||||
displayAppName string
|
displayAppName string
|
||||||
@@ -230,7 +231,8 @@ func StartupApp(appName, displayAppName, appVersion, appVersionTag, latestReleas
|
|||||||
ipcRatingHandler,
|
ipcRatingHandler,
|
||||||
a.ServerManager,
|
a.ServerManager,
|
||||||
a.callOnReactivate,
|
a.callOnReactivate,
|
||||||
func() { _ = a.callOnExit() })
|
func() { _ = a.callOnExit() },
|
||||||
|
a.callOnReloadTheme)
|
||||||
go a.ipcServer.Serve(listener)
|
go a.ipcServer.Serve(listener)
|
||||||
} else {
|
} else {
|
||||||
log.Printf("error starting IPC server: %s", err.Error())
|
log.Printf("error starting IPC server: %s", err.Error())
|
||||||
@@ -284,6 +286,7 @@ func (a *App) ClearCaches() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
a.ImageManager.ClearInMemoryCache()
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkPortablePath() string {
|
func checkPortablePath() string {
|
||||||
@@ -339,6 +342,12 @@ func (a *App) callOnReactivate() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *App) callOnReloadTheme() {
|
||||||
|
if a.OnReloadTheme != nil {
|
||||||
|
a.OnReloadTheme()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (a *App) callOnExit() error {
|
func (a *App) callOnExit() error {
|
||||||
if a.OnExit == nil {
|
if a.OnExit == nil {
|
||||||
return errors.New("no quit handler registered")
|
return errors.New("no quit handler registered")
|
||||||
@@ -710,6 +719,8 @@ func (a *App) checkFlagsAndSendIPCMsg(cli *ipc.Client) error {
|
|||||||
return cli.PauseAfterCurrent()
|
return cli.PauseAfterCurrent()
|
||||||
case *FlagShow:
|
case *FlagShow:
|
||||||
return cli.Show()
|
return cli.Show()
|
||||||
|
case *FlagReloadTheme:
|
||||||
|
return cli.ReloadTheme()
|
||||||
case VolumeCLIArg >= 0:
|
case VolumeCLIArg >= 0:
|
||||||
return cli.SetVolume(VolumeCLIArg)
|
return cli.SetVolume(VolumeCLIArg)
|
||||||
case VolumePctCLIArg != 0:
|
case VolumePctCLIArg != 0:
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ var (
|
|||||||
FlagPauseAfterCurrent = flag.Bool("pause-after-current", false, "pause playback after current track")
|
FlagPauseAfterCurrent = flag.Bool("pause-after-current", false, "pause playback after current track")
|
||||||
FlagStartMinimized = flag.Bool("start-minimized", false, "start app minimized")
|
FlagStartMinimized = flag.Bool("start-minimized", false, "start app minimized")
|
||||||
FlagShow = flag.Bool("show", false, "show minimized app")
|
FlagShow = flag.Bool("show", false, "show minimized app")
|
||||||
|
FlagReloadTheme = flag.Bool("reload-theme", false, "reload the current theme")
|
||||||
FlagShuffle = flag.Bool("shuffle", false, "shuffle the tracklist (to be used with either -play-album-by-id or -play-playlist-by-id)")
|
FlagShuffle = flag.Bool("shuffle", false, "shuffle the tracklist (to be used with either -play-album-by-id or -play-playlist-by-id)")
|
||||||
FlagVersion = flag.Bool("version", false, "print app version and exit")
|
FlagVersion = flag.Bool("version", false, "print app version and exit")
|
||||||
FlagHelp = flag.Bool("help", false, "print command line options and exit")
|
FlagHelp = flag.Bool("help", false, "print command line options and exit")
|
||||||
|
|||||||
@@ -66,10 +66,7 @@ func NewImageManager(ctx context.Context, s *ServerManager, baseCacheDir string)
|
|||||||
maxOnDiskCacheSizeBytes: defaultDiskCacheSizeBytes,
|
maxOnDiskCacheSizeBytes: defaultDiskCacheSizeBytes,
|
||||||
serverFetchSema: make(chan any, maxConcurrentServerFetches),
|
serverFetchSema: make(chan any, maxConcurrentServerFetches),
|
||||||
}
|
}
|
||||||
s.OnLogout(func() {
|
s.OnLogout(i.ClearInMemoryCache)
|
||||||
i.thumbnailCache.Clear()
|
|
||||||
i.clearFullSizeCover()
|
|
||||||
})
|
|
||||||
i.thumbnailCache.OnEvictTaskRan = func() {
|
i.thumbnailCache.OnEvictTaskRan = func() {
|
||||||
i.clearFullSizeCoverIfExpired()
|
i.clearFullSizeCoverIfExpired()
|
||||||
i.pruneOnDiskCache()
|
i.pruneOnDiskCache()
|
||||||
@@ -195,6 +192,12 @@ func (i *ImageManager) RefreshCachedArtistImageIfExpired(artistID string, imgURL
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ClearInMemoryCache clears images from the in-memory caches.
|
||||||
|
func (i *ImageManager) ClearInMemoryCache() {
|
||||||
|
i.thumbnailCache.Clear()
|
||||||
|
i.clearFullSizeCover()
|
||||||
|
}
|
||||||
|
|
||||||
func (i *ImageManager) ensureCoverCacheDir() string {
|
func (i *ImageManager) ensureCoverCacheDir() string {
|
||||||
// if user logged out with pending fetches in progress,
|
// if user logged out with pending fetches in progress,
|
||||||
// make sure we don't write to nil (00000000-*0) cache directory
|
// make sure we don't write to nil (00000000-*0) cache directory
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ const (
|
|||||||
VolumePath = "/volume" // ?v=<vol>
|
VolumePath = "/volume" // ?v=<vol>
|
||||||
VolumeAdjustPath = "/volume/adjust" // ?pct=<+/- percentage>
|
VolumeAdjustPath = "/volume/adjust" // ?pct=<+/- percentage>
|
||||||
ShowPath = "/window/show"
|
ShowPath = "/window/show"
|
||||||
|
ReloadThemePath = "/window/reload-theme"
|
||||||
QuitPath = "/window/quit"
|
QuitPath = "/window/quit"
|
||||||
RateCurrentTrackPath = "/current_track/rate" // ?r=<rating 0-5>
|
RateCurrentTrackPath = "/current_track/rate" // ?r=<rating 0-5>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -128,6 +128,11 @@ func (c *Client) Show() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Client) ReloadTheme() error {
|
||||||
|
_, err := c.sendRequest(ReloadThemePath)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Client) Quit() error {
|
func (c *Client) Quit() error {
|
||||||
_, err := c.sendRequest(QuitPath)
|
_, err := c.sendRequest(QuitPath)
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -47,10 +47,11 @@ type serverImpl struct {
|
|||||||
sm ServerManager
|
sm ServerManager
|
||||||
showFn func()
|
showFn func()
|
||||||
quitFn func()
|
quitFn func()
|
||||||
|
reloadThemeFn func()
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewServer(pbHandler PlaybackHandler, rateFn func(int), sm ServerManager, showFn, quitFn func()) IPCServer {
|
func NewServer(pbHandler PlaybackHandler, rateFn func(int), sm ServerManager, showFn, quitFn, reloadThemeFn func()) IPCServer {
|
||||||
s := &serverImpl{pbHandler: pbHandler, rateFn: rateFn, sm: sm, showFn: showFn, quitFn: quitFn}
|
s := &serverImpl{pbHandler: pbHandler, rateFn: rateFn, sm: sm, showFn: showFn, quitFn: quitFn, reloadThemeFn: reloadThemeFn}
|
||||||
s.server = &http.Server{
|
s.server = &http.Server{
|
||||||
Handler: s.createHandler(),
|
Handler: s.createHandler(),
|
||||||
}
|
}
|
||||||
@@ -77,6 +78,7 @@ func (s *serverImpl) createHandler() http.Handler {
|
|||||||
m.HandleFunc(ShowPath, s.makeSimpleEndpointHandler(func() {
|
m.HandleFunc(ShowPath, s.makeSimpleEndpointHandler(func() {
|
||||||
s.showFn()
|
s.showFn()
|
||||||
}))
|
}))
|
||||||
|
m.HandleFunc(ReloadThemePath, s.makeSimpleEndpointHandler(s.reloadThemeFn))
|
||||||
m.HandleFunc(QuitPath, s.makeSimpleEndpointHandler(func() {
|
m.HandleFunc(QuitPath, s.makeSimpleEndpointHandler(func() {
|
||||||
s.quitFn()
|
s.quitFn()
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/dweymouth/go-jellyfin"
|
"github.com/dweymouth/go-jellyfin"
|
||||||
@@ -179,6 +180,14 @@ func (s *ServerManager) connect(connection ServerConnection, password string) (m
|
|||||||
var cli, altCli mediaprovider.Server
|
var cli, altCli mediaprovider.Server
|
||||||
timeout := time.Second * time.Duration(s.config.Application.RequestTimeoutSeconds)
|
timeout := time.Second * time.Duration(s.config.Application.RequestTimeoutSeconds)
|
||||||
|
|
||||||
|
if connection.ServerType == ServerTypeJellyfin {
|
||||||
|
connection.Hostname = NormalizeJellyfinURL(connection.Hostname)
|
||||||
|
connection.AltHostname = NormalizeJellyfinURL(connection.AltHostname)
|
||||||
|
} else {
|
||||||
|
connection.Hostname = NormalizeServerURL(connection.Hostname)
|
||||||
|
connection.AltHostname = NormalizeServerURL(connection.AltHostname)
|
||||||
|
}
|
||||||
|
|
||||||
if connection.ServerType == ServerTypeJellyfin {
|
if connection.ServerType == ServerTypeJellyfin {
|
||||||
client, err := jellyfin.NewClient(connection.Hostname, res.AppName, res.AppVersion, jellyfin.WithTimeout(timeout))
|
client, err := jellyfin.NewClient(connection.Hostname, res.AppName, res.AppVersion, jellyfin.WithTimeout(timeout))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -266,3 +275,28 @@ func (s *ServerManager) checkSetInsecureSkipVerify(skip bool, cli *http.Client)
|
|||||||
func (a *ServerManager) GetServer() mediaprovider.MediaProvider {
|
func (a *ServerManager) GetServer() mediaprovider.MediaProvider {
|
||||||
return a.Server
|
return a.Server
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NormalizeServerURL applies common normalization to a server URL:
|
||||||
|
// prepends "http://" if no scheme is present, then strips trailing slashes.
|
||||||
|
func NormalizeServerURL(rawURL string) string {
|
||||||
|
if rawURL == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if !strings.Contains(rawURL, "://") {
|
||||||
|
rawURL = "http://" + rawURL
|
||||||
|
}
|
||||||
|
rawURL = strings.TrimRight(rawURL, "/")
|
||||||
|
return rawURL
|
||||||
|
}
|
||||||
|
|
||||||
|
// NormalizeJellyfinURL applies common normalization then additionally strips
|
||||||
|
// known Jellyfin web UI path suffixes (/web/index.html and /web).
|
||||||
|
func NormalizeJellyfinURL(rawURL string) string {
|
||||||
|
rawURL = NormalizeServerURL(rawURL)
|
||||||
|
if strings.HasSuffix(rawURL, "/web/index.html") {
|
||||||
|
rawURL = strings.TrimSuffix(rawURL, "/web/index.html")
|
||||||
|
} else if strings.HasSuffix(rawURL, "/web") {
|
||||||
|
rawURL = strings.TrimSuffix(rawURL, "/web")
|
||||||
|
}
|
||||||
|
return rawURL
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package backend
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestNormalizeServerURL(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
input string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{"", ""},
|
||||||
|
{"http://192.168.1.1:8096", "http://192.168.1.1:8096"},
|
||||||
|
{"https://music.example.com", "https://music.example.com"},
|
||||||
|
{"192.168.1.1:4533", "http://192.168.1.1:4533"},
|
||||||
|
{"music.example.com", "http://music.example.com"},
|
||||||
|
{"http://192.168.1.1:8096/", "http://192.168.1.1:8096"},
|
||||||
|
{"http://192.168.1.1:8096///", "http://192.168.1.1:8096"},
|
||||||
|
{"192.168.1.1:8096/", "http://192.168.1.1:8096"},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
got := NormalizeServerURL(tt.input)
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("NormalizeServerURL(%q) = %q, want %q", tt.input, got, tt.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormalizeJellyfinURL(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
input string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{"", ""},
|
||||||
|
{"http://192.168.1.1:8096", "http://192.168.1.1:8096"},
|
||||||
|
{"192.168.1.1:8096", "http://192.168.1.1:8096"},
|
||||||
|
{"192.168.1.1:8096/", "http://192.168.1.1:8096"},
|
||||||
|
{"192.168.1.1:8096/web/index.html", "http://192.168.1.1:8096"},
|
||||||
|
{"http://192.168.1.1:8096/web/index.html", "http://192.168.1.1:8096"},
|
||||||
|
{"http://192.168.1.1:8096/web/", "http://192.168.1.1:8096"},
|
||||||
|
{"http://192.168.1.1:8096/web", "http://192.168.1.1:8096"},
|
||||||
|
{"https://jellyfin.example.com/web/index.html", "https://jellyfin.example.com"},
|
||||||
|
{"https://jellyfin.example.com/web/", "https://jellyfin.example.com"},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
got := NormalizeJellyfinURL(tt.input)
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("NormalizeJellyfinURL(%q) = %q, want %q", tt.input, got, tt.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -110,6 +110,7 @@ func main() {
|
|||||||
mainWindow.Window.SetMaster()
|
mainWindow.Window.SetMaster()
|
||||||
myApp.OnReactivate = util.FyneDoFunc(mainWindow.Show)
|
myApp.OnReactivate = util.FyneDoFunc(mainWindow.Show)
|
||||||
myApp.OnExit = util.FyneDoFunc(mainWindow.Quit)
|
myApp.OnExit = util.FyneDoFunc(mainWindow.Quit)
|
||||||
|
myApp.OnReloadTheme = util.FyneDoFunc(mainWindow.ReloadTheme)
|
||||||
|
|
||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" {
|
||||||
windowStartupTasks := sync.OnceFunc(func() {
|
windowStartupTasks := sync.OnceFunc(func() {
|
||||||
|
|||||||
@@ -273,6 +273,11 @@ func (m *MainWindow) setInitialSize() {
|
|||||||
m.Window.Resize(m.DesiredSize())
|
m.Window.Resize(m.DesiredSize())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *MainWindow) ReloadTheme() {
|
||||||
|
m.theme.ReloadThemeFile()
|
||||||
|
fyne.CurrentApp().Settings().SetTheme(m.theme)
|
||||||
|
}
|
||||||
|
|
||||||
func (m *MainWindow) StartupPage() controller.Route {
|
func (m *MainWindow) StartupPage() controller.Route {
|
||||||
switch m.App.Config.Application.StartupPage {
|
switch m.App.Config.Application.StartupPage {
|
||||||
case "Artists":
|
case "Artists":
|
||||||
|
|||||||
@@ -110,6 +110,11 @@ func NewMyTheme(config *backend.ThemeConfig, themeFileDir string) *MyTheme {
|
|||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ReloadThemeFile reloads the currently loaded theme file.
|
||||||
|
func (m *MyTheme) ReloadThemeFile() {
|
||||||
|
m.loadedThemeFile = nil
|
||||||
|
}
|
||||||
|
|
||||||
func (m *MyTheme) Color(name fyne.ThemeColorName, defVariant fyne.ThemeVariant) color.Color {
|
func (m *MyTheme) Color(name fyne.ThemeColorName, defVariant fyne.ThemeVariant) color.Color {
|
||||||
// load theme file if necessary
|
// load theme file if necessary
|
||||||
if m.loadedThemeFile == nil || m.config.ThemeFile != m.loadedThemeFilename {
|
if m.loadedThemeFile == nil || m.config.ThemeFile != m.loadedThemeFilename {
|
||||||
|
|||||||
Reference in New Issue
Block a user