Last active
August 14, 2021 20:52
-
-
Save ewollesen/ac51b20c863184c88c56fe9587c663e6 to your computer and use it in GitHub Desktop.
A small go binary for putting MPD status in your i3 statusbar
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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), `\`, "\", -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