aboutsummaryrefslogblamecommitdiff
path: root/bin/mpdlrc
blob: b1871a59f1f038aefc9ca3648a81233006a7044a (plain) (tree)









































                                                                             
                                  
                     
 
                             

                



































                                                                             


                                     





                                                                               



                                                                    

                     
     























































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