2020-08-21 13:54:32 +00:00
package main
import (
"encoding/json"
"fmt"
2020-08-31 19:37:23 +00:00
"io"
2020-08-23 15:25:27 +00:00
"io/ioutil"
2020-08-22 18:01:51 +00:00
"log"
"net"
"os"
2020-09-06 12:13:36 +00:00
"os/signal"
2020-08-22 18:01:51 +00:00
"sort"
2020-08-27 19:26:01 +00:00
"strconv"
2020-08-22 18:01:51 +00:00
"strings"
2020-08-26 21:10:36 +00:00
"time"
2020-08-25 13:40:56 +00:00
"github.com/godbus/dbus/v5"
flag "github.com/spf13/pflag"
2020-08-21 13:54:32 +00:00
)
2020-08-22 13:39:23 +00:00
var knownPlayers = map [ string ] string {
"plasma-browser-integration" : "Browser" ,
"noson" : "Noson" ,
}
2020-08-23 15:25:27 +00:00
var knownBrowsers = map [ string ] string {
"mozilla" : "Firefox" ,
"chrome" : "Chrome" ,
"chromium" : "Chromium" ,
}
2020-08-21 13:54:32 +00:00
type Player struct {
2020-08-22 13:39:23 +00:00
player dbus . BusObject
fullName , name , title , artist , album string
2020-09-14 15:14:30 +00:00
position int64
2020-08-27 19:26:01 +00:00
pid uint32
2020-08-22 13:39:23 +00:00
playing , stopped bool
metadata map [ string ] dbus . Variant
conn * dbus . Conn
2020-08-21 13:54:32 +00:00
}
2020-08-22 13:39:23 +00:00
const (
INTERFACE = "org.mpris.MediaPlayer2"
PATH = "/org/mpris/MediaPlayer2"
// NameOwnerChanged
MATCH_NOC = "type='signal',path='/org/freedesktop/DBus',interface='org.freedesktop.DBus',member='NameOwnerChanged'"
// PropertiesChanged
MATCH_PC = "type='signal',path='/org/mpris/MediaPlayer2',interface='org.freedesktop.DBus.Properties'"
2020-08-22 18:01:51 +00:00
SOCK = "/tmp/waybar-mpris.sock"
2020-08-31 19:37:23 +00:00
LOGFILE = "/tmp/waybar-mpris.log"
2020-09-14 15:14:30 +00:00
POLL = 1
2020-08-22 13:39:23 +00:00
)
2020-08-22 14:40:24 +00:00
var (
2020-09-14 15:14:30 +00:00
PLAY = "▶"
PAUSE = ""
SEP = " - "
ORDER = "SYMBOL:ARTIST:ALBUM:TITLE:POSITION"
AUTOFOCUS = false
COMMANDS = [ ] string { "player-next" , "player-prev" , "next" , "prev" , "toggle" , "list" }
SHOW_POS = false
INTERPOLATE = false
2020-08-22 14:40:24 +00:00
)
2020-08-22 13:39:23 +00:00
// NewPlayer returns a new player object.
func NewPlayer ( conn * dbus . Conn , name string ) ( p * Player ) {
playerName := strings . ReplaceAll ( name , INTERFACE + "." , "" )
2020-08-27 19:26:01 +00:00
var pid uint32
conn . BusObject ( ) . Call ( "org.freedesktop.DBus.GetConnectionUnixProcessID" , 0 , name ) . Store ( & pid )
2020-08-22 13:39:23 +00:00
for key , val := range knownPlayers {
if strings . Contains ( name , key ) {
playerName = val
2020-08-31 19:14:14 +00:00
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
2020-08-23 15:25:27 +00:00
}
}
2020-08-22 13:39:23 +00:00
}
}
p = & Player {
player : conn . Object ( name , PATH ) ,
conn : conn ,
name : playerName ,
fullName : name ,
2020-08-27 19:26:01 +00:00
pid : pid ,
2020-08-21 13:54:32 +00:00
}
p . Refresh ( )
2020-08-22 13:39:23 +00:00
return
2020-08-21 13:54:32 +00:00
}
2020-08-22 13:39:23 +00:00
// Refresh grabs playback info.
func ( p * Player ) Refresh ( ) ( err error ) {
val , err := p . player . GetProperty ( INTERFACE + ".Player.PlaybackStatus" )
2020-08-21 13:54:32 +00:00
if err != nil {
2020-08-22 13:39:23 +00:00
p . playing = false
p . stopped = false
p . metadata = map [ string ] dbus . Variant { }
p . title = ""
p . artist = ""
p . album = ""
return
2020-08-21 13:54:32 +00:00
}
2020-08-22 13:39:23 +00:00
strVal := val . String ( )
if strings . Contains ( strVal , "Playing" ) {
2020-08-21 13:54:32 +00:00
p . playing = true
p . stopped = false
2020-08-22 13:39:23 +00:00
} else if strings . Contains ( strVal , "Paused" ) {
2020-08-21 13:54:32 +00:00
p . playing = false
p . stopped = false
} else {
p . playing = false
p . stopped = true
}
2020-08-22 13:39:23 +00:00
metadata , err := p . player . GetProperty ( INTERFACE + ".Player.Metadata" )
2020-08-21 13:54:32 +00:00
if err != nil {
2020-08-22 13:39:23 +00:00
p . metadata = map [ string ] dbus . Variant { }
p . title = ""
p . artist = ""
p . album = ""
2020-08-21 13:54:32 +00:00
return
}
2020-08-22 13:39:23 +00:00
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
2020-08-21 13:54:32 +00:00
}
2020-08-26 21:10:36 +00:00
func µsToString ( µs int64 ) string {
seconds := int ( µs / 1e6 )
minutes := int ( seconds / 60 )
seconds -= minutes * 60
return fmt . Sprintf ( "%02d:%02d" , minutes , seconds )
}
2020-09-14 15:14:30 +00:00
// inc is the increment in seconds to add if the player reports an identical position.
2020-08-26 21:10:36 +00:00
func ( p * Player ) Position ( ) string {
// position is in microseconds so we prob need int64 to be safe
2020-09-11 20:24:11 +00:00
v := p . metadata [ "mpris:length" ] . Value ( )
var l int64
if v != nil {
l = v . ( int64 )
} else {
return ""
}
2020-08-26 21:10:36 +00:00
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 ""
}
2020-09-14 15:14:30 +00:00
if INTERPOLATE && position == µsToString ( p . position ) {
np := p . position + int64 ( POLL * 1000000 )
position = µsToString ( np )
}
p . position = pos . Value ( ) . ( int64 )
2020-08-26 21:10:36 +00:00
return position + "/" + length
}
2020-08-21 13:54:32 +00:00
func ( p * Player ) JSON ( ) string {
2020-08-22 14:40:24 +00:00
data := map [ string ] string { }
symbol := PLAY
data [ "class" ] = "paused"
if p . playing {
symbol = PAUSE
data [ "class" ] = "playing"
}
2020-08-26 21:10:36 +00:00
var pos string
if SHOW_POS {
pos = p . Position ( )
if pos != "" {
pos = "(" + pos + ")"
}
}
2020-08-22 13:39:23 +00:00
var items [ ] string
2020-08-22 14:40:24 +00:00
order := strings . Split ( ORDER , ":" )
for _ , v := range order {
if v == "SYMBOL" {
items = append ( items , symbol )
} else if v == "ARTIST" {
if p . artist != "" {
items = append ( items , p . artist )
}
} else if v == "ALBUM" {
if p . album != "" {
items = append ( items , p . album )
}
} else if v == "TITLE" {
2020-08-22 18:01:51 +00:00
if p . title != "" {
2020-08-22 14:40:24 +00:00
items = append ( items , p . title )
}
2020-08-26 21:10:36 +00:00
} else if v == "POSITION" && SHOW_POS {
if pos != "" {
items = append ( items , pos )
}
2020-08-22 13:39:23 +00:00
}
}
if len ( items ) == 0 {
return "{}"
}
2020-08-22 14:40:24 +00:00
text := ""
for i , v := range items {
right := ""
2020-08-26 21:10:36 +00:00
if ( v == symbol || v == pos ) && i != len ( items ) - 1 {
2020-08-22 14:40:24 +00:00
right = " "
2020-08-26 21:10:36 +00:00
} else if i != len ( items ) - 1 && items [ i + 1 ] != symbol && items [ i + 1 ] != pos {
2020-08-22 14:40:24 +00:00
right = SEP
} else {
right = " "
}
text += v + right
}
2020-08-21 13:54:32 +00:00
data [ "tooltip" ] = fmt . Sprintf (
2020-08-22 13:39:23 +00:00
"%s\nby %s\n" ,
2020-09-14 15:14:30 +00:00
strings . ReplaceAll ( p . title , "&" , "&" ) ,
strings . ReplaceAll ( p . artist , "&" , "&" ) )
2020-08-22 13:39:23 +00:00
if p . album != "" {
2020-09-14 15:14:30 +00:00
data [ "tooltip" ] += "from " + strings . ReplaceAll ( p . album , "&" , "&" ) + "\n"
2020-08-22 13:39:23 +00:00
}
data [ "tooltip" ] += "(" + p . name + ")"
2020-08-22 14:40:24 +00:00
data [ "text" ] = text
out , err := json . Marshal ( data )
2020-08-22 13:39:23 +00:00
if err != nil {
return "{}"
}
2020-08-22 14:40:24 +00:00
return string ( out )
2020-08-21 13:54:32 +00:00
}
2020-08-22 13:39:23 +00:00
type PlayerList struct {
2020-08-22 18:01:51 +00:00
list List
current uint
conn * dbus . Conn
2020-08-22 13:39:23 +00:00
}
type List [ ] * Player
2020-08-21 13:54:32 +00:00
2020-08-22 13:39:23 +00:00
func ( ls List ) Len ( ) int {
return len ( ls )
2020-08-21 13:54:32 +00:00
}
2020-08-22 13:39:23 +00:00
func ( ls List ) 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
}
2020-08-21 13:54:32 +00:00
}
2020-08-22 13:39:23 +00:00
// Reverse order
2020-08-21 13:54:32 +00:00
return states [ 0 ] > states [ 1 ]
}
2020-08-22 13:39:23 +00:00
func ( ls List ) Swap ( i , j int ) {
ls [ i ] , ls [ j ] = ls [ j ] , ls [ i ]
}
func ( pl * PlayerList ) Remove ( fullName string ) {
2020-08-22 18:01:51 +00:00
currentName := pl . list [ pl . current ] . fullName
2020-08-22 13:39:23 +00:00
var i int
found := false
for ind , p := range pl . list {
if p . fullName == fullName {
i = ind
found = true
break
}
}
if found {
pl . list [ 0 ] , pl . list [ i ] = pl . list [ i ] , pl . list [ 0 ]
pl . list = pl . list [ 1 : ]
2020-08-22 18:01:51 +00:00
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 . Println ( pl . JSON ( ) )
}
2020-08-22 13:39:23 +00:00
}
// ls[len(ls)-1], ls[i] = ls[i], ls[len(ls)-1]
// ls = ls[:len(ls)-1]
}
func ( pl * PlayerList ) 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 {
2020-08-25 13:40:56 +00:00
// Don't add playerctld, it just duplicates other players
if strings . HasPrefix ( name , INTERFACE ) && ! strings . Contains ( name , "playerctld" ) {
2020-08-22 13:39:23 +00:00
pl . New ( name )
}
}
return nil
}
func ( pl * PlayerList ) New ( name string ) {
pl . list = append ( pl . list , NewPlayer ( pl . conn , name ) )
2020-08-22 18:01:51 +00:00
if AUTOFOCUS {
pl . current = uint ( len ( pl . list ) - 1 )
}
2020-08-22 13:39:23 +00:00
}
func ( pl * PlayerList ) Sort ( ) {
sort . Sort ( pl . list )
2020-08-22 18:01:51 +00:00
pl . current = 0
2020-08-22 13:39:23 +00:00
}
func ( pl * PlayerList ) Refresh ( ) {
for i := range pl . list {
pl . list [ i ] . Refresh ( )
}
}
func ( pl * PlayerList ) JSON ( ) string {
if len ( pl . list ) != 0 {
2020-08-22 18:01:51 +00:00
return pl . list [ pl . current ] . JSON ( )
2020-08-22 13:39:23 +00:00
}
return "{}"
2020-08-21 13:54:32 +00:00
}
2020-08-22 18:01:51 +00:00
func ( pl * PlayerList ) Next ( ) {
pl . list [ pl . current ] . player . Call ( INTERFACE + ".Player.Next" , 0 )
}
func ( pl * PlayerList ) Prev ( ) {
pl . list [ pl . current ] . player . Call ( INTERFACE + ".Player.Previous" , 0 )
}
func ( pl * PlayerList ) Toggle ( ) {
pl . list [ pl . current ] . player . Call ( INTERFACE + ".Player.PlayPause" , 0 )
}
2020-08-21 13:54:32 +00:00
func main ( ) {
2020-08-31 19:37:23 +00:00
logfile , err := os . OpenFile ( LOGFILE , os . O_CREATE | os . O_APPEND | os . O_RDWR , 0666 )
if err != nil {
log . Fatalf ( "Couldn't open %s for writing: %s" , LOGFILE , err )
}
mw := io . MultiWriter ( logfile , os . Stdout )
log . SetOutput ( mw )
os . Stderr = logfile
2020-08-22 14:40:24 +00:00
flag . StringVar ( & PLAY , "play" , PLAY , "Play symbol/text to use." )
flag . StringVar ( & PAUSE , "pause" , PAUSE , "Pause symbol/text to use." )
flag . StringVar ( & SEP , "separator" , SEP , "Separator string to use between artist, album, and title." )
flag . StringVar ( & ORDER , "order" , ORDER , "Element order." )
2020-08-22 18:01:51 +00:00
flag . BoolVar ( & AUTOFOCUS , "autofocus" , AUTOFOCUS , "Auto switch to currently playing music players." )
2020-08-26 21:10:36 +00:00
flag . BoolVar ( & SHOW_POS , "position" , SHOW_POS , "Show current position between brackets, e.g (04:50/05:00)" )
2020-09-14 15:14:30 +00:00
flag . BoolVar ( & INTERPOLATE , "interpolate" , INTERPOLATE , "Interpolate track position (helpful for players that don't update regularly, e.g mpDris2)" )
2020-08-22 18:01:51 +00:00
var command string
flag . StringVar ( & command , "send" , "" , "send command to already runnning waybar-mpris instance. (options: " + strings . Join ( COMMANDS , "/" ) + ")" )
2020-08-22 14:40:24 +00:00
flag . Parse ( )
2020-08-22 18:01:51 +00:00
if command != "" {
conn , err := net . Dial ( "unix" , SOCK )
if err != nil {
log . Fatalln ( "Couldn't dial:" , err )
}
_ , err = conn . Write ( [ ] byte ( command ) )
if err != nil {
log . Fatalln ( "Couldn't send command" )
}
fmt . Println ( "Sent." )
2020-08-27 19:26:01 +00:00
if command == "list" {
buf := make ( [ ] byte , 512 )
nr , err := conn . Read ( buf )
if err != nil {
log . Fatalf ( "Couldn't read response." )
}
response := string ( buf [ 0 : nr ] )
fmt . Println ( "Response:" )
fmt . Printf ( response )
}
2020-08-31 19:14:14 +00:00
os . Exit ( 0 )
}
conn , err := dbus . SessionBus ( )
if err != nil {
log . Fatalln ( "Error connecting to DBus:" , err )
}
players := & PlayerList {
conn : conn ,
}
players . Reload ( )
players . Sort ( )
players . Refresh ( )
fmt . Println ( players . JSON ( ) )
lastLine := ""
// fmt.Println("New array", players)
// Start command listener
if _ , err := os . Stat ( SOCK ) ; err == nil {
fmt . Printf ( "Socket %s already exists, this could mean waybar-mpris is already running.\nStarting this instance will overwrite the file, possibly stopping other instances from accepting commands.\n" , SOCK )
var input string
ignoreChoice := false
fmt . Printf ( "Continue? [y/n]: " )
go func ( ) {
fmt . Scanln ( & input )
if strings . Contains ( input , "y" ) && ! ignoreChoice {
os . Remove ( SOCK )
}
} ( )
time . Sleep ( 5 * time . Second )
if input == "" {
fmt . Printf ( "\nRemoving due to lack of input.\n" )
ignoreChoice = true
os . Remove ( SOCK )
2020-08-22 18:01:51 +00:00
}
2020-08-31 19:14:14 +00:00
}
go func ( ) {
listener , err := net . Listen ( "unix" , SOCK )
2020-09-06 12:13:36 +00:00
c := make ( chan os . Signal , 1 )
signal . Notify ( c , os . Interrupt )
go func ( ) {
<- c
os . Remove ( SOCK )
os . Exit ( 1 )
} ( )
2020-08-31 19:14:14 +00:00
if err != nil {
log . Fatalln ( "Couldn't establish socket connection at" , SOCK )
2020-08-22 18:01:51 +00:00
}
2020-08-31 19:14:14 +00:00
defer func ( ) {
listener . Close ( )
2020-08-22 18:01:51 +00:00
os . Remove ( SOCK )
2020-08-31 19:14:14 +00:00
} ( )
for {
con , err := listener . Accept ( )
2020-08-22 18:01:51 +00:00
if err != nil {
2020-08-31 19:14:14 +00:00
log . Println ( "Couldn't accept:" , err )
continue
2020-08-22 18:01:51 +00:00
}
2020-08-31 19:14:14 +00:00
buf := make ( [ ] byte , 512 )
nr , err := con . Read ( buf )
if err != nil {
log . Println ( "Couldn't read:" , err )
continue
}
command := string ( buf [ 0 : nr ] )
if command == "player-next" {
length := len ( players . list )
if length != 1 {
if players . current < uint ( length - 1 ) {
players . current += 1
} else {
players . current = 0
2020-08-22 13:39:23 +00:00
}
2020-08-31 19:14:14 +00:00
players . Refresh ( )
fmt . Println ( players . JSON ( ) )
}
} else if command == "player-prev" {
length := len ( players . list )
if length != 1 {
if players . current != 0 {
players . current -= 1
} else {
players . current = uint ( length - 1 )
2020-08-27 19:26:01 +00:00
}
2020-08-31 19:14:14 +00:00
players . Refresh ( )
fmt . Println ( players . JSON ( ) )
}
} else if command == "next" {
players . Next ( )
} else if command == "prev" {
players . Prev ( )
} else if command == "toggle" {
players . Toggle ( )
} else if command == "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 = "*"
2020-08-27 19:26:01 +00:00
}
2020-08-31 19:14:14 +00:00
resp += fmt . Sprintf ( "%0" + strconv . Itoa ( pad ) + "d%s: Name: %s; Playing: %t; PID: %d\n" , i , symbol , p . fullName , p . playing , p . pid )
2020-08-21 14:44:59 +00:00
}
2020-08-31 19:14:14 +00:00
con . Write ( [ ] byte ( resp ) )
} else {
fmt . Println ( "Invalid command" )
2020-08-21 14:44:59 +00:00
}
2020-08-31 19:14:14 +00:00
}
} ( )
2020-08-26 21:10:36 +00:00
2020-08-31 19:14:14 +00:00
conn . BusObject ( ) . Call ( "org.freedesktop.DBus.AddMatch" , 0 , MATCH_NOC )
conn . BusObject ( ) . Call ( "org.freedesktop.DBus.AddMatch" , 0 , MATCH_PC )
if SHOW_POS {
go func ( ) {
for {
2020-09-14 15:14:30 +00:00
time . Sleep ( POLL * time . Second )
2020-09-06 12:13:36 +00:00
if len ( players . list ) != 0 {
if players . list [ players . current ] . playing {
go fmt . Println ( players . JSON ( ) )
}
2020-08-26 21:10:36 +00:00
}
2020-08-31 19:14:14 +00:00
}
} ( )
}
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 )
2020-08-22 18:01:51 +00:00
}
}
2020-08-31 19:14:14 +00:00
}
} else if strings . Contains ( v . Name , "PropertiesChanged" ) && strings . Contains ( v . Body [ 0 ] . ( string ) , INTERFACE + ".Player" ) {
players . Refresh ( )
if AUTOFOCUS {
players . Sort ( )
}
if l := players . JSON ( ) ; l != lastLine {
lastLine = l
fmt . Println ( l )
2020-08-22 14:40:24 +00:00
}
2020-08-21 14:44:59 +00:00
}
2020-08-21 13:54:32 +00:00
}
}