Skip to content

Instantly share code, notes, and snippets.

@ewollesen
Last active August 14, 2021 20:52
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ewollesen/ac51b20c863184c88c56fe9587c663e6 to your computer and use it in GitHub Desktop.
Save ewollesen/ac51b20c863184c88c56fe9587c663e6 to your computer and use it in GitHub Desktop.
A small go binary for putting MPD status in your i3 statusbar
// Copyright (C) 2018 Eric Wollesen <ericw at xmtp dot net>
// https://gist.github.com/ewollesen/ac51b20c863184c88c56fe9587c663e6
// $ git clone https://gist.github.com/ewollesen/ac51b20c863184c88c56fe9587c663e6 $GOPATH/src/$(hostname -s)/i3status-mpd
// $ go install $(hostname -s)/i3status-mpd
package main
import (
"bytes"
"flag"
"fmt"
"html"
"io/ioutil"
"os"
"os/exec"
"regexp"
"strings"
"github.com/fhs/gompd/mpd"
)
const (
// As far as I can tell, there are (at least) two different sets of glyphs I
// can use. The first, I think are "standard unicode". They're typically
// rendered in the Noto Sans or Symbola fonts. They seem to have some
// vertical alignment issues. These glyphs are listed below.
// ⏩ ⏪ ⏭ ⏮ ⏯ ⏴ ⏵ ⏸ ⏹ ⏺ ⏻
//
// The second set of glyphs I can use is from FontAwesome:
//           
//
// I guess if there were a quick and easy way to check for fontawesome, I
// could switch these at runtime.
//
// To see if FontAwesome is installed:
// $ if fc-list "FontAwesome" | grep -q 'FontAwesome'; then echo yes; else echo no; fi
//
// The FontAwesome glyphs are a touch big, so I like to use a slightly
// smaller size. The rise value lifts them up a little so they're better
// centered vertically.
iconPlay = "<span font='FontAwesome 7' rise='1700'></span> "
iconPause = "<span font='FontAwesome 7' rise='1700'></span> "
iconStop = "<span font='FontAwesome 7' rise='1700'></span> "
iconSpotify = " <span font='FontAwesome 11' rise='500'></span>"
)
var (
noIcons = flag.Bool("no-icons", false, "disable the use of icons")
noMarkup = flag.Bool("no-markup", false, "disable the use of pango markup")
)
type statusInfo struct {
State string
Title string
Artist string
Album string
Name string
}
func main() {
mpdStatusInfo, err := getMPDInfo()
if err != nil {
printRedPango("%s", err)
os.Exit(0)
}
spotifyStatusInfo, err := getSpotifyInfo()
if err != nil {
printRedPango("%s", err)
os.Exit(0)
}
playingInfo := mpdStatusInfo
spotifyIcon := ""
if spotifyStatusInfo != nil &&
spotifyStatusInfo.State == "play" && mpdStatusInfo.State != "play" {
spotifyIcon = iconSpotify
playingInfo = spotifyStatusInfo
}
icon := ""
switch playingInfo.State {
case "play":
icon = iconPlay
case "pause":
icon = iconPause
default:
icon = iconStop
}
if *noIcons {
icon = ""
}
// Could fall back to the file as well perhaps, good for streams.
if playingInfo.Title == "" {
fmt.Printf("%sNo current song", iconStop)
os.Exit(0)
}
openAlbum, closeAlbum := "<i>", "</i>"
if *noMarkup {
openAlbum, closeAlbum = "", ""
}
dot := "•"
if *noMarkup {
dot = "-"
}
fallback := fmt.Sprintf("%s%s %s %s",
icon,
escape("\""+playingInfo.Title+"\""),
escape(dot),
escape(playingInfo.Name))
if playingInfo.Artist == "" || playingInfo.Album == "" {
if strings.Contains(playingInfo.Name, "FLAC over HTTP") {
pieces := strings.Split(playingInfo.Title, " - ")
album, artist, title := "", "", playingInfo.Title
if len(pieces) >= 3 {
album, artist, title = pieces[0], pieces[1], pieces[2]
fmt.Printf("%s%s %s %s %s %s%s%s%s",
icon,
escape("\""+title+"\""),
escape(dot),
escape(artist),
escape(dot),
openAlbum,
escape(album),
closeAlbum, spotifyIcon)
} else {
fmt.Printf(fallback)
}
} else {
fmt.Printf(fallback)
}
} else {
fmt.Printf("%s%s %s %s %s %s%s%s%s",
icon,
escape("\""+playingInfo.Title+"\""),
escape(dot),
escape(playingInfo.Artist),
escape(dot),
openAlbum,
escape(playingInfo.Album),
closeAlbum, spotifyIcon)
}
os.Exit(0)
}
var (
spotifyAlbumRe = regexp.MustCompile(`xesam:album\s+(.*)`)
spotifyArtistRe = regexp.MustCompile(`xesam:artist\s+(.*)`)
spotifyTitleRe = regexp.MustCompile(`xesam:title\s+(.*)`)
)
func getSpotifyInfo() (*statusInfo, error) {
var cmd *exec.Cmd
var err error
cmd = exec.Command("playerctl", "--player=spotify", "status")
buf := &bytes.Buffer{}
cmd.Stdout = buf
err = cmd.Run()
if err != nil {
if strings.Contains(err.Error(), "not found") {
return nil, nil
}
if strings.Contains(err.Error(), "exit status 1") {
return nil, nil
}
// if errors.Is(err, exec.Error) { // file not found
// return nil, nil
// }
return nil, fmt.Errorf("Spotify: error retrieving status: %s", err)
}
state := normalizeSpotifyState(buf.String())
cmd = exec.Command("playerctl", "--player=spotify", "metadata")
buf = &bytes.Buffer{}
cmd.Stdout = buf
err = cmd.Run()
if err != nil {
return nil, fmt.Errorf("Spotify: error retrieving metadata: %s", err)
}
metadata, err := ioutil.ReadAll(buf)
if err != nil {
return nil, fmt.Errorf("Spotify: error reading metadata: %s", err)
}
matches := spotifyAlbumRe.FindSubmatch(metadata)
if len(matches) == 0 {
return nil, fmt.Errorf("Spotify: no album found")
}
album := string(matches[1])
matches = spotifyArtistRe.FindSubmatch(metadata)
if len(matches) == 0 {
return nil, fmt.Errorf("Spotify: no artist found")
}
artist := string(matches[1])
matches = spotifyTitleRe.FindSubmatch(metadata)
if len(matches) == 0 {
return nil, fmt.Errorf("Spotify: no title found")
}
title := string(matches[1])
return &statusInfo{
Title: strings.TrimSpace(title),
Artist: strings.TrimSpace(artist),
Album: strings.TrimSpace(album),
State: state,
}, nil
}
func normalizeSpotifyState(state string) string {
switch strings.ToLower(strings.TrimSpace(state)) {
case "playing":
return "play"
case "paused":
return "pause"
case "stopped":
return "stop"
default:
fmt.Fprintf(os.Stderr, "state: %q", state)
return "unknown"
}
}
func getMPDInfo() (*statusInfo, error) {
mpdHost := os.Getenv("MPD_HOST")
if mpdHost == "" {
mpdHost = "localhost"
}
mpdPassword := ""
pieces := strings.Split(mpdHost, "@")
if len(pieces) > 1 {
mpdPassword, mpdHost = pieces[0], pieces[1]
}
mpdPort := os.Getenv("MPD_PORT")
if mpdPort == "" {
mpdPort = "6600"
}
client, err := mpd.DialAuthenticated("tcp", mpdHost+":"+mpdPort, mpdPassword)
if err != nil {
if strings.HasSuffix(err.Error(), "connection refused") {
printYellowPango("MPD not running")
os.Exit(0)
}
return nil, fmt.Errorf("MPD: unable to connect: %s", err)
}
statusAttrs, err := client.Status()
if err != nil {
return nil, fmt.Errorf("MPD: error querying status: %s", err)
}
songAttrs, err := client.CurrentSong()
if err != nil {
return nil, fmt.Errorf("MPD: error querying current song: %s", err)
}
if strings.Contains(songAttrs["Title"], "-\u200b") {
// album - artist - title
pieces := strings.Split(songAttrs["Title"], "-\u200b")
if len(pieces) == 3 {
for idx, piece := range pieces {
pieces[idx] = strings.TrimSpace(piece)
}
songAttrs["Title"] = pieces[2]
songAttrs["Artist"] = pieces[1]
songAttrs["Album"] = pieces[0]
}
}
return &statusInfo{
Title: songAttrs["Title"],
Artist: songAttrs["Artist"],
Album: songAttrs["Album"],
State: statusAttrs["state"],
Name: songAttrs["Name"],
}, nil
}
func escape(text string) string {
return strings.Replace(html.EscapeString(text), `\`, "&#92;", -1)
}
func red(text string) string {
if *noMarkup {
return text
}
return fmt.Sprintf("<span foreground=\\\"red\\\">%s</span>", text)
}
func printRedPango(template string, args ...interface{}) {
text := fmt.Sprintf(template, args...)
fmt.Print(red(html.EscapeString(text)))
}
func yellow(text string) string {
if *noMarkup {
return text
}
return fmt.Sprintf("<span foreground=\\\"yellow\\\">%s</span>", text)
}
func printYellowPango(template string, args ...interface{}) {
text := fmt.Sprintf(template, args...)
fmt.Print(yellow(html.EscapeString(text)))
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment