aboutsummaryrefslogtreecommitdiff
path: root/bin/mpdlrc
blob: 3fc672d7da241a238262bc8a26c2ab7132feaa41 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
#!/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;
        waitpid $pid, 0;
        $pid = undef;
    }
}

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