forked from Elara6331/infinitime
		
	Switch player to MPRIS interface
This commit is contained in:
		
							
								
								
									
										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 | ||||
| } | ||||
		Reference in New Issue
	
	Block a user