Files
supersonic/backend/mpris.go
T
Michael Manganiello c7dfc9efb3 fix: Enable Loop mode sync for MPRIS
Loop mode changes were being correctly reflected in Supersonic, when
updated in the toolbar, but not the other way around. We were only
missing an `Options` changes emit when loop mode is changed in
Supersonic.
2024-02-24 15:17:13 -03:00

336 lines
7.8 KiB
Go

package backend
import (
"encoding/base32"
"errors"
"strconv"
"github.com/dweymouth/supersonic/backend/mediaprovider"
"github.com/dweymouth/supersonic/backend/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/"
noTrackObjectPath = "/org/mpris/MediaPlayer2/TrackList/NoTrack"
)
var (
_ types.OrgMprisMediaPlayer2Adapter = (*MPRISHandler)(nil)
_ types.OrgMprisMediaPlayer2PlayerAdapter = (*MPRISHandler)(nil)
_ types.OrgMprisMediaPlayer2PlayerAdapterLoopStatus = (*MPRISHandler)(nil)
)
var (
errNotSupported = errors.New("not supported")
)
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
curTrackPath string // empty for no track
pm *PlaybackManager
s *server.Server
evt *events.EventHandler
}
func NewMPRISHandler(playerName string, pm *PlaybackManager) *MPRISHandler {
m := &MPRISHandler{playerName: playerName, pm: pm, connErr: errors.New("not started")}
m.s = server.NewServer(playerName, m, m)
m.evt = events.NewEventHandler(m.s)
pm.OnSeek(func() {
if m.connErr == nil {
pos := secondsToMicroseconds(pm.PlayerStatus().TimePos)
m.evt.Player.OnSeek(pos)
}
})
pm.OnSongChange(func(tr, _ *mediaprovider.Track) {
if tr == nil {
m.curTrackPath = ""
} else {
m.curTrackPath = dbusTrackIDPrefix + encodeTrackId(tr.ID)
}
if m.connErr == nil {
m.evt.Player.OnTitle()
}
})
pm.OnVolumeChange(func(vol int) {
if m.connErr == nil {
m.evt.Player.OnVolume()
}
})
pm.OnLoopModeChange(func(loopMode LoopMode) {
if m.connErr == nil {
m.evt.Player.OnOptions()
}
})
emitPlayStatus := func() {
if m.connErr == nil {
m.evt.Player.OnPlayPause()
}
}
m.pm.OnStopped(emitPlayStatus)
m.pm.OnPlaying(emitPlayStatus)
m.pm.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.pm.SeekNext()
}
func (m *MPRISHandler) Previous() error {
return m.pm.SeekBackOrPrevious()
}
func (m *MPRISHandler) Pause() error {
if m.pm.PlayerStatus().State == player.Playing {
return m.pm.PlayPause()
}
return nil
}
func (m *MPRISHandler) PlayPause() error {
return m.pm.PlayPause()
}
func (m *MPRISHandler) Stop() error {
return m.pm.Stop()
}
func (m *MPRISHandler) Play() error {
switch m.pm.PlayerStatus().State {
case player.Paused:
return m.pm.PlayPause()
case player.Stopped:
return m.pm.PlayFromBeginning()
}
return nil
}
func (m *MPRISHandler) Seek(offset types.Microseconds) error {
// MPRIS seek command is relative to current position
pos := m.pm.PlayerStatus().TimePos + microsecondsToSeconds(offset)
return m.pm.SeekSeconds(pos)
}
func (m *MPRISHandler) SetPosition(trackId string, position types.Microseconds) error {
if m.curTrackPath == trackId {
return m.pm.SeekSeconds(microsecondsToSeconds(position))
}
return nil
}
func (m *MPRISHandler) OpenUri(uri string) error {
return errNotSupported
}
func (m *MPRISHandler) PlaybackStatus() (types.PlaybackStatus, error) {
switch m.pm.PlayerStatus().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.GetLoopMode() {
case LoopAll:
return types.LoopStatusPlaylist, nil
case LoopOne:
return types.LoopStatusTrack, nil
case LoopNone:
return types.LoopStatusNone, nil
}
return "", errors.New("unknown loop status")
}
func (m *MPRISHandler) SetLoopStatus(status types.LoopStatus) error {
switch status {
case types.LoopStatusPlaylist:
m.pm.SetLoopMode(LoopAll)
case types.LoopStatusTrack:
m.pm.SetLoopMode(LoopOne)
case types.LoopStatusNone:
m.pm.SetLoopMode(LoopNone)
default:
return errors.New("unknown loop status")
}
return nil
}
func (m *MPRISHandler) Rate() (float64, error) {
return 1, nil
}
func (m *MPRISHandler) SetRate(float64) error {
return errNotSupported
}
func (m *MPRISHandler) Metadata() (types.Metadata, error) {
trackObjPath := noTrackObjectPath
if m.curTrackPath != "" {
trackObjPath = m.curTrackPath
}
status := m.pm.PlayerStatus()
var tr mediaprovider.Track
if np := m.pm.NowPlaying(); np != nil && status.State != player.Stopped {
tr = *np
}
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(trackObjPath),
Length: secondsToMicroseconds(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,
ContentCreated: strconv.Itoa(tr.Year),
UseCount: tr.PlayCount,
ArtUrl: artURL,
}, nil
}
func (m *MPRISHandler) Volume() (float64, error) {
return float64(m.pm.Volume()) / 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.pm.PlayerStatus().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 microsecondsToSeconds(m types.Microseconds) float64 {
return float64(m) / 1_000_000
}
func secondsToMicroseconds(s float64) types.Microseconds {
return types.Microseconds(s * 1_000_000)
}
func encodeTrackId(id string) string {
data := []byte(id)
return base32.StdEncoding.WithPadding('0').EncodeToString(data)
}