File Coverage

blib/lib/App/MPDJ.pm
Criterion Covered Total %
statement 21 152 13.8
branch 0 32 0.0
condition 0 15 0.0
subroutine 7 34 20.5
pod 0 23 0.0
total 28 256 10.9


line stmt bran cond sub pod time code
1             package App::MPDJ;
2              
3 1     1   39963 use strict;
  1         2  
  1         40  
4 1     1   4 use warnings;
  1         2  
  1         30  
5 1     1   32 use 5.010;
  1         8  
  1         55  
6              
7             our $VERSION = '1.09';
8              
9 1     1   1101 use Net::MPD;
  1         79718  
  1         50  
10 1     1   1164 use Proc::Daemon;
  1         14661  
  1         42  
11 1     1   1076 use Log::Dispatch;
  1         18090  
  1         37  
12 1     1   1962 use AppConfig;
  1         5957  
  1         1887  
13              
14             sub new {
15 0     0 0   my ($class, @options) = @_;
16              
17 0           my $self = bless {
18             last_call => 0,
19             config_errors => [],
20             @options
21             }, $class;
22             }
23              
24 0     0 0   sub mpd { shift->{mpd} }
25 0     0 0   sub log { shift->{log} }
26 0     0 0   sub config { shift->{config} }
27              
28             sub parse_options {
29 0     0 0   my ($self, @args) = @_;
30              
31             $self->{config} = AppConfig->new({
32 0     0     ERROR => sub { push @{ $self->{config_errors} }, \@_; },
  0            
33             CASE => 1,
34             },
35             'conf|f=s' => {
36 0     0     VALIDATE => sub { -e shift }
37             },
38 0           'before|b=i' => { DEFAULT => 2 },
39             'after|a=i' => { DEFAULT => 2 },
40             'calls-path=s' => { DEFAULT => 'calls' },
41             'calls-freq=i' => { DEFAULT => 3600 },
42             'daemon|D!' => { DEFAULT => 1 },
43             'mpd=s' => { DEFAULT => 'localhost' },
44             'music-path=s' => { DEFAULT => 'music' },
45             'syslog|s=s' => { DEFAULT => '' },
46             'conlog|l=s' => { DEFAULT => '' },
47             'help|h' => { ACTION => \&help, },
48             'version|V' => { ACTION => \&version, });
49              
50 0           $self->_getopt(@args); # to get --conf option, if any
51              
52 0   0       my @configs =
53             $self->config->get('conf') || ('/etc/mpdj.conf', "$ENV{HOME}/.mpdjrc");
54 0           foreach my $config (@configs) {
55 0 0         if (-e $config) {
56 0 0         say "Loading config ($config)" if $self->config->get('conlog');
57 0           $self->config->file($config);
58             } else {
59 0 0         say "Config file skipped ($config)" if $self->config->get('conlog');
60             }
61             }
62              
63 0           $self->_getopt(@args); # to override config file
64             }
65              
66             sub _getopt {
67 0     0     my ($self, @args) = @_;
68              
69 0           $self->config->getopt([@args]); # do not consume @args
70              
71 0 0         if (@{ $self->{config_errors} }) {
  0            
72 0           foreach my $err (@{ $self->{config_errors} }) {
  0            
73 0           printf STDERR @$err;
74 0           print STDERR "\n";
75             }
76 0           $self->help;
77             }
78             }
79              
80             sub connect {
81 0     0 0   my ($self) = @_;
82              
83 0           $self->{mpd} = Net::MPD->connect($self->config->get('mpd'));
84             }
85              
86             sub execute {
87 0     0 0   my ($self) = @_;
88              
89             local @SIG{qw( INT TERM HUP )} = sub {
90 0     0     $self->log->notice('Exiting');
91 0           exit 0;
92 0           };
93              
94 0           my @loggers;
95 0 0         push @loggers,
96             ([ 'Screen', min_level => $self->config->get('conlog'), newline => 1 ])
97             if $self->config->get('conlog');
98 0 0         push @loggers,
99             ([ 'Syslog', min_level => $self->config->get('syslog'), ident => 'mpdj' ])
100             if $self->config->get('syslog');
101              
102 0           $self->{log} = Log::Dispatch->new(outputs => \@loggers);
103              
104 0 0         if ($self->config->get('daemon')) {
105 0           $self->log->notice('Forking to background');
106 0           Proc::Daemon::Init;
107             }
108              
109 0           $self->connect;
110 0           $self->configure;
111              
112 0           $self->mpd->subscribe('mpdj');
113              
114 0           $self->update_cache;
115              
116 0           while (1) {
117 0           $self->log->debug('Waiting');
118 0           my @changes =
119             $self->mpd->idle(qw(database player playlist message options));
120 0           $self->mpd->update_status();
121              
122 0           foreach my $subsystem (@changes) {
123 0           my $function = $subsystem . '_changed';
124 0           $self->$function();
125             }
126             }
127             }
128              
129             sub configure {
130 0     0 0   my ($self) = @_;
131              
132 0           $self->log->notice('Configuring MPD server');
133              
134 0           $self->mpd->repeat(0);
135 0           $self->mpd->random(0);
136              
137 0 0         if ($self->config->get('calls-freq')) {
138 0           my $now = time;
139 0           $self->{last_call} = $now - $now % $self->config->get('calls-freq');
140 0           $self->log->notice("Set last call to $self->{last_call}");
141             }
142             }
143              
144             sub update_cache {
145 0     0 0   my ($self) = @_;
146              
147 0           $self->log->notice('Updating music and calls cache...');
148              
149 0           foreach my $category (('music', 'calls')) {
150              
151 0           @{ $self->{$category} } = grep { $_->{type} eq 'file' }
  0            
  0            
152             $self->mpd->list_all($self->config->get("${category}-path"));
153              
154 0           my $total = scalar(@{ $self->{$category} });
  0            
155 0 0         if ($total) {
156 0           $self->log->notice(sprintf 'Total %s available: %d', $category, $total);
157             } else {
158 0           $self->log->warning(
159             "No $category available. Path should be mpd path not file system.");
160             }
161             }
162             }
163              
164             sub remove_old_songs {
165 0     0 0   my ($self) = @_;
166              
167 0   0       my $song = $self->mpd->song || 0;
168 0           my $count = $song - $self->config->get('before');
169 0 0         if ($count > 0) {
170 0           $self->log->info("Deleting $count old songs");
171 0           $self->mpd->delete("0:$count");
172             }
173             }
174              
175             sub add_new_songs {
176 0     0 0   my ($self) = @_;
177              
178 0   0       my $song = $self->mpd->song || 0;
179 0           my $count =
180             $self->config->get('after') + $song - $self->mpd->playlist_length + 1;
181 0 0         if ($count > 0) {
182 0           $self->log->info("Adding $count new songs");
183 0           $self->add_song for 1 .. $count;
184             }
185             }
186              
187             sub add_song {
188 0     0 0   my ($self) = @_;
189              
190 0           $self->add_random_item_from_category('music');
191             }
192              
193             sub add_call {
194 0     0 0   my ($self) = @_;
195              
196 0           $self->log->info('Injecting call');
197              
198 0           $self->add_random_item_from_category('calls', 'immediate');
199              
200 0           my $now = time;
201 0           $self->{last_call} = $now - $now % $self->config->get('calls-freq');
202 0           $self->log->info('Set last call to ' . $self->{last_call});
203             }
204              
205             sub add_random_item_from_category {
206 0     0 0   my ($self, $category, $next) = @_;
207              
208 0           my @items = @{ $self->{$category} };
  0            
209              
210 0           my $index = int rand scalar @items;
211 0           my $item = $items[$index];
212              
213 0           my $uri = $item->{uri};
214 0   0       my $song = $self->mpd->song || 0;
215 0 0         my $pos = $next ? $song + 1 : $self->mpd->playlist_length;
216 0           $self->log->info('Adding ' . $uri . ' at position ' . $pos);
217              
218 0           $self->mpd->add_id($uri, $pos);
219             }
220              
221             sub time_for_call {
222 0     0 0   my ($self) = @_;
223              
224 0 0         return unless $self->config->get('calls-freq');
225 0           return time - $self->{last_call} > $self->config->get('calls-freq');
226             }
227              
228             sub version {
229 0     0 0   say "mpdj (App::MPDJ) version $VERSION";
230 0           exit;
231             }
232              
233             sub help {
234 0     0 0   print <<'HELP';
235             Usage: mpdj [options]
236              
237             Options:
238             --mpd MPD connection string (password@host:port)
239             -s,--syslog Turns on syslog output (debug, info, notice, warn[ing], error, etc)
240             -l,--conlog Turns on console output (same choices as --syslog)
241             --no-daemon Turn off daemonizing
242             -b,--before Number of songs to keep in playlist before current song
243             -a,--after Number of songs to keep in playlist after current song
244             -c,--calls-freq Frequency to inject call signs in seconds
245             --calls-path Path to call sign files
246             --music-path Path to music files
247             -f,--conf Config file to use
248             -V,--version Show version information and exit
249             -h,--help Show this help and exit
250             HELP
251              
252 0           exit;
253             }
254              
255             sub database_changed {
256 0     0 0   my ($self) = @_;
257              
258 0           $self->update_cache;
259             }
260              
261             sub player_changed {
262 0     0 0   my ($self) = @_;
263              
264 0 0         $self->add_call() if $self->time_for_call();
265 0           $self->add_new_songs();
266 0           $self->remove_old_songs();
267             }
268              
269             sub playlist_changed {
270 0     0 0   my ($self) = @_;
271              
272 0           $self->player_changed();
273             }
274              
275             sub message_changed {
276 0     0 0   my $self = shift;
277              
278 0           my @messages = $self->mpd->read_messages();
279              
280 0           foreach my $message (@messages) {
281 0           my $function = 'handle_message_' . $message->{channel};
282 0           $self->$function($message->{message});
283             }
284             }
285              
286             sub options_changed {
287 0     0 0   my $self = shift;
288              
289 0           $self->log->notice('Resetting configuration');
290              
291 0           $self->mpd->repeat(0);
292 0           $self->mpd->random(0);
293             }
294              
295             sub handle_message_mpdj {
296 0     0 0   my ($self, $message) = @_;
297              
298 0           my ($option, $value) = split /\s+/, $message, 2;
299              
300 0 0 0       if ($option eq 'before' or $option eq 'after' or $option eq 'calls-freq') {
      0        
301 0 0         return unless $value =~ /^\d+$/;
302 0           $self->log->info(sprintf 'Setting %s to %s (was %s)',
303             $option, $value, $self->config->get($option));
304 0           $self->config->set($option, $value);
305 0           $self->player_changed();
306             }
307             }
308              
309             1;
310              
311             __END__