Compare commits

..

21 Commits

Author SHA1 Message Date
16465c44a2 add license.
All checks were successful
continuous-integration/drone/push Build is passing
2021-02-13 22:28:30 +00:00
7a55b6d605 add message on issues
All checks were successful
continuous-integration/drone/push Build is passing
2021-02-06 00:53:04 +00:00
40f813ddc3 Use fixes for spotify
All checks were successful
continuous-integration/drone/push Build is passing
fixes error caused by spotify returning a uint64 rather than int64 track
length. Also note spotify doesn't return a position so it is not
displayed.
2021-01-17 12:25:27 +00:00
7a3b3d5216 update readme, mention AUR package
All checks were successful
continuous-integration/drone/push Build is passing
2021-01-16 17:03:48 +00:00
750317a0d7 url
All checks were successful
continuous-integration/drone/push Build is passing
2021-01-15 22:21:33 +00:00
648f56f1d0 apt update agh
Some checks failed
continuous-integration/drone/push Build is failing
2021-01-15 22:20:08 +00:00
2c3c989480 oops, forgot to add python
Some checks failed
continuous-integration/drone/push Build is failing
2021-01-15 22:18:53 +00:00
bda0879199 add drone/buildrone
Some checks failed
continuous-integration/drone/push Build is failing
2021-01-15 22:15:45 +00:00
937769e2f7 use external mpris2client lib
now at github.com/hrfee/mpris2client.
2021-01-08 00:54:34 +00:00
233b5dd1ca improve ignoring of playerctld
also now ignores signals from it by storing its unique sender name on
startup and checking each signal.
2021-01-07 17:59:02 +00:00
bcd2a83dd9 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.
2021-01-07 16:10:20 +00:00
af0af02059 add some comments and change identifier names 2020-12-15 20:44:03 +00:00
9b02fc2851 print clone message to stderr
any non-json output at all seems to break waybar now, so this avoids it.
2020-11-26 19:11:38 +00:00
a84ae6bd5b fix help output 2020-11-16 21:59:31 +00:00
78ce37db9f Run multiple instances by attaching to original
Fixes #1. When another instance is detected, a command is sent over the
socket for the original to write to stdout and to a temporary file, which is then
tailed by the new instance. Can be overridden with --replace.
2020-09-28 23:48:51 +01:00
b97a950582 add optional position interpolation, escape ampersands
added --interpolate which will increment the track position if there
hasnt been a change after a second. Kinda buggy, so disabled by default.
Ampersands are escaped in the tooltip.
2020-09-14 16:14:30 +01:00
018e3ce34e fix crash when tracklength not available 2020-09-11 21:24:11 +01:00
8343e713fe remove socket on exit; fix --position crash 2020-09-06 13:13:36 +01:00
6ed1eb428f log to /tmp/waybar-mpris.log 2020-08-31 20:37:23 +01:00
f4090c1f07 ask before removing existing socket
automatically removes the socket after 5 seconds of no input so that its
functions headlessly.
2020-08-31 20:14:14 +01:00
3918d2f30e ignore player-next/prev if only one player avavilable
stops the tooltip from flickering.
2020-08-28 21:57:54 +01:00
22 changed files with 426 additions and 387 deletions

27
.drone.yml Normal file
View File

@@ -0,0 +1,27 @@
name: waybar-mpris
kind: pipeline
type: docker
steps:
- name: build
image: golang:latest
commands:
- apt update -y
- apt install -y python3-pip curl
- go mod download
- curl -sL https://git.io/goreleaser > goreleaser.sh
- chmod +x goreleaser.sh
- ./goreleaser.sh --snapshot --skip-publish --rm-dist
- wget https://builds.hrfee.pw/upload.py
- pip3 install requests
- bash -c 'python3 upload.py https://builds2.hrfee.pw hrfee waybar-mpris ./dist/*.tar.gz'
environment:
BUILDRONE_KEY:
from_secret: BUILDRONE_KEY
trigger:
branch:
- main
event:
exclude:
- pull_request

33
.goreleaser.yml Normal file
View File

@@ -0,0 +1,33 @@
project_name: waybar-mpris
release:
gitea:
owner: hrfee
name: waybar-mpris
name_template: "v{{.Version}}"
before:
hooks:
- go mod download
builds:
- dir: ./
env:
- CGO_ENABLED=0
goos:
- linux
goarch:
- amd64
- arm
- arm64
archives:
- replacements:
linux: Linux
amd64: x86_64
checksum:
name_template: 'checksums.txt'
snapshot:
name_template: "git-{{.ShortCommit}}"
changelog:
sort: asc
filters:
exclude:
- '^docs:'
- '^test:'

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 Harvey Tindall
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -7,7 +7,8 @@ a waybar component/utility for displaying and controlling MPRIS2 compliant media
MPRIS2 is widely supported, so this component should work with:
* Chrome/Chromium
* Other browsers (with kde plasma integration installed)
* Firefox (Limited support with `media.hardwaremediakeys.enabled = true` in about:config)
* Other browsers (using KDE Plasma Integration)
* VLC
* Spotify
* Noson
@@ -15,10 +16,14 @@ MPRIS2 is widely supported, so this component should work with:
* Most other music/media players
## Install
`go get git.hrfee.pw/hrfee/waybar-mpris` will install the program, as well as the go dbus bindings and pflags for command-line arguments.
Available on the AUR as [waybar-mpris-git](https://aur.archlinux.org/packages/waybar-mpris-git/) (Thanks @nichobi!)
or just grab the `waybar-mpris` binary from here and place it in your PATH.
`go get git.hrfee.pw/hrfee/waybar-mpris` will compile from source and install.
You can also download a precompiled binaries from [here](https://builds2.hrfee.pw/view/hrfee/waybar-mpris).
## Issues
Stick them on [mpris2client](https://github.com/hrfee/mpris2client) or the [og](https://github.com/hrfee/waybar-mpris) repository (both on github) if you can't make an account here.
## Usage
When running, the program will pipe out json in waybar's format. Add something like this to your waybar `config.json`:
```
@@ -42,10 +47,12 @@ When running, the program will pipe out json in waybar's format. Add something l
```
Usage of waybar-mpris:
--autofocus Auto switch to currently playing music players.
--interpolate Interpolate track position (helpful for players that don't update regularly, e.g mpDris2)
--order string Element order. (default "SYMBOL:ARTIST:ALBUM:TITLE:POSITION")
--pause string Pause symbol/text to use. (default "\uf8e3")
--play string Play symbol/text to use. (default "▶")
--position Show current position between brackets, e.g (04:50/05:00)
--replace Replace any running instances
--send string send command to already runnning waybar-mpris instance. (options: player-next/player-prev/next/prev/toggle)
--separator string Separator string to use between artist, album, and title. (default " - ")
```
@@ -55,10 +62,12 @@ Usage of waybar-mpris:
* `--separator` specifies a string to separate the artist, album and title text.
* `--autofocus` makes waybar-mpris automatically focus on currently playing music players.
* `--position` enables the display of the track position.
* `--interpolate` increments the track position every second. This is useful for players (e.g mpDris2) that don't regularly update the position.
* `--replace`: By default, new instances will attach to the existing one so that the output is identical. This lets this instance replace any others running. It isn't recommended.
* `--send` sends commands to an already running waybar-mpris instance via a unix socket. Commands:
* `player-next`: Switch to displaying and controlling next available player.
* `player-prev`: Same as `player-next`, but for the previous player.
* `next/prev`: Next/previous track on the selected player.
* `toggle`: Play/pause.
* *Note: you can also bind these commands to keys in your sway/other wm config.*
* You can also bind these commands to Media keys in your WM config.

3
dist/checksums.txt vendored Normal file
View File

@@ -0,0 +1,3 @@
57ca953082ee67820539a128d6c285b1bc1e4e917e1376c86cbbc2436880330a waybar-mpris_git-750317a_Linux_x86_64.tar.gz
6e638c4c4e7b1aa324abbbbedda193600390988001c9d4c8d5ed4700f1f6ca0a waybar-mpris_git-750317a_Linux_armv6.tar.gz
c710c2cd4e4a3ed3c4d5e2b8f2a86077904c26365102496ea9b429e5ad2204a6 waybar-mpris_git-750317a_Linux_arm64.tar.gz

88
dist/config.yaml vendored Normal file
View File

@@ -0,0 +1,88 @@
project_name: waybar-mpris
release:
github:
owner: hrfee
name: waybar-mpris
gitea:
owner: hrfee
name: waybar-mpris
name_template: v{{.Version}}
milestones:
- repo:
owner: hrfee
name: waybar-mpris
name_template: '{{ .Tag }}'
scoop:
name: waybar-mpris
commit_author:
name: goreleaserbot
email: goreleaser@carlosbecker.com
commit_msg_template: Scoop update for {{ .ProjectName }} version {{ .Tag }}
builds:
- id: waybar-mpris
goos:
- linux
goarch:
- amd64
- arm
- arm64
goarm:
- "6"
targets:
- linux_amd64
- linux_arm_6
- linux_arm64
dir: ./
main: .
ldflags:
- -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}} -X main.builtBy=goreleaser
binary: waybar-mpris
env:
- CGO_ENABLED=0
lang: go
gobinary: go
archives:
- id: default
builds:
- waybar-mpris
name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}{{ if .Mips }}_{{ .Mips }}{{ end }}'
replacements:
amd64: x86_64
linux: Linux
format: tar.gz
files:
- licence*
- LICENCE*
- license*
- LICENSE*
- readme*
- README*
- changelog*
- CHANGELOG*
allow_different_binary_count: false
snapshot:
name_template: git-{{.ShortCommit}}
checksum:
name_template: checksums.txt
algorithm: sha256
changelog:
filters:
exclude:
- '^docs:'
- '^test:'
sort: asc
dist: dist
env_files:
github_token: ~/.config/goreleaser/github_token
gitlab_token: ~/.config/goreleaser/gitlab_token
gitea_token: ~/.config/goreleaser/gitea_token
before:
hooks:
- go mod download
source:
name_template: '{{ .ProjectName }}-{{ .Version }}'
format: tar.gz
github_urls:
download: https://github.com
gitlab_urls:
download: https://gitlab.com

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
dist/waybar-mpris_linux_amd64/waybar-mpris vendored Executable file

Binary file not shown.

BIN
dist/waybar-mpris_linux_arm64/waybar-mpris vendored Executable file

Binary file not shown.

BIN
dist/waybar-mpris_linux_arm_6/waybar-mpris vendored Executable file

Binary file not shown.

6
go.mod
View File

@@ -3,6 +3,12 @@ module git.hrfee.pw/hrfee/waybar-mpris
go 1.15
require (
github.com/fsnotify/fsnotify v1.4.9 // indirect
github.com/godbus/dbus/v5 v5.0.3
github.com/hpcloud/tail v1.0.0
github.com/hrfee/mpris2client v0.0.5
github.com/spf13/pflag v1.0.5
golang.org/x/sys v0.0.0-20210113181707-4bcb84eeeb78 // indirect
gopkg.in/fsnotify.v1 v1.4.7 // indirect
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
)

25
go.sum
View File

@@ -1,4 +1,29 @@
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
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/hrfee/mpris2client v0.0.0-20210115213010-244c4cd6a569 h1:aCHzdXajnRUeaaW9FHzunrjGcRqmyFedwO0ODJRAdBk=
github.com/hrfee/mpris2client v0.0.0-20210115213010-244c4cd6a569/go.mod h1:tVpzzAlsljmQevNA4mJwUy1onUUiaQcRAa4gdl37okY=
github.com/hrfee/mpris2client v0.0.2 h1:AqsDoJCKyKjSGwVk3sJ5fjx+7M0tsSueZbVUIDxS1xI=
github.com/hrfee/mpris2client v0.0.2/go.mod h1:tVpzzAlsljmQevNA4mJwUy1onUUiaQcRAa4gdl37okY=
github.com/hrfee/mpris2client v0.0.4 h1:2WnCkTWGBr1eg3qHFjxDo7hIeCeQWh3jJ0hFJSts9Ks=
github.com/hrfee/mpris2client v0.0.4/go.mod h1:tVpzzAlsljmQevNA4mJwUy1onUUiaQcRAa4gdl37okY=
github.com/hrfee/mpris2client v0.0.5 h1:h3gcb8Q68i6SfcEgOxL9cXcGee9ekvxXltrUxGwRLDU=
github.com/hrfee/mpris2client v0.0.5/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-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200928205150-006507a75852 h1:sXxgOAXy8JwHhZnPuItAlUtwIlxrlEqi28mKhUR+zZY=
golang.org/x/sys v0.0.0-20200928205150-006507a75852/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201116194326-cc9327a14d48 h1:AYCWBZhgIw6XobZ5CibNJr0Rc4ZofGGKvWa1vcx2IGk=
golang.org/x/sys v0.0.0-20201116194326-cc9327a14d48/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210113181707-4bcb84eeeb78 h1:nVuTkr9L6Bq62qpUqKo/RnZCFfzDBL0bYo6w9OJUqZY=
golang.org/x/sys v0.0.0-20210113181707-4bcb84eeeb78/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=

Binary file not shown.

Before

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 KiB

After

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 947 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 991 B

593
main.go
View File

@@ -3,184 +3,55 @@ package main
import (
"encoding/json"
"fmt"
"io/ioutil"
"io"
"log"
"net"
"os"
"sort"
"strconv"
"os/signal"
"strings"
"time"
"github.com/godbus/dbus/v5"
"github.com/hpcloud/tail"
mpris2 "github.com/hrfee/mpris2client"
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",
}
type Player struct {
player dbus.BusObject
fullName, name, title, artist, album string
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"
// 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'"
SOCK = "/tmp/waybar-mpris.sock"
SOCK = "/tmp/waybar-mpris.sock"
LOGFILE = "/tmp/waybar-mpris.log"
OUTFILE = "/tmp/waybar-mpris.out"
POLL = 1
)
// Mostly default values for flag options.
var (
PLAY = "▶"
PAUSE = ""
SEP = " - "
ORDER = "SYMBOL:ARTIST:ALBUM:TITLE:POSITION"
AUTOFOCUS = false
COMMANDS = []string{"player-next", "player-prev", "next", "prev", "toggle", "list"}
SHOW_POS = false
// Available commands that can be sent to running instances.
COMMANDS = []string{"player-next", "player-prev", "next", "prev", "toggle", "list"}
SHOW_POS = false
INTERPOLATE = false
REPLACE = false
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
if val == "Browser" {
file, err := ioutil.ReadFile(fmt.Sprintf("/proc/%d/cmdline", pid))
if err == nil {
cmd := string(file)
for k, v := range knownBrowsers {
if strings.Contains(cmd, k) {
playerName = v
break
}
}
}
}
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)
}
func (p *Player) Position() string {
// position is in microseconds so we prob need int64 to be safe
l := p.metadata["mpris:length"].Value().(int64)
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 ""
}
return position + "/" + length
}
func (p *Player) JSON() string {
// JSON returns json for waybar to consume.
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 + ")"
}
@@ -188,22 +59,23 @@ func (p *Player) JSON() string {
var items []string
order := strings.Split(ORDER, ":")
for _, v := range order {
if v == "SYMBOL" {
switch v {
case "SYMBOL":
items = append(items, symbol)
} else if v == "ARTIST" {
if p.artist != "" {
items = append(items, p.artist)
case "ARTIST":
if p.Artist != "" {
items = append(items, p.Artist)
}
} else if v == "ALBUM" {
if p.album != "" {
items = append(items, p.album)
case "ALBUM":
if p.Album != "" {
items = append(items, p.Album)
}
} else if v == "TITLE" {
if p.title != "" {
items = append(items, p.title)
case "TITLE":
if p.Title != "" {
items = append(items, p.Title)
}
} else if v == "POSITION" && SHOW_POS {
if pos != "" {
case "POSITION":
if pos != "" && SHOW_POS {
items = append(items, pos)
}
}
@@ -226,12 +98,12 @@ func (p *Player) JSON() string {
data["tooltip"] = fmt.Sprintf(
"%s\nby %s\n",
p.title,
p.artist)
if p.album != "" {
data["tooltip"] += "from " + 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 {
@@ -240,127 +112,43 @@ func (p *Player) JSON() string {
return string(out)
}
type PlayerList struct {
list List
current uint
conn *dbus.Conn
type players struct {
mpris2 *mpris2.Mpris2
}
type List []*Player
func (ls List) Len() int {
return len(ls)
}
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
}
}
// Reverse order
return states[0] > states[1]
}
func (ls List) Swap(i, j int) {
ls[i], ls[j] = ls[j], ls[i]
}
func (pl *PlayerList) 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 {
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.Println(pl.JSON())
}
}
// 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 {
// 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 *PlayerList) New(name string) {
pl.list = append(pl.list, NewPlayer(pl.conn, name))
if AUTOFOCUS {
pl.current = uint(len(pl.list) - 1)
}
}
func (pl *PlayerList) Sort() {
sort.Sort(pl.list)
pl.current = 0
}
func (pl *PlayerList) Refresh() {
for i := range pl.list {
pl.list[i].Refresh()
}
}
func (pl *PlayerList) 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 *PlayerList) Next() {
pl.list[pl.current].player.Call(INTERFACE+".Player.Next", 0)
}
func (pl *players) Next() { pl.mpris2.List[pl.mpris2.Current].Next() }
func (pl *PlayerList) Prev() {
pl.list[pl.current].player.Call(INTERFACE+".Player.Previous", 0)
}
func (pl *players) Prev() { pl.mpris2.List[pl.mpris2.Current].Previous() }
func (pl *PlayerList) 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)
if err != nil {
log.Fatalf("Couldn't open %s for writing: %s", LOGFILE, err)
}
mw := io.MultiWriter(logfile, os.Stdout)
log.SetOutput(mw)
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.")
flag.BoolVar(&AUTOFOCUS, "autofocus", AUTOFOCUS, "Auto switch to currently playing music players.")
flag.BoolVar(&SHOW_POS, "position", SHOW_POS, "Show current position between brackets, e.g (04:50/05:00)")
flag.BoolVar(&INTERPOLATE, "interpolate", INTERPOLATE, "Interpolate track position (helpful for players that don't update regularly, e.g mpDris2)")
flag.BoolVar(&REPLACE, "replace", REPLACE, "replace existing waybar-mpris if found. When false, new instance will clone the original instances output.")
var command string
flag.StringVar(&command, "send", "", "send command to already runnning waybar-mpris instance. (options: "+strings.Join(COMMANDS, "/")+")")
flag.Parse()
os.Stderr = logfile
if command != "" {
conn, err := net.Dial("unix", SOCK)
@@ -376,133 +164,172 @@ func main() {
buf := make([]byte, 512)
nr, err := conn.Read(buf)
if err != nil {
log.Fatalf("Couldn't read response.")
log.Fatalln("Couldn't read response.")
}
response := string(buf[0:nr])
fmt.Println("Response:")
fmt.Printf(response)
}
} else {
conn, err := dbus.SessionBus()
if err != nil {
panic(err)
}
players := &PlayerList{
conn: conn,
}
players.Reload()
players.Sort()
players.Refresh()
fmt.Println(players.JSON())
lastLine := ""
// fmt.Println("New array", players)
// Start command listener
go func() {
os.Remove(SOCK)
listener, err := net.Listen("unix", SOCK)
if err != nil {
log.Fatalln("Couldn't establish socket connection at", SOCK)
os.Exit(0)
}
// fmt.Println("New array", players)
// Start command listener
if _, err := os.Stat(SOCK); err == nil {
if REPLACE {
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)
}
defer listener.Close()
} else if conn, err := net.Dial("unix", SOCK); err == nil {
// When waybar-mpris is already running, we attach to its output instead of launching a whole new instance.
// Print to stderr to avoid errors from waybar
os.Stderr.WriteString("waybar-mpris is already running. This instance will clone its output.")
if err != nil {
log.Fatalln("Couldn't dial:", err)
}
_, err = conn.Write([]byte("share"))
if err != nil {
log.Fatalln("Couldn't send command")
}
buf := make([]byte, 512)
nr, err := conn.Read(buf)
if err != nil {
log.Fatalln("Couldn't read response.")
}
if string(buf[0:nr]) == "success" {
t, err := tail.TailFile(OUTFILE, tail.Config{
Follow: true,
MustExist: true,
Logger: tail.DiscardingLogger,
})
if err == nil {
for line := range t.Lines {
fmt.Println(line.Text)
}
}
}
} else {
os.Remove(SOCK)
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)
signal.Notify(c, os.Interrupt)
go func() {
<-c
os.Remove(OUTFILE)
os.Remove(SOCK)
os.Exit(1)
}()
if err != nil {
log.Fatalf("Couldn't establish socket connection at %s (error %s)\n", SOCK, err)
}
defer func() {
listener.Close()
os.Remove(SOCK)
}()
for {
con, err := listener.Accept()
if err != nil {
log.Println("Couldn't accept:", err)
continue
}
buf := make([]byte, 512)
nr, err := con.Read(buf)
if err != nil {
log.Println("Couldn't read:", err)
continue
}
command := string(buf[0:nr])
switch command {
case "player-next":
length := len(players.mpris2.List)
if length != 1 {
if players.mpris2.Current < uint(length-1) {
players.mpris2.Current++
} else {
players.mpris2.Current = 0
}
players.mpris2.Refresh()
}
case "player-prev":
length := len(players.mpris2.List)
if length != 1 {
if players.mpris2.Current != 0 {
players.mpris2.Current--
} else {
players.mpris2.Current = uint(length - 1)
}
players.mpris2.Refresh()
}
case "next":
players.Next()
case "prev":
players.Prev()
case "toggle":
players.Toggle()
case "list":
con.Write([]byte(players.mpris2.String()))
case "share":
out, err := os.Create(OUTFILE)
if err != nil {
con.Write([]byte(fmt.Sprintf("Failed: %s", err)))
}
WRITER = io.MultiWriter(os.Stdout, out)
con.Write([]byte("success"))
default:
fmt.Println("Invalid command")
}
}
}()
go players.mpris2.Listen()
if SHOW_POS {
go func() {
for {
con, err := listener.Accept()
if err != nil {
log.Println("Couldn't accept:", err)
continue
}
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" {
if players.current < uint(len(players.list)-1) {
players.current += 1
} else {
players.current = 0
time.Sleep(POLL * time.Second)
if len(players.mpris2.List) != 0 {
if players.mpris2.List[players.mpris2.Current].Playing {
go fmt.Fprintln(WRITER, players.JSON())
}
players.Refresh()
fmt.Println(players.JSON())
} else if command == "player-prev" {
if players.current != 0 {
players.current -= 1
} else {
players.current = uint(len(players.list) - 1)
}
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 = "*"
}
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))
} else {
fmt.Println("Invalid command")
}
}
}()
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 {
time.Sleep(1000 * time.Millisecond)
if players.list[players.current].playing {
go fmt.Println(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()
if AUTOFOCUS {
players.Sort()
}
if l := players.JSON(); l != lastLine {
lastLine = l
fmt.Println(l)
}
}
fmt.Fprintln(WRITER, players.JSON())
for v := range players.mpris2.Messages {
if v.Name == "refresh" {
if AUTOFOCUS {
players.mpris2.Sort()
}
if l := players.JSON(); l != lastLine {
lastLine = l
fmt.Fprintln(WRITER, l)
}
}
}
players.mpris2.Refresh()
}

Binary file not shown.