From 1e8c9484d2e4698bd5037558bfe1da36b2721853 Mon Sep 17 00:00:00 2001 From: razorkitty Date: Mon, 21 Nov 2022 19:59:54 +0000 Subject: [PATCH] copy mpris implementation from infinitime library to itd, where it really belongs moved dbus.go to an internal utils package added context function parameter to initMusicCtrl and updated main.go to pass it updated calls.go, maps.go, music.go, and notifs.go to use utils package for getting a dus connection --- calls.go | 6 +- dbus.go => internal/utils/dbus.go | 6 +- main.go | 2 +- maps.go | 5 +- music.go | 36 ++-- notifs.go | 3 +- pkg/mpris/mpris.go | 272 ++++++++++++++++++++++++++++++ 7 files changed, 304 insertions(+), 26 deletions(-) rename dbus.go => internal/utils/dbus.go (80%) create mode 100644 pkg/mpris/mpris.go diff --git a/calls.go b/calls.go index 04ded15..5efc2f4 100644 --- a/calls.go +++ b/calls.go @@ -7,11 +7,13 @@ import ( "github.com/godbus/dbus/v5" "github.com/rs/zerolog/log" "go.arsenm.dev/infinitime" + "go.arsenm.dev/itd/internal/utils" + ) func initCallNotifs(ctx context.Context, dev *infinitime.Device) error { // Connect to system bus. This connection is for method calls. - conn, err := newSystemBusConn(ctx) + conn, err := utils.NewSystemBusConn(ctx) if err != nil { return err } @@ -29,7 +31,7 @@ func initCallNotifs(ctx context.Context, dev *infinitime.Device) error { } // Connect to system bus. This connection is for monitoring. - monitorConn, err := newSystemBusConn(ctx) + monitorConn, err := utils.NewSystemBusConn(ctx) if err != nil { return err } diff --git a/dbus.go b/internal/utils/dbus.go similarity index 80% rename from dbus.go rename to internal/utils/dbus.go index 2db96a5..a5765ad 100644 --- a/dbus.go +++ b/internal/utils/dbus.go @@ -1,4 +1,4 @@ -package main +package utils import ( "context" @@ -6,7 +6,7 @@ import ( "github.com/godbus/dbus/v5" ) -func newSystemBusConn(ctx context.Context) (*dbus.Conn, error) { +func NewSystemBusConn(ctx context.Context) (*dbus.Conn, error) { // Connect to dbus session bus conn, err := dbus.SystemBusPrivate(dbus.WithContext(ctx)) if err != nil { @@ -23,7 +23,7 @@ func newSystemBusConn(ctx context.Context) (*dbus.Conn, error) { return conn, nil } -func newSessionBusConn(ctx context.Context) (*dbus.Conn, error) { +func NewSessionBusConn(ctx context.Context) (*dbus.Conn, error) { // Connect to dbus session bus conn, err := dbus.SessionBusPrivate(dbus.WithContext(ctx)) if err != nil { diff --git a/main.go b/main.go index 0804475..c34ebcf 100644 --- a/main.go +++ b/main.go @@ -146,7 +146,7 @@ func main() { } // Initialize music controls - err = initMusicCtrl(dev) + err = initMusicCtrl(ctx, dev) if err != nil { log.Error().Err(err).Msg("Error initializing music control") } diff --git a/maps.go b/maps.go index dec731f..0555de8 100644 --- a/maps.go +++ b/maps.go @@ -7,6 +7,7 @@ import ( "github.com/godbus/dbus/v5" "github.com/rs/zerolog/log" "go.arsenm.dev/infinitime" + "go.arsenm.dev/itd/internal/utils" ) const ( @@ -19,7 +20,7 @@ const ( func initPureMaps(ctx context.Context, dev *infinitime.Device) error { // Connect to session bus. This connection is for method calls. - conn, err := newSessionBusConn(ctx) + conn, err := utils.NewSessionBusConn(ctx) if err != nil { return err } @@ -30,7 +31,7 @@ func initPureMaps(ctx context.Context, dev *infinitime.Device) error { } // Connect to session bus. This connection is for method calls. - monitorConn, err := newSessionBusConn(ctx) + monitorConn, err := utils.NewSessionBusConn(ctx) if err != nil { return err } diff --git a/music.go b/music.go index cac2fe2..5e1e4d6 100644 --- a/music.go +++ b/music.go @@ -19,29 +19,31 @@ package main import ( + "context" + "github.com/rs/zerolog/log" "go.arsenm.dev/infinitime" - "go.arsenm.dev/infinitime/pkg/player" "go.arsenm.dev/itd/translit" + "go.arsenm.dev/itd/pkg/mpris" ) -func initMusicCtrl(dev *infinitime.Device) error { - player.Init() +func initMusicCtrl(ctx context.Context, dev *infinitime.Device) error { + mpris.Init(ctx) maps := k.Strings("notifs.translit.use") translit.Transliterators["custom"] = translit.Map(k.Strings("notifs.translit.custom")) - player.OnChange(func(ct player.ChangeType, val string) { + mpris.OnChange(func(ct mpris.ChangeType, val string) { newVal := translit.Transliterate(val, maps...) if !firmwareUpdating { switch ct { - case player.ChangeTypeStatus: - dev.Music.SetStatus(val == "Playing") - case player.ChangeTypeTitle: - dev.Music.SetTrack(newVal) - case player.ChangeTypeAlbum: - dev.Music.SetAlbum(newVal) - case player.ChangeTypeArtist: + case mpris.ChangeTypeStatus: + dev.Music.SetStatus(val == "Playing") + case mpris.ChangeTypeTitle: + dev.Music.SetTrack(newVal) + case mpris.ChangeTypeAlbum: + dev.Music.SetAlbum(newVal) + case mpris.ChangeTypeArtist: dev.Music.SetArtist(newVal) } } @@ -58,17 +60,17 @@ func initMusicCtrl(dev *infinitime.Device) error { // Perform appropriate action based on event switch musicEvt { case infinitime.MusicEventPlay: - player.Play() + mpris.Play() case infinitime.MusicEventPause: - player.Pause() + mpris.Pause() case infinitime.MusicEventNext: - player.Next() + mpris.Next() case infinitime.MusicEventPrev: - player.Prev() + mpris.Prev() case infinitime.MusicEventVolUp: - player.VolUp(uint(k.Int("music.vol.interval"))) + mpris.VolUp(uint(k.Int("music.vol.interval"))) case infinitime.MusicEventVolDown: - player.VolDown(uint(k.Int("music.vol.interval"))) + mpris.VolDown(uint(k.Int("music.vol.interval"))) } } }() diff --git a/notifs.go b/notifs.go index 669a52c..a3e8e36 100644 --- a/notifs.go +++ b/notifs.go @@ -26,11 +26,12 @@ import ( "github.com/rs/zerolog/log" "go.arsenm.dev/infinitime" "go.arsenm.dev/itd/translit" + "go.arsenm.dev/itd/internal/utils" ) func initNotifRelay(ctx context.Context, dev *infinitime.Device) error { // Connect to dbus session bus - bus, err := newSessionBusConn(ctx) + bus, err := utils.NewSessionBusConn(ctx) if err != nil { return err } diff --git a/pkg/mpris/mpris.go b/pkg/mpris/mpris.go new file mode 100644 index 0000000..196af20 --- /dev/null +++ b/pkg/mpris/mpris.go @@ -0,0 +1,272 @@ +package mpris + +import ( + "context" + "strings" + "sync" + + "github.com/godbus/dbus/v5" + "go.arsenm.dev/itd/internal/utils" +) + +var ( + method, monitor *dbus.Conn + monitorCh chan *dbus.Message + onChangeOnce sync.Once +) + +// Init makes required connections to DBis and +// initializes change monitoring channel +func Init(ctx context.Context) error { + // Connect to session bus for monitoring + monitorConn, err := utils.NewSessionBusConn(ctx) + if err != nil { + return err + } + // Add match rule for PropertiesChanged on media player + monitorConn.AddMatchSignal( + dbus.WithMatchObjectPath("/org/mpris/MediaPlayer2"), + dbus.WithMatchInterface("org.freedesktop.DBus.Properties"), + dbus.WithMatchMember("PropertiesChanged"), + ) + monitorCh = make(chan *dbus.Message, 10) + monitorConn.Eavesdrop(monitorCh) + + // Connect to session bus for method calls + methodConn, err := utils.NewSessionBusConn(ctx) + if err != nil { + return err + } + method, monitor = methodConn, monitorConn + return nil +} + +// Exit closes all connections and channels +func Exit() { + close(monitorCh) + method.Close() + monitor.Close() +} + +// Play uses MPRIS to play media +func Play() error { + player, err := getPlayerObj() + if err != nil { + return err + } + if player != nil { + call := player.Call("org.mpris.MediaPlayer2.Player.Play", 0) + if call.Err != nil { + return call.Err + } + } + return nil +} + +// Pause uses MPRIS to pause media +func Pause() error { + player, err := getPlayerObj() + if err != nil { + return err + } + if player != nil { + call := player.Call("org.mpris.MediaPlayer2.Player.Pause", 0) + if call.Err != nil { + return call.Err + } + } + return nil +} + +// Next uses MPRIS to skip to next media +func Next() error { + player, err := getPlayerObj() + if err != nil { + return err + } + if player != nil { + call := player.Call("org.mpris.MediaPlayer2.Player.Next", 0) + if call.Err != nil { + return call.Err + } + } + return nil +} + +// Prev uses MPRIS to skip to previous media +func Prev() error { + player, err := getPlayerObj() + if err != nil { + return err + } + if player != nil { + call := player.Call("org.mpris.MediaPlayer2.Player.Previous", 0) + if call.Err != nil { + return call.Err + } + } + return nil +} + +func VolUp(percent uint) error { + + player, err := getPlayerObj() + if err != nil { + return err + } + if player != nil { + currentVal, err := player.GetProperty("org.mpris.MediaPlayer2.Player.Volume") + if err != nil { + return err + } + newVal := currentVal.Value().(float64) + (float64(percent) / 100) + err = player.SetProperty("org.mpris.MediaPlayer2.Player.Volume", newVal) + if err != nil { + return err + } + } + return nil +} + +func VolDown(percent uint) error { + + player, err := getPlayerObj() + if err != nil { + return err + } + if player != nil { + currentVal, err := player.GetProperty("org.mpris.MediaPlayer2.Player.Volume") + if err != nil { + return err + } + newVal := currentVal.Value().(float64) - (float64(percent) / 100) + err = player.SetProperty("org.mpris.MediaPlayer2.Player.Volume", newVal) + if err != nil { + return err + } + } + return nil +} + +type ChangeType int + +const ( + ChangeTypeTitle ChangeType = iota + ChangeTypeArtist + ChangeTypeAlbum + ChangeTypeStatus +) + +func (ct ChangeType) String() string { + switch ct { + case ChangeTypeTitle: + return "Title" + case ChangeTypeAlbum: + return "Album" + case ChangeTypeArtist: + return "Artist" + case ChangeTypeStatus: + return "Status" + } + return "" +} + +// OnChange runs cb when a value changes +func OnChange(cb func(ChangeType, string)) { + go onChangeOnce.Do(func() { + // For every message on channel + for msg := range monitorCh { + // Parse PropertiesChanged + iface, changed, ok := parsePropertiesChanged(msg) + if !ok || iface != "org.mpris.MediaPlayer2.Player" { + continue + } + + // For every property changed + for name, val := range changed { + // If metadata changed + if name == "Metadata" { + // Get fields + fields := val.Value().(map[string]dbus.Variant) + // For every field + for name, val := range fields { + // Handle each field appropriately + if strings.HasSuffix(name, "title") { + title := val.Value().(string) + if title == "" { + title = "Unknown " + ChangeTypeTitle.String() + } + cb(ChangeTypeTitle, title) + } else if strings.HasSuffix(name, "album") { + album := val.Value().(string) + if album == "" { + album = "Unknown " + ChangeTypeAlbum.String() + } + cb(ChangeTypeAlbum, album) + } else if strings.HasSuffix(name, "artist") { + var artists string + switch artistVal := val.Value().(type) { + case string: + artists = artistVal + case []string: + artists = strings.Join(artistVal, ", ") + } + if artists == "" { + artists = "Unknown " + ChangeTypeArtist.String() + } + cb(ChangeTypeArtist, artists) + } + } + } else if name == "PlaybackStatus" { + // Handle status change + cb(ChangeTypeStatus, val.Value().(string)) + } + } + } + }) +} + +// getPlayerNames gets all DBus MPRIS player bus names +func getPlayerNames(conn *dbus.Conn) ([]string, error) { + var names []string + err := conn.BusObject().Call("org.freedesktop.DBus.ListNames", 0).Store(&names) + if err != nil { + return nil, err + } + var players []string + for _, name := range names { + if strings.HasPrefix(name, "org.mpris.MediaPlayer2") { + players = append(players, name) + } + } + return players, nil +} + +// GetPlayerObj gets the object corresponding to the first +// bus name found in DBus +func getPlayerObj() (dbus.BusObject, error) { + players, err := getPlayerNames(method) + if err != nil { + return nil, err + } + if len(players) == 0 { + return nil, nil + } + return method.Object(players[0], "/org/mpris/MediaPlayer2"), nil +} + +// parsePropertiesChanged parses a DBus PropertiesChanged signal +func parsePropertiesChanged(msg *dbus.Message) (iface string, changed map[string]dbus.Variant, ok bool) { + if len(msg.Body) != 3 { + return "", nil, false + } + iface, ok = msg.Body[0].(string) + if !ok { + return + } + changed, ok = msg.Body[1].(map[string]dbus.Variant) + if !ok { + return + } + return +}