Home -> Projects -> Live lyrics for music
I used to use pianobar (a command line Pandora client) to stream music. I used its bash hook mechanism to fetch lyrics in real time with glyrc. Recently I decided (for a variety of reasons) I’d rather store/play my music locally, so I wrote a scraper to collect a large list of songs I like and downloaded them all with youtube-dl (see 104.3 song title/artist fetcher). I used glyrc to fetch lyrics again, but I didn’t have a live display of lyrics … until this project.
I play my music with VLC (specifically cvlc -ZIncurses path/to/music). VLC has DBus hooks, so I started poking around to see what I could do with them.
I found two useful tools for DBus introspection. The first is dbus-monitor, a command line program that listens for DBus methods (optionally filtering them in various ways) and prints them out. For its simplest usage, run
dbus-monitor --profile
or
dbus-monitor --monitor
The “profile” mode outputs one line per DBus message with timing information and some metadata about the message. The “monitor” mode is fairly verbose – in general I’d start by using “profile” to find a good filter and then use “monitor” to see the actual message contents.
The other tool is a built-in DBus introspection method:
dbus-send --print-reply --dest=org.mpris.MediaPlayer2.vlc
/org/mpris/MediaPlayer2 org.freedesktop.DBus.Introspectable.Introspect
The destination --dest=org.mpris.MediaPlayer2.vlc and path /org/mpris/MediaPlayer2 came from a combination of watching dbus-monitor and searching for vlc DBus documentation online. This command outputs the DBus interace for VLC. Most notably it documents the “PropertiesChanged” signal I saw on dbus-monitor and the “Get” method for fetching properties on demand.
From my research online it seems like very few people use DBus to interact with VLC. I was able to find two good resources, though. The first is a set of commands to play/pause VLC over DBus. It isn’t what I wanted to do, but since I have little experience with dbus-send it was quite useful to have correctly-formatted dbus-send commands for VLC. (Incidentally it seems like a lot of people find qdbus to be more user-friendly, but I prefer not to depend on QT for this project). The other reference is the MPRIS DBus API/documentation. MPRIS = Media Player Remote Interfacing Specification, and it turns out that most music players with a DBus interface implement at least the most basic level of this specification. The specification for Player says that it contains a Metadata property, which, among other things, contains a URL specifying the currently-playing track. The updated Metadata is returned as part of the PropertiesChanged signal, and I can query it manually with a DBus Get method.
Unfortunately, strings in DBus messages are escaped, making it non-trivial to do path manipulation and load lyrics. The encoding mechanism is similar to the encoding used for URLs, so I found and adapted someone else’s solution for decoding URLs on the command line.
Send a DBus Get method to retrieve Metadata and parse the currently-playing URL:
function get_current_track_url() {
DBUS_DEST="org.mpris.MediaPlayer2.vlc"
DBUS_PATH="/org/mpris/MediaPlayer2"
DBUS_METHOD="org.freedesktop.DBus.Properties.Get"
GET_TARGET="string:org.mpris.MediaPlayer2.Player"
PROPERTY="string:Metadata"
XESAM_URL=$(dbus-send --print-reply=literal --dest=$DBUS_DEST "$DBUS_PATH" "$DBUS_METHOD" "$GET_TARGET" "$PROPERTY" | grep -o 'file://\S*')
echo "$XESAM_URL"
}
Decode the URL (URI?) into a path:
function decode_url() {
LINE="$1"
VALUE_ONLY=$(echo "$LINE" | tr " " "\n" | grep -o "file://.*")
DECODED=$(echo "$VALUE_ONLY" | sed 's/%\([0-9A-F][0-9A-F]\)/\\\\x\1/g' | xargs echo -e | cut -c 8-)
echo "$DECODED"
}
Monitor for song changes:
dbus-monitor --monitor "type=signal,interface=org.freedesktop.DBus.Properties,path=/org/mpris/MediaPlayer2,member=PropertiesChanged" | while read line
do
if echo "$line" | grep -q 'file://'
then
...
fi
done
The overall structure I want is:
lesslessThis is requires some attention to detail because (as I learned experimentally) less is not a very good background task (in particular scrolling works badly). So less needs to run in the foreground and the background task needs to pass messages to the foreground task when new lyrics are available.
Convert the path to a song to the path to the lyrics. In my setup lyrics exist in lyrics.txt in the same directory as the song. See my song fetching project for details on generating those lyrics.
function track_path_to_lyrics_path() {
TRACK_PATH="$1"
PARENT_PATH=$(dirname "${TRACK_PATH}")
LYRICS_PATH="${PARENT_PATH}/lyrics.txt"
echo "${LYRICS_PATH}"
}
A wrapper for less with nice arguments, mostly because I have functions for everything else:
function display_lyrics() {
less -c --quit-on-intr "$1"
}
The background task will write new lyrics paths to a FIFO and “notify” the main thread that new lyrics exist by killing less
LYRICS_FIFO=$(echo "/tmp/lyrics_fifo_$$")
rm -f "$LYRICS_FIFO"
mkfifo "$LYRICS_FIFO"
exec 3<> "$LYRICS_FIFO";
...
dbus-monitor --monitor "type=signal,interface=org.freedesktop.DBus.Properties,path=/org/mpris/MediaPlayer2,member=PropertiesChanged" | while read line
do
if echo "$line" | grep -q 'file://'
then
URL=$(echo "$line" | sed 's/.*string "\(.*\)"/\1/')
TRACK_PATH=$(decode_url "$URL")
LYRICS_PATH=$(track_path_to_lyrics_path "$TRACK_PATH")
if [ "$LYRICS_PATH" != "$OLD_LYRICS_PATH" ]
then
OLD_LYRICS_PATH="$LYRICS_PATH"
pgrep -P $$ less | xargs kill -9 2>/dev/null
echo "$OLD_LYRICS_PATH" > "$LYRICS_FIFO"
fi
fi
done &
Clean up background tasks + FIFO on exit:
function cleanup() {
jobs -p | xargs kill -9
rm -f "$LYRICS_FIFO"
}
trap cleanup EXIT
Before starting the background task:
INIT_TRACK_URL=$(get_current_track_url)
INIT_TRACK_PATH=$(decode_url "$INIT_TRACK_URL")
INIT_LYRICS_PATH=$(track_path_to_lyrics_path "$INIT_TRACK_PATH")
OLD_LYRICS_PATH="$INIT_LYRICS_PATH"
echo "$OLD_LYRICS_PATH" > "$LYRICS_FIFO"
while true
do
if read line
then
echo "$line"
display_lyrics "$line" 2>/dev/null
fi
done <"$LYRICS_FIFO"
Here, also on Github (added to the song title/artist fetcher repo).