move mpris2 component to separate module
I'll likely put it in a different repo as i'm thinking of writing a last.fm scrobbler and it'd be reused there.
This commit is contained in:
		
							parent
							
								
									af0af02059
								
							
						
					
					
						commit
						bcd2a83dd9
					
				| @ -7,6 +7,7 @@ a waybar component/utility for displaying and controlling MPRIS2 compliant media | ||||
| 
 | ||||
| MPRIS2 is widely supported, so this component should work with: | ||||
| * Chrome/Chromium | ||||
| * Firefox (Potentially, with `media.hardwaremediakeys.enabled = true` in about:config) | ||||
| * Other browsers (with kde plasma integration installed) | ||||
| * VLC | ||||
| * Spotify | ||||
|  | ||||
							
								
								
									
										3
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										3
									
								
								go.mod
									
									
									
									
									
								
							| @ -2,7 +2,10 @@ module git.hrfee.pw/hrfee/waybar-mpris | ||||
| 
 | ||||
| go 1.15 | ||||
| 
 | ||||
| replace git.hrfee.pw/hrfee/waybar-mpris/mpris2client => ./mpris2client | ||||
| 
 | ||||
| require ( | ||||
| 	git.hrfee.pw/hrfee/waybar-mpris/mpris2client v0.0.0-00010101000000-000000000000 | ||||
| 	github.com/godbus/dbus/v5 v5.0.3 | ||||
| 	github.com/hpcloud/tail v1.0.0 | ||||
| 	github.com/spf13/pflag v1.0.5 | ||||
|  | ||||
							
								
								
									
										391
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										391
									
								
								main.go
									
									
									
									
									
								
							| @ -4,51 +4,21 @@ import ( | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"io/ioutil" | ||||
| 	"log" | ||||
| 	"net" | ||||
| 	"os" | ||||
| 	"os/signal" | ||||
| 	"sort" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| 	"time" | ||||
| 
 | ||||
| 	mpris2 "git.hrfee.pw/hrfee/waybar-mpris/mpris2client" | ||||
| 	"github.com/godbus/dbus/v5" | ||||
| 	"github.com/hpcloud/tail" | ||||
| 	flag "github.com/spf13/pflag" | ||||
| ) | ||||
| 
 | ||||
| var knownPlayers = map[string]string{ | ||||
| 	"plasma-browser-integration": "Browser", | ||||
| 	"noson":                      "Noson", | ||||
| } | ||||
| 
 | ||||
| var knownBrowsers = map[string]string{ | ||||
| 	"mozilla":  "Firefox", | ||||
| 	"chrome":   "Chrome", | ||||
| 	"chromium": "Chromium", | ||||
| } | ||||
| 
 | ||||
| // Player represents an active music player.
 | ||||
| type Player struct { | ||||
| 	player                               dbus.BusObject | ||||
| 	fullName, name, title, artist, album string | ||||
| 	position                             int64 | ||||
| 	pid                                  uint32 | ||||
| 	playing, stopped                     bool | ||||
| 	metadata                             map[string]dbus.Variant | ||||
| 	conn                                 *dbus.Conn | ||||
| } | ||||
| 
 | ||||
| // Various paths and values to use elsewhere.
 | ||||
| const ( | ||||
| 	INTERFACE = "org.mpris.MediaPlayer2" | ||||
| 	PATH      = "/org/mpris/MediaPlayer2" | ||||
| 	// For the NameOwnerChanged signal.
 | ||||
| 	MATCH_NOC = "type='signal',path='/org/freedesktop/DBus',interface='org.freedesktop.DBus',member='NameOwnerChanged'" | ||||
| 	// For the PropertiesChanged signal. It doesn't match exactly (couldn't get that to work) so we check it manually.
 | ||||
| 	MATCH_PC = "type='signal',path='/org/mpris/MediaPlayer2',interface='org.freedesktop.DBus.Properties'" | ||||
| 	SOCK    = "/tmp/waybar-mpris.sock" | ||||
| 	LOGFILE = "/tmp/waybar-mpris.log" | ||||
| 	OUTFILE = "/tmp/waybar-mpris.out" | ||||
| @ -70,144 +40,18 @@ var ( | ||||
| 	WRITER      io.Writer = os.Stdout | ||||
| ) | ||||
| 
 | ||||
| // NewPlayer returns a new player object.
 | ||||
| func NewPlayer(conn *dbus.Conn, name string) (p *Player) { | ||||
| 	playerName := strings.ReplaceAll(name, INTERFACE+".", "") | ||||
| 	var pid uint32 | ||||
| 	conn.BusObject().Call("org.freedesktop.DBus.GetConnectionUnixProcessID", 0, name).Store(&pid) | ||||
| 	for key, val := range knownPlayers { | ||||
| 		if strings.Contains(name, key) { | ||||
| 			playerName = val | ||||
| 			break | ||||
| 		} | ||||
| 	} | ||||
| 	if playerName == "Browser" { | ||||
| 		file, err := ioutil.ReadFile(fmt.Sprintf("/proc/%d/cmdline", pid)) | ||||
| 		if err == nil { | ||||
| 			cmd := string(file) | ||||
| 			for key, val := range knownBrowsers { | ||||
| 				if strings.Contains(cmd, key) { | ||||
| 					playerName = val | ||||
| 					break | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	p = &Player{ | ||||
| 		player:   conn.Object(name, PATH), | ||||
| 		conn:     conn, | ||||
| 		name:     playerName, | ||||
| 		fullName: name, | ||||
| 		pid:      pid, | ||||
| 	} | ||||
| 	p.Refresh() | ||||
| 	return | ||||
| } | ||||
| 
 | ||||
| // Refresh grabs playback info.
 | ||||
| func (p *Player) Refresh() (err error) { | ||||
| 	val, err := p.player.GetProperty(INTERFACE + ".Player.PlaybackStatus") | ||||
| 	if err != nil { | ||||
| 		p.playing = false | ||||
| 		p.stopped = false | ||||
| 		p.metadata = map[string]dbus.Variant{} | ||||
| 		p.title = "" | ||||
| 		p.artist = "" | ||||
| 		p.album = "" | ||||
| 		return | ||||
| 	} | ||||
| 	strVal := val.String() | ||||
| 	if strings.Contains(strVal, "Playing") { | ||||
| 		p.playing = true | ||||
| 		p.stopped = false | ||||
| 	} else if strings.Contains(strVal, "Paused") { | ||||
| 		p.playing = false | ||||
| 		p.stopped = false | ||||
| 	} else { | ||||
| 		p.playing = false | ||||
| 		p.stopped = true | ||||
| 	} | ||||
| 	metadata, err := p.player.GetProperty(INTERFACE + ".Player.Metadata") | ||||
| 	if err != nil { | ||||
| 		p.metadata = map[string]dbus.Variant{} | ||||
| 		p.title = "" | ||||
| 		p.artist = "" | ||||
| 		p.album = "" | ||||
| 		return | ||||
| 	} | ||||
| 	p.metadata = metadata.Value().(map[string]dbus.Variant) | ||||
| 	switch artist := p.metadata["xesam:artist"].Value().(type) { | ||||
| 	case []string: | ||||
| 		p.artist = strings.Join(artist, ", ") | ||||
| 	case string: | ||||
| 		p.artist = artist | ||||
| 	default: | ||||
| 		p.artist = "" | ||||
| 	} | ||||
| 	switch title := p.metadata["xesam:title"].Value().(type) { | ||||
| 	case string: | ||||
| 		p.title = title | ||||
| 	default: | ||||
| 		p.title = "" | ||||
| 	} | ||||
| 	switch album := p.metadata["xesam:album"].Value().(type) { | ||||
| 	case string: | ||||
| 		p.album = album | ||||
| 	default: | ||||
| 		p.album = "" | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func µsToString(µs int64) string { | ||||
| 	seconds := int(µs / 1e6) | ||||
| 	minutes := int(seconds / 60) | ||||
| 	seconds -= minutes * 60 | ||||
| 	return fmt.Sprintf("%02d:%02d", minutes, seconds) | ||||
| } | ||||
| 
 | ||||
| // Position figures out the track position in MM:SS, interpolating the value if necessary.
 | ||||
| func (p *Player) Position() string { | ||||
| 	// position is in microseconds so we prob need int64 to be safe
 | ||||
| 	v := p.metadata["mpris:length"].Value() | ||||
| 	var l int64 | ||||
| 	if v != nil { | ||||
| 		l = v.(int64) | ||||
| 	} else { | ||||
| 		return "" | ||||
| 	} | ||||
| 	length := µsToString(l) | ||||
| 	if length == "" { | ||||
| 		return "" | ||||
| 	} | ||||
| 	pos, err := p.player.GetProperty(INTERFACE + ".Player.Position") | ||||
| 	if err != nil { | ||||
| 		return "" | ||||
| 	} | ||||
| 	position := µsToString(pos.Value().(int64)) | ||||
| 	if position == "" { | ||||
| 		return "" | ||||
| 	} | ||||
| 	if INTERPOLATE && position == µsToString(p.position) { | ||||
| 		np := p.position + int64(POLL*1000000) | ||||
| 		position = µsToString(np) | ||||
| 	} | ||||
| 	p.position = pos.Value().(int64) | ||||
| 	return position + "/" + length | ||||
| } | ||||
| 
 | ||||
| // JSON returns json for waybar to consume.
 | ||||
| func (p *Player) JSON() string { | ||||
| func playerJSON(p *mpris2.Player) string { | ||||
| 	data := map[string]string{} | ||||
| 	symbol := PLAY | ||||
| 	data["class"] = "paused" | ||||
| 	if p.playing { | ||||
| 	if p.Playing { | ||||
| 		symbol = PAUSE | ||||
| 		data["class"] = "playing" | ||||
| 	} | ||||
| 	var pos string | ||||
| 	if SHOW_POS { | ||||
| 		pos = p.Position() | ||||
| 		pos = p.StringPosition() | ||||
| 		if pos != "" { | ||||
| 			pos = "(" + pos + ")" | ||||
| 		} | ||||
| @ -219,16 +63,16 @@ func (p *Player) JSON() string { | ||||
| 		case "SYMBOL": | ||||
| 			items = append(items, symbol) | ||||
| 		case "ARTIST": | ||||
| 			if p.artist != "" { | ||||
| 				items = append(items, p.artist) | ||||
| 			if p.Artist != "" { | ||||
| 				items = append(items, p.Artist) | ||||
| 			} | ||||
| 		case "ALBUM": | ||||
| 			if p.album != "" { | ||||
| 				items = append(items, p.album) | ||||
| 			if p.Album != "" { | ||||
| 				items = append(items, p.Album) | ||||
| 			} | ||||
| 		case "TITLE": | ||||
| 			if p.title != "" { | ||||
| 				items = append(items, p.title) | ||||
| 			if p.Title != "" { | ||||
| 				items = append(items, p.Title) | ||||
| 			} | ||||
| 		case "POSITION": | ||||
| 			if pos != "" && SHOW_POS { | ||||
| @ -254,12 +98,12 @@ func (p *Player) JSON() string { | ||||
| 
 | ||||
| 	data["tooltip"] = fmt.Sprintf( | ||||
| 		"%s\nby %s\n", | ||||
| 		strings.ReplaceAll(p.title, "&", "&"), | ||||
| 		strings.ReplaceAll(p.artist, "&", "&")) | ||||
| 	if p.album != "" { | ||||
| 		data["tooltip"] += "from " + strings.ReplaceAll(p.album, "&", "&") + "\n" | ||||
| 		strings.ReplaceAll(p.Title, "&", "&"), | ||||
| 		strings.ReplaceAll(p.Artist, "&", "&")) | ||||
| 	if p.Album != "" { | ||||
| 		data["tooltip"] += "from " + strings.ReplaceAll(p.Album, "&", "&") + "\n" | ||||
| 	} | ||||
| 	data["tooltip"] += "(" + p.name + ")" | ||||
| 	data["tooltip"] += "(" + p.Name + ")" | ||||
| 	data["text"] = text | ||||
| 	out, err := json.Marshal(data) | ||||
| 	if err != nil { | ||||
| @ -268,115 +112,22 @@ func (p *Player) JSON() string { | ||||
| 	return string(out) | ||||
| } | ||||
| 
 | ||||
| type availablePlayers struct { | ||||
| 	list    playerArray | ||||
| 	current uint | ||||
| 	conn    *dbus.Conn | ||||
| type players struct { | ||||
| 	mpris2 *mpris2.Mpris2 | ||||
| } | ||||
| 
 | ||||
| type playerArray []*Player | ||||
| 
 | ||||
| func (ls playerArray) Len() int { | ||||
| 	return len(ls) | ||||
| } | ||||
| 
 | ||||
| func (ls playerArray) Less(i, j int) bool { | ||||
| 	var states [2]uint8 | ||||
| 	for i, p := range []bool{ls[i].playing, ls[j].playing} { | ||||
| 		if p { | ||||
| 			states[i] = 1 | ||||
| 		} | ||||
| 	} | ||||
| 	// Reverse order
 | ||||
| 	return states[0] > states[1] | ||||
| } | ||||
| 
 | ||||
| func (ls playerArray) Swap(i, j int) { | ||||
| 	ls[i], ls[j] = ls[j], ls[i] | ||||
| } | ||||
| 
 | ||||
| func (pl *availablePlayers) Remove(fullName string) { | ||||
| 	currentName := pl.list[pl.current].fullName | ||||
| 	var i int | ||||
| 	found := false | ||||
| 	for ind, p := range pl.list { | ||||
| 		if p.fullName == fullName { | ||||
| 			i = ind | ||||
| 			found = true | ||||
| 			break | ||||
| 		} | ||||
| 	} | ||||
| 	if !found { | ||||
| 		return | ||||
| 	} | ||||
| 	pl.list[0], pl.list[i] = pl.list[i], pl.list[0] | ||||
| 	pl.list = pl.list[1:] | ||||
| 	found = false | ||||
| 	for ind, p := range pl.list { | ||||
| 		if p.fullName == currentName { | ||||
| 			pl.current = uint(ind) | ||||
| 			found = true | ||||
| 			break | ||||
| 		} | ||||
| 	} | ||||
| 	if !found { | ||||
| 		pl.current = 0 | ||||
| 		pl.Refresh() | ||||
| 		fmt.Fprintln(WRITER, pl.JSON()) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (pl *availablePlayers) Reload() error { | ||||
| 	var buses []string | ||||
| 	err := pl.conn.BusObject().Call("org.freedesktop.DBus.ListNames", 0).Store(&buses) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	for _, name := range buses { | ||||
| 		// Don't add playerctld, it just duplicates other players
 | ||||
| 		if strings.HasPrefix(name, INTERFACE) && !strings.Contains(name, "playerctld") { | ||||
| 			pl.New(name) | ||||
| 		} | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func (pl *availablePlayers) New(name string) { | ||||
| 	pl.list = append(pl.list, NewPlayer(pl.conn, name)) | ||||
| 	if AUTOFOCUS { | ||||
| 		pl.current = uint(len(pl.list) - 1) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (pl *availablePlayers) Sort() { | ||||
| 	sort.Sort(pl.list) | ||||
| 	pl.current = 0 | ||||
| } | ||||
| 
 | ||||
| func (pl *availablePlayers) Refresh() { | ||||
| 	for i := range pl.list { | ||||
| 		pl.list[i].Refresh() | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (pl *availablePlayers) JSON() string { | ||||
| 	if len(pl.list) != 0 { | ||||
| 		return pl.list[pl.current].JSON() | ||||
| func (pl *players) JSON() string { | ||||
| 	if len(pl.mpris2.List) != 0 { | ||||
| 		return playerJSON(pl.mpris2.List[pl.mpris2.Current]) | ||||
| 	} | ||||
| 	return "{}" | ||||
| } | ||||
| 
 | ||||
| func (pl *availablePlayers) Next() { | ||||
| 	pl.list[pl.current].player.Call(INTERFACE+".Player.Next", 0) | ||||
| } | ||||
| func (pl *players) Next() { pl.mpris2.List[pl.mpris2.Current].Next() } | ||||
| 
 | ||||
| func (pl *availablePlayers) Prev() { | ||||
| 	pl.list[pl.current].player.Call(INTERFACE+".Player.Previous", 0) | ||||
| } | ||||
| func (pl *players) Prev() { pl.mpris2.List[pl.mpris2.Current].Previous() } | ||||
| 
 | ||||
| func (pl *availablePlayers) Toggle() { | ||||
| 	pl.list[pl.current].player.Call(INTERFACE+".Player.PlayPause", 0) | ||||
| } | ||||
| func (pl *players) Toggle() { pl.mpris2.List[pl.mpris2.Current].Toggle() } | ||||
| 
 | ||||
| func main() { | ||||
| 	logfile, err := os.OpenFile(LOGFILE, os.O_CREATE|os.O_APPEND|os.O_RDWR, 0666) | ||||
| @ -421,18 +172,6 @@ func main() { | ||||
| 		} | ||||
| 		os.Exit(0) | ||||
| 	} | ||||
| 	conn, err := dbus.SessionBus() | ||||
| 	if err != nil { | ||||
| 		log.Fatalln("Error connecting to DBus:", err) | ||||
| 	} | ||||
| 	players := &availablePlayers{ | ||||
| 		conn: conn, | ||||
| 	} | ||||
| 	players.Reload() | ||||
| 	players.Sort() | ||||
| 	players.Refresh() | ||||
| 	fmt.Fprintln(WRITER, players.JSON()) | ||||
| 	lastLine := "" | ||||
| 	// fmt.Println("New array", players)
 | ||||
| 	// Start command listener
 | ||||
| 	if _, err := os.Stat(SOCK); err == nil { | ||||
| @ -486,6 +225,16 @@ func main() { | ||||
| 			os.Remove(OUTFILE) | ||||
| 		} | ||||
| 	} | ||||
| 	conn, err := dbus.SessionBus() | ||||
| 	if err != nil { | ||||
| 		log.Fatalln("Error connecting to DBus:", err) | ||||
| 	} | ||||
| 	players := &players{ | ||||
| 		mpris2: mpris2.NewMpris2(conn, INTERPOLATE, POLL, AUTOFOCUS), | ||||
| 	} | ||||
| 	players.mpris2.Reload() | ||||
| 	players.mpris2.Sort() | ||||
| 	lastLine := "" | ||||
| 	go func() { | ||||
| 		listener, err := net.Listen("unix", SOCK) | ||||
| 		c := make(chan os.Signal, 1) | ||||
| @ -518,26 +267,24 @@ func main() { | ||||
| 			command := string(buf[0:nr]) | ||||
| 			switch command { | ||||
| 			case "player-next": | ||||
| 				length := len(players.list) | ||||
| 				length := len(players.mpris2.List) | ||||
| 				if length != 1 { | ||||
| 					if players.current < uint(length-1) { | ||||
| 						players.current++ | ||||
| 					if players.mpris2.Current < uint(length-1) { | ||||
| 						players.mpris2.Current++ | ||||
| 					} else { | ||||
| 						players.current = 0 | ||||
| 						players.mpris2.Current = 0 | ||||
| 					} | ||||
| 					players.Refresh() | ||||
| 					fmt.Fprintln(WRITER, players.JSON()) | ||||
| 					players.mpris2.Refresh() | ||||
| 				} | ||||
| 			case "player-prev": | ||||
| 				length := len(players.list) | ||||
| 				length := len(players.mpris2.List) | ||||
| 				if length != 1 { | ||||
| 					if players.current != 0 { | ||||
| 						players.current-- | ||||
| 					if players.mpris2.Current != 0 { | ||||
| 						players.mpris2.Current-- | ||||
| 					} else { | ||||
| 						players.current = uint(length - 1) | ||||
| 						players.mpris2.Current = uint(length - 1) | ||||
| 					} | ||||
| 					players.Refresh() | ||||
| 					fmt.Fprintln(WRITER, players.JSON()) | ||||
| 					players.mpris2.Refresh() | ||||
| 				} | ||||
| 			case "next": | ||||
| 				players.Next() | ||||
| @ -546,21 +293,7 @@ func main() { | ||||
| 			case "toggle": | ||||
| 				players.Toggle() | ||||
| 			case "list": | ||||
| 				resp := "" | ||||
| 				pad := 0 | ||||
| 				i := len(players.list) | ||||
| 				for i != 0 { | ||||
| 					i /= 10 | ||||
| 					pad++ | ||||
| 				} | ||||
| 				for i, p := range players.list { | ||||
| 					symbol := "" | ||||
| 					if uint(i) == players.current { | ||||
| 						symbol = "*" | ||||
| 					} | ||||
| 					resp += fmt.Sprintf("%0"+strconv.Itoa(pad)+"d%s: Name: %s; Playing: %t; PID: %d\n", i, symbol, p.fullName, p.playing, p.pid) | ||||
| 				} | ||||
| 				con.Write([]byte(resp)) | ||||
| 				con.Write([]byte(players.mpris2.String())) | ||||
| 			case "share": | ||||
| 				out, err := os.Create(OUTFILE) | ||||
| 				if err != nil { | ||||
| @ -573,45 +306,24 @@ func main() { | ||||
| 			} | ||||
| 		} | ||||
| 	}() | ||||
| 
 | ||||
| 	conn.BusObject().Call("org.freedesktop.DBus.AddMatch", 0, MATCH_NOC) | ||||
| 	conn.BusObject().Call("org.freedesktop.DBus.AddMatch", 0, MATCH_PC) | ||||
| 	go players.mpris2.Listen() | ||||
| 	if SHOW_POS { | ||||
| 		go func() { | ||||
| 			for { | ||||
| 				time.Sleep(POLL * time.Second) | ||||
| 				if len(players.list) != 0 { | ||||
| 					if players.list[players.current].playing { | ||||
| 				if len(players.mpris2.List) != 0 { | ||||
| 					if players.mpris2.List[players.mpris2.Current].Playing { | ||||
| 						go fmt.Fprintln(WRITER, players.JSON()) | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| 		}() | ||||
| 	} | ||||
| 	c := make(chan *dbus.Signal, 10) | ||||
| 	conn.Signal(c) | ||||
| 	for v := range c { | ||||
| 		// fmt.Printf("SIGNAL: Sender %s, Path %s, Name %s, Body %s\n", v.Sender, v.Path, v.Name, v.Body)
 | ||||
| 		if strings.Contains(v.Name, "NameOwnerChanged") { | ||||
| 			switch name := v.Body[0].(type) { | ||||
| 			case string: | ||||
| 				var pid uint32 | ||||
| 				conn.BusObject().Call("org.freedesktop.DBus.GetConnectionUnixProcessID", 0, name).Store(&pid) | ||||
| 				// Ignore playerctld again
 | ||||
| 				if strings.Contains(name, INTERFACE) && !strings.Contains(name, "playerctld") { | ||||
| 					if pid == 0 { | ||||
| 						// fmt.Println("Removing", name)
 | ||||
| 						players.Remove(name) | ||||
| 					} else { | ||||
| 						// fmt.Println("Adding", name)
 | ||||
| 						players.New(name) | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| 		} else if strings.Contains(v.Name, "PropertiesChanged") && strings.Contains(v.Body[0].(string), INTERFACE+".Player") { | ||||
| 			players.Refresh() | ||||
| 	fmt.Fprintln(WRITER, players.JSON()) | ||||
| 	for v := range players.mpris2.Messages { | ||||
| 		if v.Name == "refresh" { | ||||
| 			if AUTOFOCUS { | ||||
| 				players.Sort() | ||||
| 				players.mpris2.Sort() | ||||
| 			} | ||||
| 			if l := players.JSON(); l != lastLine { | ||||
| 				lastLine = l | ||||
| @ -619,4 +331,5 @@ func main() { | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	players.mpris2.Refresh() | ||||
| } | ||||
|  | ||||
							
								
								
									
										5
									
								
								mpris2client/go.mod
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								mpris2client/go.mod
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,5 @@ | ||||
| module git.hrfee.pw/hrfee/waybar-mpris/mpris2client | ||||
| 
 | ||||
| go 1.15 | ||||
| 
 | ||||
| require github.com/godbus/dbus/v5 v5.0.3 // indirect | ||||
							
								
								
									
										3
									
								
								mpris2client/go.sum
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								mpris2client/go.sum
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,3 @@ | ||||
| github.com/godbus/dbus v4.1.0+incompatible h1:WqqLRTsQic3apZUK9qC5sGNfXthmPXzUZ7nQPrNITa4= | ||||
| github.com/godbus/dbus/v5 v5.0.3 h1:ZqHaoEF7TBzh4jzPmqVhE/5A1z9of6orkAe5uHoAeME= | ||||
| github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= | ||||
							
								
								
									
										355
									
								
								mpris2client/mpris2.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										355
									
								
								mpris2client/mpris2.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,355 @@ | ||||
| package mpris2client | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"io/ioutil" | ||||
| 	"sort" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| 
 | ||||
| 	"github.com/godbus/dbus/v5" | ||||
| ) | ||||
| 
 | ||||
| // Various paths and values to use elsewhere.
 | ||||
| const ( | ||||
| 	INTERFACE = "org.mpris.MediaPlayer2" | ||||
| 	PATH      = "/org/mpris/MediaPlayer2" | ||||
| 	// For the NameOwnerChanged signal.
 | ||||
| 	MATCH_NOC = "type='signal',path='/org/freedesktop/DBus',interface='org.freedesktop.DBus',member='NameOwnerChanged'" | ||||
| 	// For the PropertiesChanged signal. It doesn't match exactly (couldn't get that to work) so we check it manually.
 | ||||
| 	MATCH_PC = "type='signal',path='/org/mpris/MediaPlayer2',interface='org.freedesktop.DBus.Properties'" | ||||
| 	Refresh  = "refresh" | ||||
| ) | ||||
| 
 | ||||
| var knownPlayers = map[string]string{ | ||||
| 	"plasma-browser-integration": "Browser", | ||||
| 	"noson":                      "Noson", | ||||
| } | ||||
| 
 | ||||
| var knownBrowsers = map[string]string{ | ||||
| 	"mozilla":  "Firefox", | ||||
| 	"chrome":   "Chrome", | ||||
| 	"chromium": "Chromium", | ||||
| } | ||||
| 
 | ||||
| // Player represents an active media player.
 | ||||
| type Player struct { | ||||
| 	Player                                            dbus.BusObject | ||||
| 	FullName, Name, Title, Artist, AlbumArtist, Album string | ||||
| 	Position                                          int64 | ||||
| 	pid                                               uint32 | ||||
| 	Playing, Stopped                                  bool | ||||
| 	metadata                                          map[string]dbus.Variant | ||||
| 	conn                                              *dbus.Conn | ||||
| 	poll                                              int | ||||
| 	interpolate                                       bool | ||||
| } | ||||
| 
 | ||||
| // NewPlayer returns a new player object.
 | ||||
| func NewPlayer(conn *dbus.Conn, name string, interpolate bool, poll int) (p *Player) { | ||||
| 	playerName := strings.ReplaceAll(name, INTERFACE+".", "") | ||||
| 	var pid uint32 | ||||
| 	conn.BusObject().Call("org.freedesktop.DBus.GetConnectionUnixProcessID", 0, name).Store(&pid) | ||||
| 	for key, val := range knownPlayers { | ||||
| 		if strings.Contains(name, key) { | ||||
| 			playerName = val | ||||
| 			break | ||||
| 		} | ||||
| 	} | ||||
| 	if playerName == "Browser" { | ||||
| 		file, err := ioutil.ReadFile(fmt.Sprintf("/proc/%d/cmdline", pid)) | ||||
| 		if err == nil { | ||||
| 			cmd := string(file) | ||||
| 			for key, val := range knownBrowsers { | ||||
| 				if strings.Contains(cmd, key) { | ||||
| 					playerName = val | ||||
| 					break | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	p = &Player{ | ||||
| 		Player:      conn.Object(name, PATH), | ||||
| 		conn:        conn, | ||||
| 		Name:        playerName, | ||||
| 		FullName:    name, | ||||
| 		pid:         pid, | ||||
| 		interpolate: interpolate, | ||||
| 		poll:        poll, | ||||
| 	} | ||||
| 	p.Refresh() | ||||
| 	return | ||||
| } | ||||
| 
 | ||||
| func (p *Player) String() string { | ||||
| 	return fmt.Sprintf("Name: %s; Playing: %t; PID: %d", p.FullName, p.Playing, p.pid) | ||||
| } | ||||
| 
 | ||||
| // Refresh grabs playback info.
 | ||||
| func (p *Player) Refresh() (err error) { | ||||
| 	val, err := p.Player.GetProperty(INTERFACE + ".Player.PlaybackStatus") | ||||
| 	if err != nil { | ||||
| 		p.Playing = false | ||||
| 		p.Stopped = false | ||||
| 		p.metadata = map[string]dbus.Variant{} | ||||
| 		p.Title = "" | ||||
| 		p.Artist = "" | ||||
| 		p.AlbumArtist = "" | ||||
| 		p.Album = "" | ||||
| 		return | ||||
| 	} | ||||
| 	strVal := val.String() | ||||
| 	if strings.Contains(strVal, "Playing") { | ||||
| 		p.Playing = true | ||||
| 		p.Stopped = false | ||||
| 	} else if strings.Contains(strVal, "Paused") { | ||||
| 		p.Playing = false | ||||
| 		p.Stopped = false | ||||
| 	} else { | ||||
| 		p.Playing = false | ||||
| 		p.Stopped = true | ||||
| 	} | ||||
| 	metadata, err := p.Player.GetProperty(INTERFACE + ".Player.Metadata") | ||||
| 	if err != nil { | ||||
| 		p.metadata = map[string]dbus.Variant{} | ||||
| 		p.Title = "" | ||||
| 		p.Artist = "" | ||||
| 		p.AlbumArtist = "" | ||||
| 		p.Album = "" | ||||
| 		return | ||||
| 	} | ||||
| 	p.metadata = metadata.Value().(map[string]dbus.Variant) | ||||
| 	switch artist := p.metadata["xesam:artist"].Value().(type) { | ||||
| 	case []string: | ||||
| 		p.Artist = strings.Join(artist, ", ") | ||||
| 	case string: | ||||
| 		p.Artist = artist | ||||
| 	default: | ||||
| 		p.Artist = "" | ||||
| 	} | ||||
| 	switch albumArtist := p.metadata["xesam:albumArtist"].Value().(type) { | ||||
| 	case []string: | ||||
| 		p.AlbumArtist = strings.Join(albumArtist, ", ") | ||||
| 	case string: | ||||
| 		p.AlbumArtist = albumArtist | ||||
| 	default: | ||||
| 		p.AlbumArtist = "" | ||||
| 	} | ||||
| 	switch title := p.metadata["xesam:title"].Value().(type) { | ||||
| 	case string: | ||||
| 		p.Title = title | ||||
| 	default: | ||||
| 		p.Title = "" | ||||
| 	} | ||||
| 	switch album := p.metadata["xesam:album"].Value().(type) { | ||||
| 	case string: | ||||
| 		p.Album = album | ||||
| 	default: | ||||
| 		p.Album = "" | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func µsToString(µs int64) string { | ||||
| 	seconds := int(µs / 1e6) | ||||
| 	minutes := int(seconds / 60) | ||||
| 	seconds -= minutes * 60 | ||||
| 	return fmt.Sprintf("%02d:%02d", minutes, seconds) | ||||
| } | ||||
| 
 | ||||
| // StringPosition figures out the track position in MM:SS/MM:SS, interpolating the value if necessary.
 | ||||
| func (p *Player) StringPosition() string { | ||||
| 	// position is in microseconds so we prob need int64 to be safe
 | ||||
| 	v := p.metadata["mpris:length"].Value() | ||||
| 	var l int64 | ||||
| 	if v != nil { | ||||
| 		l = v.(int64) | ||||
| 	} else { | ||||
| 		return "" | ||||
| 	} | ||||
| 	length := µsToString(l) | ||||
| 	if length == "" { | ||||
| 		return "" | ||||
| 	} | ||||
| 	pos, err := p.Player.GetProperty(INTERFACE + ".Player.Position") | ||||
| 	if err != nil { | ||||
| 		return "" | ||||
| 	} | ||||
| 	position := µsToString(pos.Value().(int64)) | ||||
| 	if position == "" { | ||||
| 		return "" | ||||
| 	} | ||||
| 	if p.interpolate && position == µsToString(p.Position) { | ||||
| 		np := p.Position + int64(p.poll*1e6) | ||||
| 		position = µsToString(np) | ||||
| 	} | ||||
| 	p.Position = pos.Value().(int64) | ||||
| 	return position + "/" + length | ||||
| } | ||||
| 
 | ||||
| // Next requests the next track.
 | ||||
| func (p *Player) Next() { p.Player.Call(INTERFACE+".Player.Next", 0) } | ||||
| 
 | ||||
| // Previous requests the previous track.
 | ||||
| func (p *Player) Previous() { p.Player.Call(INTERFACE+".Player.Previous", 0) } | ||||
| 
 | ||||
| // Toggle requests play/pause
 | ||||
| func (p *Player) Toggle() { p.Player.Call(INTERFACE+".Player.PlayPause", 0) } | ||||
| 
 | ||||
| type Message struct { | ||||
| 	Name, Value string | ||||
| } | ||||
| 
 | ||||
| type PlayerArray []*Player | ||||
| 
 | ||||
| func (ls PlayerArray) Len() int { | ||||
| 	return len(ls) | ||||
| } | ||||
| 
 | ||||
| func (ls PlayerArray) Less(i, j int) bool { | ||||
| 	var states [2]uint8 | ||||
| 	for i, p := range []bool{ls[i].Playing, ls[j].Playing} { | ||||
| 		if p { | ||||
| 			states[i] = 1 | ||||
| 		} | ||||
| 	} | ||||
| 	// Reverse order
 | ||||
| 	return states[0] > states[1] | ||||
| } | ||||
| 
 | ||||
| func (ls PlayerArray) Swap(i, j int) { | ||||
| 	ls[i], ls[j] = ls[j], ls[i] | ||||
| } | ||||
| 
 | ||||
| type Mpris2 struct { | ||||
| 	List        PlayerArray | ||||
| 	Current     uint | ||||
| 	conn        *dbus.Conn | ||||
| 	Messages    chan Message | ||||
| 	interpolate bool | ||||
| 	poll        int | ||||
| 	autofocus   bool | ||||
| } | ||||
| 
 | ||||
| func NewMpris2(conn *dbus.Conn, interpolate bool, poll int, autofocus bool) *Mpris2 { | ||||
| 	return &Mpris2{ | ||||
| 		List:        PlayerArray{}, | ||||
| 		Current:     0, | ||||
| 		conn:        conn, | ||||
| 		Messages:    make(chan Message), | ||||
| 		interpolate: interpolate, | ||||
| 		poll:        poll, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // Listen should be run as a Goroutine. When players become available or are removed, an mpris2.Message is sent on mpris2.Mpris2.Messages with Name "add"/"remove" and Value as the player name. When a players state changes, a message is sent on mpris2.Mpris2.Messages with Name "refresh".
 | ||||
| func (pl *Mpris2) Listen() { | ||||
| 	c := make(chan *dbus.Signal, 10) | ||||
| 	pl.conn.BusObject().Call("org.freedesktop.DBus.AddMatch", 0, MATCH_NOC) | ||||
| 	pl.conn.BusObject().Call("org.freedesktop.DBus.AddMatch", 0, MATCH_PC) | ||||
| 	pl.conn.Signal(c) | ||||
| 	for v := range c { | ||||
| 		if strings.Contains(v.Name, "NameOwnerChanged") { | ||||
| 			switch name := v.Body[0].(type) { | ||||
| 			case string: | ||||
| 				var pid uint32 | ||||
| 				pl.conn.BusObject().Call("org.freedesktop.DBus.GetConnectionUnixProcessID", 0, name).Store(&pid) | ||||
| 				// Ignore playerctld again
 | ||||
| 				if strings.Contains(name, INTERFACE) && !strings.Contains(name, "playerctld") { | ||||
| 					if pid == 0 { | ||||
| 						pl.Remove(name) | ||||
| 						pl.Messages <- Message{Name: "remove", Value: name} | ||||
| 					} else { | ||||
| 						pl.New(name) | ||||
| 						pl.Messages <- Message{Name: "add", Value: name} | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| 		} else if strings.Contains(v.Name, "PropertiesChanged") && strings.Contains(v.Body[0].(string), INTERFACE+".Player") { | ||||
| 			pl.Refresh() | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (pl *Mpris2) Remove(fullName string) { | ||||
| 	currentName := pl.List[pl.Current].FullName | ||||
| 	var i int | ||||
| 	found := false | ||||
| 	for ind, p := range pl.List { | ||||
| 		if p.FullName == fullName { | ||||
| 			i = ind | ||||
| 			found = true | ||||
| 			break | ||||
| 		} | ||||
| 	} | ||||
| 	if !found { | ||||
| 		return | ||||
| 	} | ||||
| 	pl.List[0], pl.List[i] = pl.List[i], pl.List[0] | ||||
| 	pl.List = pl.List[1:] | ||||
| 	found = false | ||||
| 	for ind, p := range pl.List { | ||||
| 		if p.FullName == currentName { | ||||
| 			pl.Current = uint(ind) | ||||
| 			found = true | ||||
| 			break | ||||
| 		} | ||||
| 	} | ||||
| 	if !found { | ||||
| 		pl.Current = 0 | ||||
| 		pl.Refresh() | ||||
| 		//fmt.Fprintln(WRITER, pl.JSON())
 | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (pl *Mpris2) Reload() error { | ||||
| 	var buses []string | ||||
| 	err := pl.conn.BusObject().Call("org.freedesktop.DBus.ListNames", 0).Store(&buses) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	for _, name := range buses { | ||||
| 		// Don't add playerctld, it just duplicates other players
 | ||||
| 		if strings.HasPrefix(name, INTERFACE) && !strings.Contains(name, "playerctld") { | ||||
| 			pl.New(name) | ||||
| 		} | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func (pl *Mpris2) String() string { | ||||
| 	resp := "" | ||||
| 	pad := 0 | ||||
| 	i := len(pl.List) | ||||
| 	for i != 0 { | ||||
| 		i /= 10 | ||||
| 		pad++ | ||||
| 	} | ||||
| 	for i, p := range pl.List { | ||||
| 		symbol := "" | ||||
| 		if uint(i) == pl.Current { | ||||
| 			symbol = "*" | ||||
| 		} | ||||
| 		resp += fmt.Sprintf("%0"+strconv.Itoa(pad)+"d", i) + symbol + ": " + p.String() + "\n" | ||||
| 	} | ||||
| 	return resp | ||||
| } | ||||
| 
 | ||||
| func (pl *Mpris2) New(name string) { | ||||
| 	pl.List = append(pl.List, NewPlayer(pl.conn, name, pl.interpolate, pl.poll)) | ||||
| 	if pl.autofocus { | ||||
| 		pl.Current = uint(len(pl.List) - 1) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (pl *Mpris2) Sort() { | ||||
| 	sort.Sort(pl.List) | ||||
| 	pl.Current = 0 | ||||
| } | ||||
| 
 | ||||
| func (pl *Mpris2) Refresh() { | ||||
| 	for i := range pl.List { | ||||
| 		pl.List[i].Refresh() | ||||
| 	} | ||||
| 	pl.Messages <- Message{Name: "refresh", Value: ""} | ||||
| } | ||||
							
								
								
									
										
											BIN
										
									
								
								waybar-mpris
									
									
									
									
									
								
							
							
						
						
									
										
											BIN
										
									
								
								waybar-mpris
									
									
									
									
									
								
							
										
											Binary file not shown.
										
									
								
							
		Loading…
	
		Reference in New Issue
	
	Block a user