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 +}