forked from Elara6331/infinitime
Switch player to MPRIS interface
This commit is contained in:
parent
ec1548ec0f
commit
d9823bf0c8
5
go.mod
5
go.mod
@ -2,4 +2,7 @@ module go.arsenm.dev/infinitime
|
|||||||
|
|
||||||
go 1.16
|
go 1.16
|
||||||
|
|
||||||
require github.com/muka/go-bluetooth v0.0.0-20211122080231-b99792bbe62a
|
require (
|
||||||
|
github.com/godbus/dbus/v5 v5.0.3
|
||||||
|
github.com/muka/go-bluetooth v0.0.0-20211122080231-b99792bbe62a
|
||||||
|
)
|
||||||
|
37
internal/utils/dbus.go
Normal file
37
internal/utils/dbus.go
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import "github.com/godbus/dbus/v5"
|
||||||
|
|
||||||
|
func NewSystemBusConn() (*dbus.Conn, error) {
|
||||||
|
// Connect to dbus session bus
|
||||||
|
conn, err := dbus.SystemBusPrivate()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = conn.Auth(nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = conn.Hello()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return conn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSessionBusConn() (*dbus.Conn, error) {
|
||||||
|
// Connect to dbus session bus
|
||||||
|
conn, err := dbus.SessionBusPrivate()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = conn.Auth(nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = conn.Hello()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return conn, nil
|
||||||
|
}
|
224
pkg/player/player.go
Normal file
224
pkg/player/player.go
Normal file
@ -0,0 +1,224 @@
|
|||||||
|
package player
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/godbus/dbus/v5"
|
||||||
|
"go.arsenm.dev/infinitime/internal/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
method, monitor *dbus.Conn
|
||||||
|
monitorCh chan *dbus.Message
|
||||||
|
)
|
||||||
|
|
||||||
|
// Init makes required connections to DBis and
|
||||||
|
// initializes change monitoring channel
|
||||||
|
func Init() error {
|
||||||
|
// Connect to session bus for monitoring
|
||||||
|
monitorConn, err := utils.NewSessionBusConn()
|
||||||
|
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)
|
||||||
|
monitorConn.Eavesdrop(monitorCh)
|
||||||
|
|
||||||
|
// Connect to session bus for method calls
|
||||||
|
methodConn, err := utils.NewSessionBusConn()
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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") {
|
||||||
|
artists := val.Value().([]string)
|
||||||
|
artistStr := strings.Join(artists, ", ")
|
||||||
|
if artistStr == "" {
|
||||||
|
artistStr = "Unknown " + ChangeTypeArtist.String()
|
||||||
|
}
|
||||||
|
cb(ChangeTypeArtist, artistStr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} 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
|
||||||
|
}
|
@ -1,112 +0,0 @@
|
|||||||
package player
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"io"
|
|
||||||
"os/exec"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Play uses playerctl to play media
|
|
||||||
func Play() error {
|
|
||||||
return exec.Command("playerctl", "play").Run()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pause uses playerctl to pause media
|
|
||||||
func Pause() error {
|
|
||||||
return exec.Command("playerctl", "pause").Run()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Next uses playerctl to skip to next media
|
|
||||||
func Next() error {
|
|
||||||
return exec.Command("playerctl", "next").Run()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prev uses playerctl to skip to previous media
|
|
||||||
func Prev() error {
|
|
||||||
return exec.Command("playerctl", "previous").Run()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Metadata uses playerctl to detect music metadata changes
|
|
||||||
func Metadata(key string, onChange func(string)) error {
|
|
||||||
// Execute playerctl command with key and follow flag
|
|
||||||
cmd := exec.Command("playerctl", "metadata", key, "-F")
|
|
||||||
// Get stdout pipe
|
|
||||||
stdout, err := cmd.StdoutPipe()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
go func() {
|
|
||||||
for {
|
|
||||||
// Read line from command stdout
|
|
||||||
line, _, err := bufio.NewReader(stdout).ReadLine()
|
|
||||||
if err == io.EOF {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// Convert line to string
|
|
||||||
data := string(line)
|
|
||||||
// If key unknown, return suitable default
|
|
||||||
if data == "No player could handle this command" || data == "" {
|
|
||||||
data = "Unknown " + strings.Title(key)
|
|
||||||
}
|
|
||||||
// Run the onChange callback
|
|
||||||
onChange(data)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
// Start command asynchronously
|
|
||||||
err = cmd.Start()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func Status(onChange func(bool)) error {
|
|
||||||
// Execute playerctl status with follow flag
|
|
||||||
cmd := exec.Command("playerctl", "status", "-F")
|
|
||||||
// Get stdout pipe
|
|
||||||
stdout, err := cmd.StdoutPipe()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
go func() {
|
|
||||||
for {
|
|
||||||
// Read line from command stdout
|
|
||||||
line, _, err := bufio.NewReader(stdout).ReadLine()
|
|
||||||
if err == io.EOF {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// Convert line to string
|
|
||||||
data := string(line)
|
|
||||||
// Run the onChange callback
|
|
||||||
onChange(data == "Playing")
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
// Start command asynchronously
|
|
||||||
err = cmd.Start()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func CurrentMetadata(key string) (string, error) {
|
|
||||||
out, err := exec.Command("playerctl", "metadata", key).Output()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
data := string(out)
|
|
||||||
if data == "No player could handle this command" || data == "" {
|
|
||||||
data = "Unknown " + strings.Title(key)
|
|
||||||
}
|
|
||||||
return data, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func CurrentStatus() (bool, error) {
|
|
||||||
out, err := exec.Command("playerctl", "status").Output()
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
data := string(out)
|
|
||||||
return data == "Playing", nil
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user