diff --git a/go.mod b/go.mod index da66dc9..0b54c25 100644 --- a/go.mod +++ b/go.mod @@ -2,12 +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/hrfee/mpris2client v0.0.0-20210108004725-d2a36745cc4a github.com/spf13/pflag v1.0.5 golang.org/x/sys v0.0.0-20201116194326-cc9327a14d48 // indirect gopkg.in/fsnotify.v1 v1.4.7 // indirect diff --git a/go.sum b/go.sum index af5c866..da8938a 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,8 @@ github.com/godbus/dbus/v5 v5.0.3 h1:ZqHaoEF7TBzh4jzPmqVhE/5A1z9of6orkAe5uHoAeME= github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/hrfee/mpris2client v0.0.0-20210108004725-d2a36745cc4a h1:3H1awMdF3eJBCg0QPpDtNW+ujQPvUCuLsqlMRoODMA4= +github.com/hrfee/mpris2client v0.0.0-20210108004725-d2a36745cc4a/go.mod h1:tVpzzAlsljmQevNA4mJwUy1onUUiaQcRAa4gdl37okY= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= golang.org/x/sys v0.0.0-20200928205150-006507a75852 h1:sXxgOAXy8JwHhZnPuItAlUtwIlxrlEqi28mKhUR+zZY= diff --git a/main.go b/main.go index ab103d6..4135e78 100644 --- a/main.go +++ b/main.go @@ -11,9 +11,9 @@ import ( "strings" "time" - mpris2 "git.hrfee.pw/hrfee/waybar-mpris/mpris2client" "github.com/godbus/dbus/v5" "github.com/hpcloud/tail" + mpris2 "github.com/hrfee/mpris2client" flag "github.com/spf13/pflag" ) diff --git a/mpris2client/go.mod b/mpris2client/go.mod deleted file mode 100644 index 75c202d..0000000 --- a/mpris2client/go.mod +++ /dev/null @@ -1,5 +0,0 @@ -module git.hrfee.pw/hrfee/waybar-mpris/mpris2client - -go 1.15 - -require github.com/godbus/dbus/v5 v5.0.3 // indirect diff --git a/mpris2client/go.sum b/mpris2client/go.sum deleted file mode 100644 index 4eefd05..0000000 --- a/mpris2client/go.sum +++ /dev/null @@ -1,3 +0,0 @@ -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= diff --git a/mpris2client/mpris2.go b/mpris2client/mpris2.go deleted file mode 100644 index e990283..0000000 --- a/mpris2client/mpris2.go +++ /dev/null @@ -1,365 +0,0 @@ -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 - // playerctld mirrors property changes of other players, so we store its UID here to ignore it. - playerctldUID string -} - -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 - if strings.Contains(name, "playerctld") { - // Store UID so we know to ignore it later - pl.playerctldUID = v.Sender - } else if strings.Contains(name, INTERFACE) { - 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") && v.Sender != pl.playerctldUID { - 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.Contains(name, "playerctld") { - // Store its UID - uid := "" - pl.conn.BusObject().Call("org.freedesktop.DBus.GetNameOwner", 0, name).Store(&uid) - pl.playerctldUID = uid - } else if strings.HasPrefix(name, INTERFACE) { - 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: ""} -} diff --git a/waybar-mpris b/waybar-mpris index a5c862d..8a574d7 100755 Binary files a/waybar-mpris and b/waybar-mpris differ