forked from Elara6331/infinitime
Switch player to MPRIS interface
This commit is contained in:
parent
4e88b4b7f8
commit
b2b0ecebdd
5
go.mod
5
go.mod
@ -2,4 +2,7 @@ module go.arsenm.dev/infinitime
|
||||
|
||||
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