#!/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 # 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; }