Merge branch 'main' into feature/shuffle

This commit is contained in:
Siiiiinth
2026-02-20 15:18:22 +01:00
11 changed files with 132 additions and 15 deletions
+12 -1
View File
@@ -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:
+1
View File
@@ -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")
+7 -4
View File
@@ -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
+1
View File
@@ -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>
) )
+5
View File
@@ -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
+4 -2
View File
@@ -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()
})) }))
+34
View File
@@ -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
}
+49
View File
@@ -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)
}
}
}
+1
View File
@@ -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() {
+5
View File
@@ -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":
+5
View File
@@ -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 {