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: | MPRIS2 is widely supported, so this component should work with: | ||||||
| * Chrome/Chromium | * Chrome/Chromium | ||||||
|  | * Firefox (Potentially, with `media.hardwaremediakeys.enabled = true` in about:config) | ||||||
| * Other browsers (with kde plasma integration installed) | * Other browsers (with kde plasma integration installed) | ||||||
| * VLC | * VLC | ||||||
| * Spotify | * Spotify | ||||||
|  | |||||||
							
								
								
									
										3
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										3
									
								
								go.mod
									
									
									
									
									
								
							| @ -2,7 +2,10 @@ module git.hrfee.pw/hrfee/waybar-mpris | |||||||
| 
 | 
 | ||||||
| go 1.15 | go 1.15 | ||||||
| 
 | 
 | ||||||
|  | replace git.hrfee.pw/hrfee/waybar-mpris/mpris2client => ./mpris2client | ||||||
|  | 
 | ||||||
| require ( | require ( | ||||||
|  | 	git.hrfee.pw/hrfee/waybar-mpris/mpris2client v0.0.0-00010101000000-000000000000 | ||||||
| 	github.com/godbus/dbus/v5 v5.0.3 | 	github.com/godbus/dbus/v5 v5.0.3 | ||||||
| 	github.com/hpcloud/tail v1.0.0 | 	github.com/hpcloud/tail v1.0.0 | ||||||
| 	github.com/spf13/pflag v1.0.5 | 	github.com/spf13/pflag v1.0.5 | ||||||
|  | |||||||
							
								
								
									
										391
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										391
									
								
								main.go
									
									
									
									
									
								
							| @ -4,51 +4,21 @@ import ( | |||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"io" | 	"io" | ||||||
| 	"io/ioutil" |  | ||||||
| 	"log" | 	"log" | ||||||
| 	"net" | 	"net" | ||||||
| 	"os" | 	"os" | ||||||
| 	"os/signal" | 	"os/signal" | ||||||
| 	"sort" |  | ||||||
| 	"strconv" |  | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"time" | 	"time" | ||||||
| 
 | 
 | ||||||
|  | 	mpris2 "git.hrfee.pw/hrfee/waybar-mpris/mpris2client" | ||||||
| 	"github.com/godbus/dbus/v5" | 	"github.com/godbus/dbus/v5" | ||||||
| 	"github.com/hpcloud/tail" | 	"github.com/hpcloud/tail" | ||||||
| 	flag "github.com/spf13/pflag" | 	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.
 | // Various paths and values to use elsewhere.
 | ||||||
| const ( | 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" | 	SOCK    = "/tmp/waybar-mpris.sock" | ||||||
| 	LOGFILE = "/tmp/waybar-mpris.log" | 	LOGFILE = "/tmp/waybar-mpris.log" | ||||||
| 	OUTFILE = "/tmp/waybar-mpris.out" | 	OUTFILE = "/tmp/waybar-mpris.out" | ||||||
| @ -70,144 +40,18 @@ var ( | |||||||
| 	WRITER      io.Writer = os.Stdout | 	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.
 | // JSON returns json for waybar to consume.
 | ||||||
| func (p *Player) JSON() string { | func playerJSON(p *mpris2.Player) string { | ||||||
| 	data := map[string]string{} | 	data := map[string]string{} | ||||||
| 	symbol := PLAY | 	symbol := PLAY | ||||||
| 	data["class"] = "paused" | 	data["class"] = "paused" | ||||||
| 	if p.playing { | 	if p.Playing { | ||||||
| 		symbol = PAUSE | 		symbol = PAUSE | ||||||
| 		data["class"] = "playing" | 		data["class"] = "playing" | ||||||
| 	} | 	} | ||||||
| 	var pos string | 	var pos string | ||||||
| 	if SHOW_POS { | 	if SHOW_POS { | ||||||
| 		pos = p.Position() | 		pos = p.StringPosition() | ||||||
| 		if pos != "" { | 		if pos != "" { | ||||||
| 			pos = "(" + pos + ")" | 			pos = "(" + pos + ")" | ||||||
| 		} | 		} | ||||||
| @ -219,16 +63,16 @@ func (p *Player) JSON() string { | |||||||
| 		case "SYMBOL": | 		case "SYMBOL": | ||||||
| 			items = append(items, symbol) | 			items = append(items, symbol) | ||||||
| 		case "ARTIST": | 		case "ARTIST": | ||||||
| 			if p.artist != "" { | 			if p.Artist != "" { | ||||||
| 				items = append(items, p.artist) | 				items = append(items, p.Artist) | ||||||
| 			} | 			} | ||||||
| 		case "ALBUM": | 		case "ALBUM": | ||||||
| 			if p.album != "" { | 			if p.Album != "" { | ||||||
| 				items = append(items, p.album) | 				items = append(items, p.Album) | ||||||
| 			} | 			} | ||||||
| 		case "TITLE": | 		case "TITLE": | ||||||
| 			if p.title != "" { | 			if p.Title != "" { | ||||||
| 				items = append(items, p.title) | 				items = append(items, p.Title) | ||||||
| 			} | 			} | ||||||
| 		case "POSITION": | 		case "POSITION": | ||||||
| 			if pos != "" && SHOW_POS { | 			if pos != "" && SHOW_POS { | ||||||
| @ -254,12 +98,12 @@ func (p *Player) JSON() string { | |||||||
| 
 | 
 | ||||||
| 	data["tooltip"] = fmt.Sprintf( | 	data["tooltip"] = fmt.Sprintf( | ||||||
| 		"%s\nby %s\n", | 		"%s\nby %s\n", | ||||||
| 		strings.ReplaceAll(p.title, "&", "&"), | 		strings.ReplaceAll(p.Title, "&", "&"), | ||||||
| 		strings.ReplaceAll(p.artist, "&", "&")) | 		strings.ReplaceAll(p.Artist, "&", "&")) | ||||||
| 	if p.album != "" { | 	if p.Album != "" { | ||||||
| 		data["tooltip"] += "from " + strings.ReplaceAll(p.album, "&", "&") + "\n" | 		data["tooltip"] += "from " + strings.ReplaceAll(p.Album, "&", "&") + "\n" | ||||||
| 	} | 	} | ||||||
| 	data["tooltip"] += "(" + p.name + ")" | 	data["tooltip"] += "(" + p.Name + ")" | ||||||
| 	data["text"] = text | 	data["text"] = text | ||||||
| 	out, err := json.Marshal(data) | 	out, err := json.Marshal(data) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| @ -268,115 +112,22 @@ func (p *Player) JSON() string { | |||||||
| 	return string(out) | 	return string(out) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| type availablePlayers struct { | type players struct { | ||||||
| 	list    playerArray | 	mpris2 *mpris2.Mpris2 | ||||||
| 	current uint |  | ||||||
| 	conn    *dbus.Conn |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| type playerArray []*Player | func (pl *players) JSON() string { | ||||||
| 
 | 	if len(pl.mpris2.List) != 0 { | ||||||
| func (ls playerArray) Len() int { | 		return playerJSON(pl.mpris2.List[pl.mpris2.Current]) | ||||||
| 	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() |  | ||||||
| 	} | 	} | ||||||
| 	return "{}" | 	return "{}" | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (pl *availablePlayers) Next() { | func (pl *players) Next() { pl.mpris2.List[pl.mpris2.Current].Next() } | ||||||
| 	pl.list[pl.current].player.Call(INTERFACE+".Player.Next", 0) |  | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| func (pl *availablePlayers) Prev() { | func (pl *players) Prev() { pl.mpris2.List[pl.mpris2.Current].Previous() } | ||||||
| 	pl.list[pl.current].player.Call(INTERFACE+".Player.Previous", 0) |  | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| func (pl *availablePlayers) Toggle() { | func (pl *players) Toggle() { pl.mpris2.List[pl.mpris2.Current].Toggle() } | ||||||
| 	pl.list[pl.current].player.Call(INTERFACE+".Player.PlayPause", 0) |  | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| func main() { | func main() { | ||||||
| 	logfile, err := os.OpenFile(LOGFILE, os.O_CREATE|os.O_APPEND|os.O_RDWR, 0666) | 	logfile, err := os.OpenFile(LOGFILE, os.O_CREATE|os.O_APPEND|os.O_RDWR, 0666) | ||||||
| @ -421,18 +172,6 @@ func main() { | |||||||
| 		} | 		} | ||||||
| 		os.Exit(0) | 		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)
 | 	// fmt.Println("New array", players)
 | ||||||
| 	// Start command listener
 | 	// Start command listener
 | ||||||
| 	if _, err := os.Stat(SOCK); err == nil { | 	if _, err := os.Stat(SOCK); err == nil { | ||||||
| @ -486,6 +225,16 @@ func main() { | |||||||
| 			os.Remove(OUTFILE) | 			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() { | 	go func() { | ||||||
| 		listener, err := net.Listen("unix", SOCK) | 		listener, err := net.Listen("unix", SOCK) | ||||||
| 		c := make(chan os.Signal, 1) | 		c := make(chan os.Signal, 1) | ||||||
| @ -518,26 +267,24 @@ func main() { | |||||||
| 			command := string(buf[0:nr]) | 			command := string(buf[0:nr]) | ||||||
| 			switch command { | 			switch command { | ||||||
| 			case "player-next": | 			case "player-next": | ||||||
| 				length := len(players.list) | 				length := len(players.mpris2.List) | ||||||
| 				if length != 1 { | 				if length != 1 { | ||||||
| 					if players.current < uint(length-1) { | 					if players.mpris2.Current < uint(length-1) { | ||||||
| 						players.current++ | 						players.mpris2.Current++ | ||||||
| 					} else { | 					} else { | ||||||
| 						players.current = 0 | 						players.mpris2.Current = 0 | ||||||
| 					} | 					} | ||||||
| 					players.Refresh() | 					players.mpris2.Refresh() | ||||||
| 					fmt.Fprintln(WRITER, players.JSON()) |  | ||||||
| 				} | 				} | ||||||
| 			case "player-prev": | 			case "player-prev": | ||||||
| 				length := len(players.list) | 				length := len(players.mpris2.List) | ||||||
| 				if length != 1 { | 				if length != 1 { | ||||||
| 					if players.current != 0 { | 					if players.mpris2.Current != 0 { | ||||||
| 						players.current-- | 						players.mpris2.Current-- | ||||||
| 					} else { | 					} else { | ||||||
| 						players.current = uint(length - 1) | 						players.mpris2.Current = uint(length - 1) | ||||||
| 					} | 					} | ||||||
| 					players.Refresh() | 					players.mpris2.Refresh() | ||||||
| 					fmt.Fprintln(WRITER, players.JSON()) |  | ||||||
| 				} | 				} | ||||||
| 			case "next": | 			case "next": | ||||||
| 				players.Next() | 				players.Next() | ||||||
| @ -546,21 +293,7 @@ func main() { | |||||||
| 			case "toggle": | 			case "toggle": | ||||||
| 				players.Toggle() | 				players.Toggle() | ||||||
| 			case "list": | 			case "list": | ||||||
| 				resp := "" | 				con.Write([]byte(players.mpris2.String())) | ||||||
| 				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)) |  | ||||||
| 			case "share": | 			case "share": | ||||||
| 				out, err := os.Create(OUTFILE) | 				out, err := os.Create(OUTFILE) | ||||||
| 				if err != nil { | 				if err != nil { | ||||||
| @ -573,45 +306,24 @@ func main() { | |||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 	}() | 	}() | ||||||
| 
 | 	go players.mpris2.Listen() | ||||||
| 	conn.BusObject().Call("org.freedesktop.DBus.AddMatch", 0, MATCH_NOC) |  | ||||||
| 	conn.BusObject().Call("org.freedesktop.DBus.AddMatch", 0, MATCH_PC) |  | ||||||
| 	if SHOW_POS { | 	if SHOW_POS { | ||||||
| 		go func() { | 		go func() { | ||||||
| 			for { | 			for { | ||||||
| 				time.Sleep(POLL * time.Second) | 				time.Sleep(POLL * time.Second) | ||||||
| 				if len(players.list) != 0 { | 				if len(players.mpris2.List) != 0 { | ||||||
| 					if players.list[players.current].playing { | 					if players.mpris2.List[players.mpris2.Current].Playing { | ||||||
| 						go fmt.Fprintln(WRITER, players.JSON()) | 						go fmt.Fprintln(WRITER, players.JSON()) | ||||||
| 					} | 					} | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
| 		}() | 		}() | ||||||
| 	} | 	} | ||||||
| 	c := make(chan *dbus.Signal, 10) | 	fmt.Fprintln(WRITER, players.JSON()) | ||||||
| 	conn.Signal(c) | 	for v := range players.mpris2.Messages { | ||||||
| 	for v := range c { | 		if v.Name == "refresh" { | ||||||
| 		// 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() |  | ||||||
| 			if AUTOFOCUS { | 			if AUTOFOCUS { | ||||||
| 				players.Sort() | 				players.mpris2.Sort() | ||||||
| 			} | 			} | ||||||
| 			if l := players.JSON(); l != lastLine { | 			if l := players.JSON(); l != lastLine { | ||||||
| 				lastLine = l | 				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