Skip to content

Commit 9f559db

Browse files
authored
Merge branch 'dweymouth:main' into main
2 parents cd2dbe1 + 826f72d commit 9f559db

43 files changed

Lines changed: 1049 additions & 115 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/build-windows.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,8 @@ jobs:
4545

4646
- name: Download mpv dll
4747
run: >
48-
wget https://github.com/shinchiro/mpv-winbuild-cmake/releases/download/20250706/mpv-dev-x86_64-v3-20250706-git-db7bc59.7z &&
49-
7z x mpv-dev-x86_64-v3-20250706-git-db7bc59.7z
48+
wget https://github.com/shinchiro/mpv-winbuild-cmake/releases/download/20250810/mpv-dev-x86_64-v3-20250810-git-01b7edc.7z &&
49+
7z x mpv-dev-x86_64-v3-20250810-git-01b7edc.7z
5050
5151
- name: Download smtc dll
5252
run: >

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
11
# Change Log
22

3+
## [0.18.0]
4+
5+
### Added
6+
- [#665](https://github.com/dweymouth/supersonic/issues/665) Make play button on Albums page configurable to shuffle or play in order
7+
- [#668](https://github.com/dweymouth/supersonic/issues/668) Add sample rate, bit depth, and channel count to track info dialog
8+
- [#669](https://github.com/dweymouth/supersonic/pull/669) Add waveform seekbar
9+
- [#671](https://github.com/dweymouth/supersonic/pull/671) Add CLI commands to start minimized, and to show/raise the app window
10+
- [#674](https://github.com/dweymouth/supersonic/pull/674) Add CLI commands to search and play albums, tracks, and playlists
11+
- [#679](https://github.com/dweymouth/supersonic/pull/679) Add player control buttons to Windows taskbar thumbnail
12+
- [#681](https://github.com/dweymouth/supersonic/pull/681) Add ability to stop playback after current track finishes
13+
314
## [0.17.0]
415

516
### Added

FyneApp.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[Details]
22
Icon = "res/appicon-512.png"
33
Name = "Supersonic"
4-
Version = "0.17.0"
4+
Version = "0.18.0"
55

66
[LinuxAndBSD]
77
Categories = ["Audio", "AudioVideo"]

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ Supersonic supports any music server with a Subsonic (or OpenSubsonic) API, or J
4242
* [x] Light and Dark themes, with optional auto theme switching
4343
* [x] High-quality gapless audio playback powered by MPV, with optional audio exclusive mode
4444
* [x] ReplayGain support (depends on files being tagged on server)
45+
* [x] Waveform seekbar
4546
* [x] [Custom themes](https://github.com/dweymouth/supersonic/wiki/Custom-Themes)
4647
* [x] MPRIS and Mac OS media center integration for media key and desktop control
4748
* [x] Built-in 15-band graphic equalizer

backend/app.go

Lines changed: 63 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"github.com/dweymouth/supersonic/backend/player"
2121
"github.com/dweymouth/supersonic/backend/player/mpv"
2222
"github.com/dweymouth/supersonic/backend/util"
23+
"github.com/dweymouth/supersonic/backend/windows"
2324
"github.com/google/uuid"
2425

2526
"github.com/20after4/configdir"
@@ -49,7 +50,7 @@ type App struct {
4950
LocalPlayer *mpv.Player
5051
UpdateChecker UpdateChecker
5152
MPRISHandler *MPRISHandler
52-
WinSMTC *SMTC
53+
WinSMTC *windows.SMTC
5354
ipcServer ipc.IPCServer
5455
LrcLibFetcher *LrcLibFetcher
5556

@@ -192,7 +193,10 @@ func StartupApp(appName, displayAppName, appVersion, appVersionTag, latestReleas
192193
ipc.DestroyConn() // cleanup socket possibly orphaned by crashed process
193194
listener, err := ipc.Listen()
194195
if err == nil {
195-
a.ipcServer = ipc.NewServer(a.PlaybackManager, a.callOnReactivate,
196+
a.ipcServer = ipc.NewServer(
197+
a.PlaybackManager,
198+
a.ServerManager,
199+
a.callOnReactivate,
196200
func() { _ = a.callOnExit() })
197201
go a.ipcServer.Serve(listener)
198202
} else {
@@ -378,25 +382,25 @@ func (a *App) setupMPRIS(mprisAppName string) {
378382
}
379383

380384
func (a *App) SetupWindowsSMTC(hwnd uintptr) {
381-
smtc, err := InitSMTCForWindow(hwnd)
385+
smtc, err := windows.InitSMTCForWindow(hwnd)
382386
if err != nil {
383387
log.Printf("error initializing SMTC: %d", err)
384388
return
385389
}
386390
a.WinSMTC = smtc
387391
smtc.UpdateMetadata(a.displayAppName, "")
388392

389-
smtc.OnButtonPressed(func(btn SMTCButton) {
393+
smtc.OnButtonPressed(func(btn windows.SMTCButton) {
390394
switch btn {
391-
case SMTCButtonPlay:
395+
case windows.SMTCButtonPlay:
392396
a.PlaybackManager.Continue()
393-
case SMTCButtonPause:
397+
case windows.SMTCButtonPause:
394398
a.PlaybackManager.Pause()
395-
case SMTCButtonNext:
399+
case windows.SMTCButtonNext:
396400
a.PlaybackManager.SeekNext()
397-
case SMTCButtonPrevious:
401+
case windows.SMTCButtonPrevious:
398402
a.PlaybackManager.SeekBackOrPrevious()
399-
case SMTCButtonStop:
403+
case windows.SMTCButtonStop:
400404
a.PlaybackManager.Stop()
401405
}
402406
})
@@ -425,18 +429,37 @@ func (a *App) SetupWindowsSMTC(hwnd uintptr) {
425429
})
426430
a.PlaybackManager.OnPlaying(func() {
427431
smtc.SetEnabled(true)
428-
smtc.UpdatePlaybackState(SMTCPlaybackStatePlaying)
432+
smtc.UpdatePlaybackState(windows.SMTCPlaybackStatePlaying)
429433
})
430434
a.PlaybackManager.OnPaused(func() {
431435
smtc.SetEnabled(true)
432-
smtc.UpdatePlaybackState(SMTCPlaybackStatePaused)
436+
smtc.UpdatePlaybackState(windows.SMTCPlaybackStatePaused)
433437
})
434438
a.PlaybackManager.OnStopped(func() {
435439
smtc.SetEnabled(false)
436-
smtc.UpdatePlaybackState(SMTCPlaybackStateStopped)
440+
smtc.UpdatePlaybackState(windows.SMTCPlaybackStateStopped)
437441
})
438442
}
439443

444+
func (a *App) SetupWindowsTaskbarButtons(hwnd uintptr) {
445+
windows.InitializeTaskbarButtons(hwnd, func(btn windows.TaskbarButton) {
446+
switch btn {
447+
case windows.TaskbarButtonPrevious:
448+
a.PlaybackManager.SeekBackOrPrevious()
449+
case windows.TaskbarButtonPlayPause:
450+
a.PlaybackManager.PlayPause()
451+
case windows.TaskbarButtonNext:
452+
a.PlaybackManager.SeekNext()
453+
}
454+
})
455+
a.PlaybackManager.OnPlaying(func() {
456+
windows.SetTaskbarButtonIsPlaying(true)
457+
})
458+
f := func() { windows.SetTaskbarButtonIsPlaying(false) }
459+
a.PlaybackManager.OnPaused(f)
460+
a.PlaybackManager.OnStopped(f)
461+
}
462+
440463
func (a *App) LoginToDefaultServer() error {
441464
serverCfg := a.ServerManager.GetDefaultServer()
442465
if serverCfg == nil {
@@ -548,6 +571,10 @@ func (a *App) checkFlagsAndSendIPCMsg(cli *ipc.Client) error {
548571
return cli.SeekBackOrPrevious()
549572
case *FlagNext:
550573
return cli.SeekNext()
574+
case *FlagStop:
575+
return cli.Stop()
576+
case *FlagStopAfterCurrent:
577+
return cli.StopAfterCurrent()
551578
case *FlagShow:
552579
return cli.Show()
553580
case VolumeCLIArg >= 0:
@@ -558,6 +585,30 @@ func (a *App) checkFlagsAndSendIPCMsg(cli *ipc.Client) error {
558585
return cli.SeekSeconds(SeekToCLIArg)
559586
case SeekByCLIArg != 0:
560587
return cli.SeekBySeconds(SeekByCLIArg)
588+
case PlayAlbumCLIArg != "":
589+
return cli.PlayAlbum(PlayAlbumCLIArg, FirstTrackCLIArg, *FlagShuffle)
590+
case PlayPlaylistCLIArg != "":
591+
return cli.PlayPlaylist(PlayPlaylistCLIArg, FirstTrackCLIArg, *FlagShuffle)
592+
case PlayTrackCLIArg != "":
593+
return cli.PlayTrack(PlayTrackCLIArg)
594+
case SearchAlbumCLIArg != "":
595+
data, err := cli.SearchAlbum(SearchAlbumCLIArg)
596+
if err == nil {
597+
fmt.Println(data)
598+
}
599+
return err
600+
case SearchPlaylistCLIArg != "":
601+
data, err := cli.SearchPlaylist(SearchPlaylistCLIArg)
602+
if err == nil {
603+
fmt.Println(data)
604+
}
605+
return err
606+
case SearchTrackCLIArg != "":
607+
data, err := cli.SearchTrack(SearchTrackCLIArg)
608+
if err == nil {
609+
fmt.Println(data)
610+
}
611+
return err
561612
default:
562613
return nil
563614
}

backend/cmdlineoptions.go

Lines changed: 68 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,42 @@ package backend
22

33
import (
44
"flag"
5+
"os"
56
"strconv"
67
"strings"
8+
9+
"golang.org/x/term"
710
)
811

912
var (
10-
VolumeCLIArg int = -1
11-
SeekToCLIArg float64 = -1
12-
SeekByCLIArg float64 = 0
13-
VolumePctCLIArg float64 = 0
14-
15-
FlagPlay = flag.Bool("play", false, "unpause or begin playback")
16-
FlagPause = flag.Bool("pause", false, "pause playback")
17-
FlagPlayPause = flag.Bool("play-pause", false, "toggle play/pause state")
18-
FlagPrevious = flag.Bool("previous", false, "seek to previous track or beginning of current")
19-
FlagNext = flag.Bool("next", false, "seek to next track")
20-
FlagStartMinimized = flag.Bool("start-minimized", false, "start app minimized")
21-
FlagShow = flag.Bool("show", false, "show minimized app")
22-
FlagVersion = flag.Bool("version", false, "print app version and exit")
23-
FlagHelp = flag.Bool("help", false, "print command line options and exit")
13+
VolumeCLIArg int = -1
14+
SeekToCLIArg float64 = -1
15+
SeekByCLIArg float64 = 0
16+
VolumePctCLIArg float64 = 0
17+
PlayAlbumCLIArg string = ""
18+
PlayPlaylistCLIArg string = ""
19+
PlayTrackCLIArg string = ""
20+
FirstTrackCLIArg int = 0
21+
SearchAlbumCLIArg string = ""
22+
SearchPlaylistCLIArg string = ""
23+
SearchTrackCLIArg string = ""
24+
25+
FlagPlay = flag.Bool("play", false, "unpause or begin playback")
26+
FlagPause = flag.Bool("pause", false, "pause playback")
27+
FlagPlayPause = flag.Bool("play-pause", false, "toggle play/pause state")
28+
FlagPrevious = flag.Bool("previous", false, "seek to previous track or beginning of current")
29+
FlagNext = flag.Bool("next", false, "seek to next track")
30+
FlagStop = flag.Bool("stop", false, "stop playback")
31+
FlagStopAfterCurrent = flag.Bool("stop-after-current", false, "stop playback after current track")
32+
FlagStartMinimized = flag.Bool("start-minimized", false, "start app minimized")
33+
FlagShow = flag.Bool("show", false, "show minimized app")
34+
FlagShuffle = flag.Bool("shuffle", false, "shuffle the tracklist (to be used with either -play-album-by-id or -play-playlist-by-id)")
35+
FlagVersion = flag.Bool("version", false, "print app version and exit")
36+
FlagHelp = flag.Bool("help", false, "print command line options and exit")
37+
38+
FlagPlayAlbum *bool
39+
FlagPlayPlaylist *bool
40+
FlagPlayTrack *bool
2441
)
2542

2643
func init() {
@@ -48,6 +65,43 @@ func init() {
4865
VolumePctCLIArg = v
4966
return err
5067
})
68+
69+
if term.IsTerminal(int(os.Stdin.Fd())) {
70+
flag.Func("play-album-by-id", "start playing the album with the given ID (can also be passed from standard input)", func(s string) error {
71+
PlayAlbumCLIArg = s
72+
return nil
73+
})
74+
flag.Func("play-playlist-by-id", "start playing the playlist with the given ID (can also be passed from standard input)", func(s string) error {
75+
PlayPlaylistCLIArg = s
76+
return nil
77+
})
78+
flag.Func("play-track-by-id", "start playing the track with the given ID (can also be passed from standard input)", func(s string) error {
79+
PlayTrackCLIArg = s
80+
return nil
81+
})
82+
} else {
83+
FlagPlayAlbum = flag.Bool("play-album-by-id", false, "")
84+
FlagPlayPlaylist = flag.Bool("play-playlist-by-id", false, "")
85+
FlagPlayTrack = flag.Bool("play-track-by-id", false, "")
86+
}
87+
flag.Func("first-track", "start playing from given track (positive integer, to be used with either -play-album or -play-playlist)", func(s string) error {
88+
v, err := strconv.Atoi(s)
89+
FirstTrackCLIArg = v
90+
return err
91+
})
92+
93+
flag.Func("search-album", "search album by name, return results as JSON", func(s string) error {
94+
SearchAlbumCLIArg = s
95+
return nil
96+
})
97+
flag.Func("search-playlist", "search playlist by name, return results as JSON", func(s string) error {
98+
SearchPlaylistCLIArg = s
99+
return nil
100+
})
101+
flag.Func("search-track", "search track by name, return results as JSON", func(s string) error {
102+
SearchTrackCLIArg = s
103+
return nil
104+
})
51105
}
52106

53107
func HaveCommandLineOptions() bool {

backend/config.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,8 @@ type AlbumPageConfig struct {
7070
type AlbumsPageConfig struct {
7171
SortOrder string // only relevant for Albums page
7272
ShowYears bool
73-
ShuffleMode string // only relevant for genre page
73+
ShuffleMode string // only relevant for Genre page
74+
PlayInOrder bool // only relevant for Albums page
7475
}
7576

7677
type ArtistPageConfig struct {

backend/ipc/api.go

Lines changed: 54 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,37 @@
11
package ipc
22

3-
import "fmt"
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"net/url"
7+
)
48

59
const (
6-
PingPath = "/ping"
7-
PlayPath = "/transport/play"
8-
PlayPausePath = "/transport/playpause"
9-
PausePath = "/transport/pause"
10-
StopPath = "/transport/stop"
11-
PreviousPath = "/transport/previous"
12-
NextPath = "/transport/next"
13-
TimePosPath = "/transport/timepos" // ?s=<seconds>
14-
SeekByPath = "/transport/seek-by" // ?s=<+/- seconds>
15-
VolumePath = "/volume" // ?v=<vol>
16-
VolumeAdjustPath = "/volume/adjust" // ?pct=<+/- percentage>
17-
ShowPath = "/window/show"
18-
QuitPath = "/window/quit"
10+
PingPath = "/ping"
11+
PlayPath = "/transport/play"
12+
PlayAlbumPath = "/transport/play-album" // ?id=<album ID>&t=<firstTrack>&s=<shuffle>
13+
PlayPlaylistPath = "/transport/play-playlist" // ?id=<playlist ID>&t=<firstTrack>&s=<shuffle>
14+
PlayTrackPath = "/transport/play-track" // ?id=<track ID>
15+
SearchAlbumPath = "/transport/search-album" // ?s=<searchQuery>
16+
SearchPlaylistPath = "/transport/search-playlist" // ?s=<searchQuery>
17+
SearchTrackPath = "/transport/search-track" // ?s=<searchQuery>
18+
PlayPausePath = "/transport/playpause"
19+
PausePath = "/transport/pause"
20+
StopPath = "/transport/stop"
21+
StopAfterCurrentPath = "/transport/stop-after-current"
22+
PreviousPath = "/transport/previous"
23+
NextPath = "/transport/next"
24+
TimePosPath = "/transport/timepos" // ?s=<seconds>
25+
SeekByPath = "/transport/seek-by" // ?s=<+/- seconds>
26+
VolumePath = "/volume" // ?v=<vol>
27+
VolumeAdjustPath = "/volume/adjust" // ?pct=<+/- percentage>
28+
ShowPath = "/window/show"
29+
QuitPath = "/window/quit"
1930
)
2031

2132
type Response struct {
22-
Error string `json:"error"`
33+
Data json.RawMessage `json:"data"`
34+
Error string `json:"error"`
2335
}
2436

2537
func SetVolumePath(vol int) string {
@@ -37,3 +49,30 @@ func SeekToSecondsPath(secs float64) string {
3749
func SeekBySecondsPath(secs float64) string {
3850
return fmt.Sprintf("%s?s=%0.2f", SeekByPath, secs)
3951
}
52+
53+
func BuildPlayAlbumPath(id string, firstTrack int, shuffle bool) string {
54+
return fmt.Sprintf("%s?id=%s&t=%d&s=%t", PlayAlbumPath, id, firstTrack, shuffle)
55+
}
56+
57+
func BuildPlayPlaylistPath(id string, firstTrack int, shuffle bool) string {
58+
return fmt.Sprintf("%s?id=%s&t=%d&s=%t", PlayPlaylistPath, id, firstTrack, shuffle)
59+
}
60+
61+
func BuildPlayTrackPath(id string) string {
62+
return fmt.Sprintf("%s?id=%s", PlayTrackPath, id)
63+
}
64+
65+
func BuildSearchAlbumPath(search string) string {
66+
s := url.QueryEscape(search)
67+
return fmt.Sprintf("%s?s=%s", SearchAlbumPath, s)
68+
}
69+
70+
func BuildSearchPlaylistPath(search string) string {
71+
s := url.QueryEscape(search)
72+
return fmt.Sprintf("%s?s=%s", SearchPlaylistPath, s)
73+
}
74+
75+
func BuildSearchTrackPath(search string) string {
76+
s := url.QueryEscape(search)
77+
return fmt.Sprintf("%s?s=%s", SearchTrackPath, s)
78+
}

0 commit comments

Comments
 (0)