#!/usr/bin/env perl
#
# mpdlrc -- Print timed lyrics from an LRC file for MPD's currently playing
# song line-by-line to stdout. See README.markdown.
#
# Author: Tom Ryder <tom@sanctum.geek.nz>
# Copyright: 2015
#
package Sanctum::Mpdlrc;
# Force me to write this properly
use strict;
use warnings;
use utf8;
use autodie qw(:all);
# Require a few modules
use Carp;
use Const::Fast;
use Net::MPD;
use Time::HiRes qw(sleep);
# Require at least Perl 5.12
use 5.012;
# Specify version number
our $VERSION = 0.1;
# Specify some constants to appease Perl::Critic
const my $SECONDS_PER_MINUTE => 60;
const my $HUNDREDTHS_PER_SECOND => 100;
# Connect to MPD, or give up and cry
my $mpd = Net::MPD->connect()
or croak('Failed to connect to MPD');
# We declare the PID outside of the main loop so we can kill it on subsequent
# iterations of the loop, should we need to restart the process.
my $pid;
# Use UTF-8 for output, because forëigñ charåcters āre ìmpørtánt
binmode STDOUT, ':encoding(utf8)';
STDOUT->autoflush(1);
# Loop waiting for MPD events
MPD: while (1) {
# Get the current status
my $status = $mpd->update_status();
# If there's a song playing, we'll try and spit some lyrics
if ( $status->{state} eq 'play' ) {
# Get details about the current song
my $song = $mpd->current_song();
# Fork a new process
$pid = fork;
# This block should only be run by the fork
if ( !$pid ) {
# Build the expected filename for the lyric file from the song's
# author and title
my $lfn = sprintf '%s/.lyrics/%s - %s.lrc', $ENV{HOME},
@{$song}{qw(Artist Title)};
# If no such file exists, we have failed
if ( !-e $lfn ) {
exit 1;
}
# Read a lyrics queue object from the file, providing it with the
# elapsed time (i.e. telling it how far into the song we already
# are)
my $lyrics = read_lyrics_queue( $lfn, $status->{elapsed} );
# Step through the lyrics queue object, sleeping the required
# amount of time before printing each line of text to stdout
foreach my $lyric ( @{$lyrics} ) {
sleep $lyric->{delay};
printf {*STDOUT} "%s\n", $lyric->{text};
}
# We, the fork, are done!
exit;
}
}
# Wait for something else to happen to the player, whether or not there's a
# forked process going
$mpd->idle('player');
# Something important happened; kill any running lyric processes
if ($pid) {
kill 'INT', $pid;
$pid = undef;
wait;
}
}
# Subroutine to read lyrics from the given filename and return a queue object
# specifying a list of lyrics to display and how long to wait before displaying
# each line
sub read_lyrics_queue {
my ( $lfn, $elapsed ) = @_;
$elapsed //= 0;
# Read the file into a list of lines
open my $lfh, q{<:encoding(utf8)}, $lfn;
my @lines = readline $lfh;
close $lfh;
# Start a list of lyric hashrefs
my @lyrics;
# Read each line
LINE: foreach my $line (@lines) {
# Get rid of trailing newlines
chomp $line;
# If the line is in LRC format, we'll queue it up
if ( $line =~ m{\[(\d+):(\d+)[.](\d+)\](.+)}msx ) {
# Read minutes, seconds, hundredth-seconds, and text from the
# matches in the line
my ( $min, $sec, $hsec, $text ) = ( $1, $2, $3, $4 );
# Flatten out the times into a number of seconds, fractional
# (that's why we need sleep() from Time::HiRes)
my $tsec =
( $min * $SECONDS_PER_MINUTE ) +
$sec +
( $hsec / $HUNDREDTHS_PER_SECOND );
# If the lyric is yet to be displayed, i.e. we haven't already
# passed the appropriate point in the song, queue it up and
# increment the elapsed time for queuing up the next lyric, if any
if ( $tsec > $elapsed ) {
my $lyric = {
delay => $tsec - $elapsed,
text => $text,
};
push @lyrics, $lyric;
$elapsed += $lyric->{delay};
}
}
}
# Return a reference to the built lyric object
return \@lyrics;
}