Merge pull request #219 from dweymouth/feature/mpris
Add MPRIS integration
This commit is contained in:
+33
-5
@@ -37,7 +37,11 @@ type App struct {
|
|||||||
PlaybackManager *PlaybackManager
|
PlaybackManager *PlaybackManager
|
||||||
Player *player.Player
|
Player *player.Player
|
||||||
UpdateChecker UpdateChecker
|
UpdateChecker UpdateChecker
|
||||||
OnReactivate func()
|
MPRISHandler *MPRISHandler
|
||||||
|
|
||||||
|
// UI callbacks to be set in main
|
||||||
|
OnReactivate func()
|
||||||
|
OnExit func()
|
||||||
|
|
||||||
appName string
|
appName string
|
||||||
appVersionTag string
|
appVersionTag string
|
||||||
@@ -50,7 +54,7 @@ func (a *App) VersionTag() string {
|
|||||||
return a.appVersionTag
|
return a.appVersionTag
|
||||||
}
|
}
|
||||||
|
|
||||||
func StartupApp(appName, appVersionTag, configFile, latestReleaseURL string) (*App, error) {
|
func StartupApp(appName, displayAppName, appVersionTag, configFile, latestReleaseURL string) (*App, error) {
|
||||||
sessionPath := configdir.LocalConfig(appName, sessionDir)
|
sessionPath := configdir.LocalConfig(appName, sessionDir)
|
||||||
if _, err := os.Stat(path.Join(sessionPath, sessionLockFile)); err == nil {
|
if _, err := os.Stat(path.Join(sessionPath, sessionLockFile)); err == nil {
|
||||||
log.Println("Another instance is running. Reactivating it...")
|
log.Println("Another instance is running. Reactivating it...")
|
||||||
@@ -103,6 +107,8 @@ func StartupApp(appName, appVersionTag, configFile, latestReleaseURL string) (*A
|
|||||||
_, _ = a.ImageManager.GetCoverThumbnail(coverID)
|
_, _ = a.ImageManager.GetCoverThumbnail(coverID)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
a.setupMPRIS(displayAppName)
|
||||||
|
|
||||||
return a, nil
|
return a, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,9 +140,7 @@ func (a *App) startSessionWatcher(sessionPath string) {
|
|||||||
activatePath := path.Join(sessionPath, sessionActivateFile)
|
activatePath := path.Join(sessionPath, sessionActivateFile)
|
||||||
if _, err := os.Stat(activatePath); err == nil {
|
if _, err := os.Stat(activatePath); err == nil {
|
||||||
os.Remove(path.Join(sessionPath, sessionActivateFile))
|
os.Remove(path.Join(sessionPath, sessionActivateFile))
|
||||||
if a.OnReactivate != nil {
|
a.callOnReactivate()
|
||||||
a.OnReactivate()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -144,6 +148,12 @@ func (a *App) startSessionWatcher(sessionPath string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *App) callOnReactivate() {
|
||||||
|
if a.OnReactivate != nil {
|
||||||
|
a.OnReactivate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (a *App) initMPV() error {
|
func (a *App) initMPV() error {
|
||||||
p := player.NewWithClientName(a.appName)
|
p := player.NewWithClientName(a.appName)
|
||||||
c := a.Config.LocalPlayback
|
c := a.Config.LocalPlayback
|
||||||
@@ -202,6 +212,23 @@ func (a *App) setupMPV() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *App) setupMPRIS(mprisAppName string) {
|
||||||
|
a.MPRISHandler = NewMPRISHandler(mprisAppName, a.Player, a.PlaybackManager)
|
||||||
|
a.MPRISHandler.ArtURLLookup = a.ImageManager.GetCoverArtUrl
|
||||||
|
a.MPRISHandler.OnRaise = func() error { a.callOnReactivate(); return nil }
|
||||||
|
a.MPRISHandler.OnQuit = func() error {
|
||||||
|
if a.OnExit == nil {
|
||||||
|
return errors.New("no quit handler registered")
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
time.Sleep(10 * time.Millisecond)
|
||||||
|
a.OnExit()
|
||||||
|
}()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
a.MPRISHandler.Start()
|
||||||
|
}
|
||||||
|
|
||||||
func (a *App) LoginToDefaultServer(string) error {
|
func (a *App) LoginToDefaultServer(string) error {
|
||||||
serverCfg := a.ServerManager.GetDefaultServer()
|
serverCfg := a.ServerManager.GetDefaultServer()
|
||||||
if serverCfg == nil {
|
if serverCfg == nil {
|
||||||
@@ -221,6 +248,7 @@ func (a *App) DeleteServerCacheDir(serverID uuid.UUID) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) Shutdown() {
|
func (a *App) Shutdown() {
|
||||||
|
a.MPRISHandler.Shutdown()
|
||||||
a.PlaybackManager.DisableCallbacks()
|
a.PlaybackManager.DisableCallbacks()
|
||||||
a.Player.Stop() // will trigger scrobble check
|
a.Player.Stop() // will trigger scrobble check
|
||||||
a.Config.LocalPlayback.Volume = a.Player.GetVolume()
|
a.Config.LocalPlayback.Volume = a.Player.GetVolume()
|
||||||
|
|||||||
@@ -88,6 +88,16 @@ func (i *ImageManager) GetFullSizeCoverArt(coverID string) (image.Image, error)
|
|||||||
return im, nil
|
return im, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (i *ImageManager) GetCoverArtUrl(coverID string) (string, error) {
|
||||||
|
path := i.filePathForCover(coverID)
|
||||||
|
if _, err := os.Stat(path); err == nil {
|
||||||
|
// this is probably broken for Windows but it's currently only used
|
||||||
|
// for MPRIS, so we are OK for now
|
||||||
|
return fmt.Sprintf("file://%s", path), nil
|
||||||
|
}
|
||||||
|
return "", errors.New("cover not found")
|
||||||
|
}
|
||||||
|
|
||||||
func (i *ImageManager) GetCachedArtistImage(artistID string) (image.Image, bool) {
|
func (i *ImageManager) GetCachedArtistImage(artistID string) (image.Image, bool) {
|
||||||
return i.loadLocalImage(i.filePathForArtistImage(artistID))
|
return i.loadLocalImage(i.filePathForArtistImage(artistID))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,303 @@
|
|||||||
|
package backend
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/dweymouth/supersonic/backend/mediaprovider"
|
||||||
|
"github.com/dweymouth/supersonic/player"
|
||||||
|
"github.com/godbus/dbus/v5"
|
||||||
|
"github.com/quarckster/go-mpris-server/pkg/events"
|
||||||
|
"github.com/quarckster/go-mpris-server/pkg/server"
|
||||||
|
"github.com/quarckster/go-mpris-server/pkg/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
const dbusTrackIDPrefix = "/Supersonic/Track/"
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ types.OrgMprisMediaPlayer2Adapter = (*MPRISHandler)(nil)
|
||||||
|
_ types.OrgMprisMediaPlayer2PlayerAdapter = (*MPRISHandler)(nil)
|
||||||
|
_ types.OrgMprisMediaPlayer2PlayerAdapterLoopStatus = (*MPRISHandler)(nil)
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
errNotImplemented = errors.New("not implemented")
|
||||||
|
)
|
||||||
|
|
||||||
|
type MPRISHandler struct {
|
||||||
|
// Function called if the player is requested to quit through MPRIS.
|
||||||
|
// Should *asynchronously* start shutdown and return immediately true if a shutdown will happen.
|
||||||
|
OnQuit func() error
|
||||||
|
|
||||||
|
// Function called if the player is requested to bring its UI to the front.
|
||||||
|
OnRaise func() error
|
||||||
|
|
||||||
|
// Function to look up the artwork URL for a given track ID
|
||||||
|
ArtURLLookup func(trackID string) (string, error)
|
||||||
|
|
||||||
|
connErr error
|
||||||
|
playerName string
|
||||||
|
p *player.Player
|
||||||
|
pm *PlaybackManager
|
||||||
|
s *server.Server
|
||||||
|
evt *events.EventHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMPRISHandler(playerName string, p *player.Player, pm *PlaybackManager) *MPRISHandler {
|
||||||
|
m := &MPRISHandler{playerName: playerName, p: p, pm: pm, connErr: errors.New("not started")}
|
||||||
|
m.s = server.NewServer(playerName, m, m)
|
||||||
|
m.evt = events.NewEventHandler(m.s)
|
||||||
|
|
||||||
|
m.p.OnSeek(func() {
|
||||||
|
if m.connErr == nil {
|
||||||
|
pos := secondsToMicroseconds(m.p.GetStatus().TimePos)
|
||||||
|
m.evt.Player.OnSeek(pos)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
m.pm.OnSongChange(func(_, _ *mediaprovider.Track) {
|
||||||
|
if m.connErr == nil {
|
||||||
|
m.evt.Player.OnTitle()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
m.pm.OnVolumeChange(func(vol int) {
|
||||||
|
if m.connErr == nil {
|
||||||
|
m.evt.Player.OnVolume()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
emitPlayStatus := func() {
|
||||||
|
if m.connErr == nil {
|
||||||
|
m.evt.Player.OnPlayPause()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.p.OnStopped(emitPlayStatus)
|
||||||
|
m.p.OnPlaying(emitPlayStatus)
|
||||||
|
m.p.OnPaused(emitPlayStatus)
|
||||||
|
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
// Starts listening for MPRIS events.
|
||||||
|
func (m *MPRISHandler) Start() {
|
||||||
|
m.connErr = nil
|
||||||
|
go func() {
|
||||||
|
// exits early with err if unable to establish D-Bus connection
|
||||||
|
m.connErr = m.s.Listen()
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stops listening for MPRIS events and releases any D-Bus resources.
|
||||||
|
func (m *MPRISHandler) Shutdown() {
|
||||||
|
if m.connErr == nil {
|
||||||
|
m.s.Stop()
|
||||||
|
m.connErr = errors.New("stopped")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// OrgMprisMediaPlayer2Adapter implementation
|
||||||
|
|
||||||
|
func (m *MPRISHandler) Identity() (string, error) {
|
||||||
|
return m.playerName, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MPRISHandler) CanQuit() (bool, error) {
|
||||||
|
return m.OnQuit != nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MPRISHandler) Quit() error {
|
||||||
|
if m.OnQuit != nil {
|
||||||
|
return m.OnQuit()
|
||||||
|
}
|
||||||
|
return errors.New("no quit handler added")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MPRISHandler) CanRaise() (bool, error) {
|
||||||
|
return m.OnRaise != nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MPRISHandler) Raise() error {
|
||||||
|
if m.OnRaise != nil {
|
||||||
|
return m.OnRaise()
|
||||||
|
}
|
||||||
|
return errors.New("no raise handler added")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MPRISHandler) HasTrackList() (bool, error) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MPRISHandler) SupportedUriSchemes() ([]string, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MPRISHandler) SupportedMimeTypes() ([]string, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// OrgMprisMediaPlayer2PlayerAdapter implementation
|
||||||
|
|
||||||
|
func (m *MPRISHandler) Next() error {
|
||||||
|
return m.p.SeekNext()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MPRISHandler) Previous() error {
|
||||||
|
return m.p.SeekBackOrPrevious()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MPRISHandler) Pause() error {
|
||||||
|
if m.p.GetStatus().State == player.Playing {
|
||||||
|
return m.p.PlayPause()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MPRISHandler) PlayPause() error {
|
||||||
|
return m.p.PlayPause()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MPRISHandler) Stop() error {
|
||||||
|
return m.p.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MPRISHandler) Play() error {
|
||||||
|
switch m.p.GetStatus().State {
|
||||||
|
case player.Paused:
|
||||||
|
return m.p.PlayPause()
|
||||||
|
case player.Stopped:
|
||||||
|
return m.p.PlayFromBeginning()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MPRISHandler) Seek(offset types.Microseconds) error {
|
||||||
|
return m.p.Seek(fmt.Sprintf("%0.2f", float64(offset)/1_000_000), player.SeekRelative)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MPRISHandler) SetPosition(trackId string, position types.Microseconds) error {
|
||||||
|
return errNotImplemented
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MPRISHandler) OpenUri(uri string) error {
|
||||||
|
return errNotImplemented
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MPRISHandler) PlaybackStatus() (types.PlaybackStatus, error) {
|
||||||
|
switch m.p.GetStatus().State {
|
||||||
|
case player.Playing:
|
||||||
|
return types.PlaybackStatusPlaying, nil
|
||||||
|
case player.Paused:
|
||||||
|
return types.PlaybackStatusPaused, nil
|
||||||
|
case player.Stopped:
|
||||||
|
return types.PlaybackStatusStopped, nil
|
||||||
|
}
|
||||||
|
return "", errors.New("unknown playback status")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MPRISHandler) LoopStatus() (types.LoopStatus, error) {
|
||||||
|
switch m.pm.LoopMode() {
|
||||||
|
case LoopModeAll:
|
||||||
|
return types.LoopStatusPlaylist, nil
|
||||||
|
case LoopModeOne:
|
||||||
|
return types.LoopStatusTrack, nil
|
||||||
|
case LoopModeNone:
|
||||||
|
return types.LoopStatusNone, nil
|
||||||
|
}
|
||||||
|
return "", errors.New("unknown loop status")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MPRISHandler) SetLoopStatus(status types.LoopStatus) error {
|
||||||
|
switch status {
|
||||||
|
case types.LoopStatusPlaylist:
|
||||||
|
return m.pm.SetLoopMode(LoopModeAll)
|
||||||
|
case types.LoopStatusTrack:
|
||||||
|
return m.pm.SetLoopMode(LoopModeOne)
|
||||||
|
case types.LoopStatusNone:
|
||||||
|
return m.pm.SetLoopMode(LoopModeNone)
|
||||||
|
}
|
||||||
|
return errors.New("unknown loop status")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MPRISHandler) Rate() (float64, error) {
|
||||||
|
return 1, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MPRISHandler) SetRate(float64) error {
|
||||||
|
return errNotImplemented
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MPRISHandler) Metadata() (types.Metadata, error) {
|
||||||
|
status := m.p.GetStatus()
|
||||||
|
var tr mediaprovider.Track
|
||||||
|
if np := m.pm.NowPlaying(); np != nil && status.State != player.Stopped {
|
||||||
|
tr = *np
|
||||||
|
}
|
||||||
|
var trackID string
|
||||||
|
if tr.ID != "" {
|
||||||
|
trackID = dbusTrackIDPrefix + tr.ID
|
||||||
|
}
|
||||||
|
var artURL string
|
||||||
|
if tr.ID != "" && m.ArtURLLookup != nil {
|
||||||
|
if u, err := m.ArtURLLookup(tr.CoverArtID); err == nil {
|
||||||
|
artURL = u
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return types.Metadata{
|
||||||
|
TrackId: dbus.ObjectPath(trackID),
|
||||||
|
Length: types.Microseconds(status.Duration),
|
||||||
|
Title: tr.Name,
|
||||||
|
Album: tr.Album,
|
||||||
|
Artist: tr.ArtistNames,
|
||||||
|
DiscNumber: tr.DiscNumber,
|
||||||
|
TrackNumber: tr.TrackNumber,
|
||||||
|
Genre: []string{tr.Genre},
|
||||||
|
UserRating: float64(tr.Rating) / 5,
|
||||||
|
ArtUrl: artURL,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MPRISHandler) Volume() (float64, error) {
|
||||||
|
return float64(m.p.GetVolume()) / 100, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MPRISHandler) SetVolume(v float64) error {
|
||||||
|
return m.pm.SetVolume(int(v * 100))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MPRISHandler) Position() (int64, error) {
|
||||||
|
return int64(secondsToMicroseconds(m.p.GetStatus().TimePos)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MPRISHandler) MinimumRate() (float64, error) {
|
||||||
|
return 1, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MPRISHandler) MaximumRate() (float64, error) {
|
||||||
|
return 1, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MPRISHandler) CanGoNext() (bool, error) {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MPRISHandler) CanGoPrevious() (bool, error) {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MPRISHandler) CanPlay() (bool, error) {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MPRISHandler) CanPause() (bool, error) {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MPRISHandler) CanSeek() (bool, error) {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MPRISHandler) CanControl() (bool, error) {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func secondsToMicroseconds(s float64) types.Microseconds {
|
||||||
|
return types.Microseconds(s * 1_000_000)
|
||||||
|
}
|
||||||
@@ -17,6 +17,14 @@ const (
|
|||||||
ReplayGainTrack = string(player.ReplayGainTrack)
|
ReplayGainTrack = string(player.ReplayGainTrack)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type LoopMode int
|
||||||
|
|
||||||
|
const (
|
||||||
|
LoopModeNone LoopMode = LoopMode(player.LoopNone)
|
||||||
|
LoopModeAll LoopMode = LoopMode(player.LoopAll)
|
||||||
|
LoopModeOne LoopMode = LoopMode(player.LoopOne)
|
||||||
|
)
|
||||||
|
|
||||||
// A high-level Subsonic-aware playback backend.
|
// A high-level Subsonic-aware playback backend.
|
||||||
// Manages loading tracks into the Player queue,
|
// Manages loading tracks into the Player queue,
|
||||||
// sending callbacks on play time updates and track changes.
|
// sending callbacks on play time updates and track changes.
|
||||||
@@ -40,7 +48,8 @@ type PlaybackManager struct {
|
|||||||
|
|
||||||
onSongChange []func(nowPlaying, justScrobbledIfAny *mediaprovider.Track)
|
onSongChange []func(nowPlaying, justScrobbledIfAny *mediaprovider.Track)
|
||||||
onPlayTimeUpdate []func(float64, float64)
|
onPlayTimeUpdate []func(float64, float64)
|
||||||
onLoopModeChange []func(string)
|
onLoopModeChange []func(LoopMode)
|
||||||
|
onVolumeChange []func(int)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewPlaybackManager(
|
func NewPlaybackManager(
|
||||||
@@ -130,10 +139,15 @@ func (p *PlaybackManager) OnPlayTimeUpdate(cb func(float64, float64)) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Registers a callback that is notified whenever the loop mode changes.
|
// Registers a callback that is notified whenever the loop mode changes.
|
||||||
func (p *PlaybackManager) OnLoopModeChange(cb func(string)) {
|
func (p *PlaybackManager) OnLoopModeChange(cb func(LoopMode)) {
|
||||||
p.onLoopModeChange = append(p.onLoopModeChange, cb)
|
p.onLoopModeChange = append(p.onLoopModeChange, cb)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Registers a callback that is notified whenever the volume changes.
|
||||||
|
func (p *PlaybackManager) OnVolumeChange(cb func(int)) {
|
||||||
|
p.onVolumeChange = append(p.onVolumeChange, cb)
|
||||||
|
}
|
||||||
|
|
||||||
// Loads the specified album into the play queue.
|
// Loads the specified album into the play queue.
|
||||||
func (p *PlaybackManager) LoadAlbum(albumID string, appendToQueue bool, shuffle bool) error {
|
func (p *PlaybackManager) LoadAlbum(albumID string, appendToQueue bool, shuffle bool) error {
|
||||||
album, err := p.sm.Server.GetAlbum(albumID)
|
album, err := p.sm.Server.GetAlbum(albumID)
|
||||||
@@ -305,12 +319,43 @@ func (p *PlaybackManager) SetNextLoopMode() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, cb := range p.onLoopModeChange {
|
for _, cb := range p.onLoopModeChange {
|
||||||
cb(p.player.GetLoopMode().String())
|
cb(LoopMode(p.player.GetLoopMode()))
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *PlaybackManager) SetLoopMode(loopMode LoopMode) error {
|
||||||
|
if err := p.player.SetLoopMode(player.LoopMode(loopMode)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, cb := range p.onLoopModeChange {
|
||||||
|
cb(loopMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PlaybackManager) LoopMode() LoopMode {
|
||||||
|
return LoopMode(p.player.GetLoopMode())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PlaybackManager) SetVolume(vol int) error {
|
||||||
|
vol = clamp(vol, 0, 100)
|
||||||
|
if err := p.player.SetVolume(vol); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, cb := range p.onVolumeChange {
|
||||||
|
cb(vol)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PlaybackManager) Volume() int {
|
||||||
|
return p.player.GetVolume()
|
||||||
|
}
|
||||||
|
|
||||||
// call BEFORE updating p.nowPlayingIdx
|
// call BEFORE updating p.nowPlayingIdx
|
||||||
func (p *PlaybackManager) checkScrobble() {
|
func (p *PlaybackManager) checkScrobble() {
|
||||||
if !p.scrobbleCfg.Enabled || len(p.playQueue) == 0 || p.nowPlayingIdx < 0 {
|
if !p.scrobbleCfg.Enabled || len(p.playQueue) == 0 || p.nowPlayingIdx < 0 {
|
||||||
|
|||||||
@@ -9,8 +9,10 @@ require (
|
|||||||
github.com/dweymouth/go-mpv v0.0.0-20230406003141-7f1858e503ee
|
github.com/dweymouth/go-mpv v0.0.0-20230406003141-7f1858e503ee
|
||||||
github.com/dweymouth/go-subsonic v0.0.0-20230614154319-792d18c75fb4
|
github.com/dweymouth/go-subsonic v0.0.0-20230614154319-792d18c75fb4
|
||||||
github.com/fsnotify/fsnotify v1.6.0
|
github.com/fsnotify/fsnotify v1.6.0
|
||||||
|
github.com/godbus/dbus/v5 v5.1.0
|
||||||
github.com/google/uuid v1.3.0
|
github.com/google/uuid v1.3.0
|
||||||
github.com/pelletier/go-toml/v2 v2.0.8
|
github.com/pelletier/go-toml/v2 v2.0.8
|
||||||
|
github.com/quarckster/go-mpris-server v1.0.1
|
||||||
github.com/zalando/go-keyring v0.2.1
|
github.com/zalando/go-keyring v0.2.1
|
||||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b
|
||||||
)
|
)
|
||||||
@@ -27,7 +29,6 @@ require (
|
|||||||
github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6 // indirect
|
github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6 // indirect
|
||||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b // indirect
|
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b // indirect
|
||||||
github.com/go-text/typesetting v0.0.0-20230405155246-bf9c697c6e16 // indirect
|
github.com/go-text/typesetting v0.0.0-20230405155246-bf9c697c6e16 // indirect
|
||||||
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
|
||||||
github.com/goki/freetype v0.0.0-20220119013949-7a161fd3728c // indirect
|
github.com/goki/freetype v0.0.0-20220119013949-7a161fd3728c // indirect
|
||||||
github.com/gopherjs/gopherjs v1.17.2 // indirect
|
github.com/gopherjs/gopherjs v1.17.2 // indirect
|
||||||
github.com/jsummers/gobmp v0.0.0-20151104160322-e2ba15ffa76e // indirect
|
github.com/jsummers/gobmp v0.0.0-20151104160322-e2ba15ffa76e // indirect
|
||||||
@@ -47,3 +48,5 @@ require (
|
|||||||
)
|
)
|
||||||
|
|
||||||
replace fyne.io/fyne/v2 v2.3.5 => github.com/dweymouth/fyne/v2 v2.3.0-rc1.0.20230505012127-ca61c153b2a5
|
replace fyne.io/fyne/v2 v2.3.5 => github.com/dweymouth/fyne/v2 v2.3.0-rc1.0.20230505012127-ca61c153b2a5
|
||||||
|
|
||||||
|
replace github.com/quarckster/go-mpris-server v1.0.1 => github.com/dweymouth/go-mpris-server v0.0.0-20230714011337-8bc0b52618a5
|
||||||
|
|||||||
@@ -76,6 +76,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
|||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/dweymouth/fyne/v2 v2.3.0-rc1.0.20230505012127-ca61c153b2a5 h1:uXbHzg9HfefA7OVHu9gahobCP5B43df3L1MwU0GbdRs=
|
github.com/dweymouth/fyne/v2 v2.3.0-rc1.0.20230505012127-ca61c153b2a5 h1:uXbHzg9HfefA7OVHu9gahobCP5B43df3L1MwU0GbdRs=
|
||||||
github.com/dweymouth/fyne/v2 v2.3.0-rc1.0.20230505012127-ca61c153b2a5/go.mod h1:X2+NrR+62mvAiAt2fwKT7035zQsE77KVV1NlvWo4vW8=
|
github.com/dweymouth/fyne/v2 v2.3.0-rc1.0.20230505012127-ca61c153b2a5/go.mod h1:X2+NrR+62mvAiAt2fwKT7035zQsE77KVV1NlvWo4vW8=
|
||||||
|
github.com/dweymouth/go-mpris-server v0.0.0-20230714011337-8bc0b52618a5 h1:QhBf0iHPPUSxalHjhls+9LFqMuNT12U3Mo/AFw4Ornc=
|
||||||
|
github.com/dweymouth/go-mpris-server v0.0.0-20230714011337-8bc0b52618a5/go.mod h1:2b4IdrpnEoEfU+6fQKjYhAgdvsiz4JxmTpDAUrMJVO4=
|
||||||
github.com/dweymouth/go-mpv v0.0.0-20230406003141-7f1858e503ee h1:ZGyJ6wp7CAfT31BugypcF/TPKEy2RrGR9JFq1JOjOpY=
|
github.com/dweymouth/go-mpv v0.0.0-20230406003141-7f1858e503ee h1:ZGyJ6wp7CAfT31BugypcF/TPKEy2RrGR9JFq1JOjOpY=
|
||||||
github.com/dweymouth/go-mpv v0.0.0-20230406003141-7f1858e503ee/go.mod h1:Ov0ieN90M7i+0k3OxhA/g1dozGs+UcPHDsMKqPgRDk0=
|
github.com/dweymouth/go-mpv v0.0.0-20230406003141-7f1858e503ee/go.mod h1:Ov0ieN90M7i+0k3OxhA/g1dozGs+UcPHDsMKqPgRDk0=
|
||||||
github.com/dweymouth/go-subsonic v0.0.0-20230614154319-792d18c75fb4 h1:cuyvB4GjTMBHKJwMJ/LFpuKfSVXuCheNj6fgxBa3m1Y=
|
github.com/dweymouth/go-subsonic v0.0.0-20230614154319-792d18c75fb4 h1:cuyvB4GjTMBHKJwMJ/LFpuKfSVXuCheNj6fgxBa3m1Y=
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
myApp, err := backend.StartupApp(appname, appVersionTag, configFile, latestReleaseURL)
|
myApp, err := backend.StartupApp(appname, displayName, appVersionTag, configFile, latestReleaseURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("fatal startup error: %v", err.Error())
|
log.Fatalf("fatal startup error: %v", err.Error())
|
||||||
}
|
}
|
||||||
@@ -41,6 +41,10 @@ func main() {
|
|||||||
}
|
}
|
||||||
mainWindow := ui.NewMainWindow(fyneApp, appname, displayName, appVersion, myApp, fyne.NewSize(w, h))
|
mainWindow := ui.NewMainWindow(fyneApp, appname, displayName, appVersion, myApp, fyne.NewSize(w, h))
|
||||||
myApp.OnReactivate = mainWindow.Show
|
myApp.OnReactivate = mainWindow.Show
|
||||||
|
myApp.OnExit = func() {
|
||||||
|
saveWindowPosition(myApp.Config, mainWindow.Window)
|
||||||
|
fyneApp.Quit()
|
||||||
|
}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
// TODO: There is a race condition with laying out the window before the
|
// TODO: There is a race condition with laying out the window before the
|
||||||
@@ -61,8 +65,7 @@ func main() {
|
|||||||
|
|
||||||
mainWindow.Show()
|
mainWindow.Show()
|
||||||
mainWindow.Window.SetCloseIntercept(func() {
|
mainWindow.Window.SetCloseIntercept(func() {
|
||||||
myApp.Config.Application.WindowHeight = int(mainWindow.Canvas().Size().Height)
|
saveWindowPosition(myApp.Config, mainWindow.Window)
|
||||||
myApp.Config.Application.WindowWidth = int(mainWindow.Canvas().Size().Width)
|
|
||||||
if myApp.Config.Application.CloseToSystemTray &&
|
if myApp.Config.Application.CloseToSystemTray &&
|
||||||
mainWindow.HaveSystemTray() {
|
mainWindow.HaveSystemTray() {
|
||||||
mainWindow.Window.Hide()
|
mainWindow.Window.Hide()
|
||||||
@@ -75,3 +78,8 @@ func main() {
|
|||||||
log.Println("Running shutdown tasks...")
|
log.Println("Running shutdown tasks...")
|
||||||
myApp.Shutdown()
|
myApp.Shutdown()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func saveWindowPosition(config *backend.Config, window fyne.Window) {
|
||||||
|
config.Application.WindowHeight = int(window.Canvas().Size().Height)
|
||||||
|
config.Application.WindowWidth = int(window.Canvas().Size().Width)
|
||||||
|
}
|
||||||
|
|||||||
+4
-5
@@ -54,9 +54,6 @@ func NewBottomPanel(p *player.Player, pm *backend.PlaybackManager, contr *contro
|
|||||||
p.OnStopped(func() {
|
p.OnStopped(func() {
|
||||||
bp.Controls.SetPlaying(false)
|
bp.Controls.SetPlaying(false)
|
||||||
})
|
})
|
||||||
pm.OnLoopModeChange(func(mode string) {
|
|
||||||
bp.AuxControls.SetLoopMode(mode)
|
|
||||||
})
|
|
||||||
|
|
||||||
bp.NowPlaying = widgets.NewNowPlayingCard()
|
bp.NowPlaying = widgets.NewNowPlayingCard()
|
||||||
bp.NowPlaying.OnShowCoverImage = func() {
|
bp.NowPlaying.OnShowCoverImage = func() {
|
||||||
@@ -100,8 +97,10 @@ func NewBottomPanel(p *player.Player, pm *backend.PlaybackManager, contr *contro
|
|||||||
})
|
})
|
||||||
|
|
||||||
bp.AuxControls = widgets.NewAuxControls(p.GetVolume())
|
bp.AuxControls = widgets.NewAuxControls(p.GetVolume())
|
||||||
bp.AuxControls.VolumeControl.OnVolumeChanged = func(v int) {
|
pm.OnLoopModeChange(bp.AuxControls.SetLoopMode)
|
||||||
_ = p.SetVolume(v)
|
pm.OnVolumeChange(bp.AuxControls.VolumeControl.SetVolume)
|
||||||
|
bp.AuxControls.VolumeControl.OnSetVolume = func(v int) {
|
||||||
|
_ = bp.playbackManager.SetVolume(v)
|
||||||
}
|
}
|
||||||
bp.AuxControls.OnChangeLoopMode(func() {
|
bp.AuxControls.OnChangeLoopMode(func() {
|
||||||
bp.playbackManager.SetNextLoopMode()
|
bp.playbackManager.SetNextLoopMode()
|
||||||
|
|||||||
+4
-4
@@ -163,15 +163,15 @@ func (m *MainWindow) SetupSystemTrayMenu(appName string, fyneApp fyne.App) {
|
|||||||
}),
|
}),
|
||||||
fyne.NewMenuItemSeparator(),
|
fyne.NewMenuItemSeparator(),
|
||||||
fyne.NewMenuItem("Volume +10%", func() {
|
fyne.NewMenuItem("Volume +10%", func() {
|
||||||
vol := m.App.Player.GetVolume()
|
vol := m.App.PlaybackManager.Volume()
|
||||||
vol = vol + int(float64(vol)*0.1)
|
vol = vol + int(float64(vol)*0.1)
|
||||||
// will clamp to range for us
|
// will clamp to range for us
|
||||||
m.BottomPanel.AuxControls.VolumeControl.SetVolume(vol)
|
m.App.PlaybackManager.SetVolume(vol)
|
||||||
}),
|
}),
|
||||||
fyne.NewMenuItem("Volume -10%", func() {
|
fyne.NewMenuItem("Volume -10%", func() {
|
||||||
vol := m.App.Player.GetVolume()
|
vol := m.App.PlaybackManager.Volume()
|
||||||
vol = vol - int(float64(vol)*0.1)
|
vol = vol - int(float64(vol)*0.1)
|
||||||
m.BottomPanel.AuxControls.VolumeControl.SetVolume(vol)
|
m.App.PlaybackManager.SetVolume(vol)
|
||||||
}),
|
}),
|
||||||
fyne.NewMenuItemSeparator(),
|
fyne.NewMenuItemSeparator(),
|
||||||
fyne.NewMenuItem("Show", m.Window.Show),
|
fyne.NewMenuItem("Show", m.Window.Show),
|
||||||
|
|||||||
+29
-13
@@ -7,6 +7,7 @@ import (
|
|||||||
"fyne.io/fyne/v2/theme"
|
"fyne.io/fyne/v2/theme"
|
||||||
"fyne.io/fyne/v2/widget"
|
"fyne.io/fyne/v2/widget"
|
||||||
|
|
||||||
|
"github.com/dweymouth/supersonic/backend"
|
||||||
myTheme "github.com/dweymouth/supersonic/ui/theme"
|
myTheme "github.com/dweymouth/supersonic/ui/theme"
|
||||||
"github.com/dweymouth/supersonic/ui/util"
|
"github.com/dweymouth/supersonic/ui/util"
|
||||||
)
|
)
|
||||||
@@ -67,14 +68,15 @@ func (a *AuxControls) OnChangeLoopMode(f func()) {
|
|||||||
a.loop.OnTapped = f
|
a.loop.OnTapped = f
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *AuxControls) SetLoopMode(mode string) {
|
func (a *AuxControls) SetLoopMode(mode backend.LoopMode) {
|
||||||
if mode == "all" {
|
switch mode {
|
||||||
|
case backend.LoopModeAll:
|
||||||
a.loop.Importance = widget.HighImportance
|
a.loop.Importance = widget.HighImportance
|
||||||
a.loop.Icon = myTheme.RepeatIcon
|
a.loop.Icon = myTheme.RepeatIcon
|
||||||
} else if mode == "one" {
|
case backend.LoopModeOne:
|
||||||
a.loop.Importance = widget.HighImportance
|
a.loop.Importance = widget.HighImportance
|
||||||
a.loop.Icon = myTheme.RepeatOneIcon
|
a.loop.Icon = myTheme.RepeatOneIcon
|
||||||
} else {
|
case backend.LoopModeNone:
|
||||||
a.loop.Importance = widget.MediumImportance
|
a.loop.Importance = widget.MediumImportance
|
||||||
a.loop.Icon = myTheme.RepeatIcon
|
a.loop.Icon = myTheme.RepeatIcon
|
||||||
}
|
}
|
||||||
@@ -160,7 +162,7 @@ type VolumeControl struct {
|
|||||||
icon *TappableIcon
|
icon *TappableIcon
|
||||||
slider *volumeSlider
|
slider *volumeSlider
|
||||||
|
|
||||||
OnVolumeChanged func(int)
|
OnSetVolume func(int)
|
||||||
|
|
||||||
muted bool
|
muted bool
|
||||||
lastVol int
|
lastVol int
|
||||||
@@ -183,24 +185,35 @@ func NewVolumeControl(initialVol int) *VolumeControl {
|
|||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sets the volume that is displayed in the slider.
|
||||||
|
// Does not invoke OnSetVolume callback.
|
||||||
|
func (v *VolumeControl) SetVolume(vol int) {
|
||||||
|
if (vol == v.lastVol && !v.muted) || (v.muted && vol == 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
v.lastVol = vol
|
||||||
|
v.muted = false
|
||||||
|
v.setDisplayedVolume(vol)
|
||||||
|
}
|
||||||
|
|
||||||
func (v *VolumeControl) onChanged(volume float64) {
|
func (v *VolumeControl) onChanged(volume float64) {
|
||||||
vol := int(volume)
|
vol := int(volume)
|
||||||
v.lastVol = vol
|
v.lastVol = vol
|
||||||
v.muted = false
|
v.muted = false
|
||||||
v.updateIconForVolume(vol)
|
v.updateIconForVolume(vol)
|
||||||
if v.OnVolumeChanged != nil {
|
v.invokeOnVolumeChange(vol)
|
||||||
v.OnVolumeChanged(vol)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *VolumeControl) toggleMute() {
|
func (v *VolumeControl) toggleMute() {
|
||||||
if !v.muted {
|
if !v.muted {
|
||||||
v.muted = true
|
v.muted = true
|
||||||
v.lastVol = int(v.slider.Value)
|
v.lastVol = int(v.slider.Value)
|
||||||
v.SetVolume(0)
|
v.setDisplayedVolume(0)
|
||||||
|
v.invokeOnVolumeChange(0)
|
||||||
} else {
|
} else {
|
||||||
v.muted = false
|
v.muted = false
|
||||||
v.SetVolume(v.lastVol)
|
v.setDisplayedVolume(v.lastVol)
|
||||||
|
v.invokeOnVolumeChange(v.lastVol)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,12 +222,15 @@ func (v *VolumeControl) CreateRenderer() fyne.WidgetRenderer {
|
|||||||
return widget.NewSimpleRenderer(v.container)
|
return widget.NewSimpleRenderer(v.container)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *VolumeControl) SetVolume(vol int) {
|
func (v *VolumeControl) setDisplayedVolume(vol int) {
|
||||||
v.slider.Value = float64(vol)
|
v.slider.Value = float64(vol)
|
||||||
v.slider.Refresh()
|
v.slider.Refresh()
|
||||||
v.updateIconForVolume(vol)
|
v.updateIconForVolume(vol)
|
||||||
if v.OnVolumeChanged != nil {
|
}
|
||||||
v.OnVolumeChanged(vol)
|
|
||||||
|
func (v *VolumeControl) invokeOnVolumeChange(vol int) {
|
||||||
|
if v.OnSetVolume != nil {
|
||||||
|
v.OnSetVolume(vol)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user