File Coverage

blib/lib/AnyEvent/Net/MPD.pm
Criterion Covered Total %
statement 33 126 26.1
branch 1 38 2.6
condition 0 15 0.0
subroutine 11 25 44.0
pod 5 7 71.4
total 50 211 23.7


line stmt bran cond sub pod time code
1             package AnyEvent::Net::MPD;
2              
3 1     1   83462 use strict;
  1         6  
  1         22  
4 1     1   5 use warnings;
  1         2  
  1         43  
5              
6             our $VERSION = '0.002';
7              
8 1     1   433 use Moo;
  1         8533  
  1         4  
9 1     1   1552 use MooX::HandlesVia;
  1         7739  
  1         5  
10             extends 'AnyEvent::Emitter';
11              
12 1     1   938 use AnyEvent;
  1         4339  
  1         29  
13 1     1   503 use AnyEvent::Socket;
  1         21840  
  1         89  
14 1     1   644 use AnyEvent::Handle;
  1         6299  
  1         37  
15              
16 1         9 use Types::Standard qw(
17             InstanceOf Int ArrayRef HashRef Str Maybe Bool CodeRef
18 1     1   530 );
  1         62305  
19              
20 1     1   1758 use Log::Any;
  1         6939  
  1         5  
21             my $log = Log::Any->get_logger( category => __PACKAGE__ );
22              
23             has version => (
24             is => 'ro',
25             isa => Str,
26             lazy => 1,
27             init_arg => undef,
28             );
29              
30             has auto_connect => (
31             is => 'ro',
32             isa => Bool,
33             default => 0,
34             );
35              
36             has state => (
37             is => 'rw',
38             isa => Str,
39             init_arg => undef,
40             default => 'created',
41             trigger => sub {
42             $_[0]->emit( state => $_[0]->{state} );
43             },
44             );
45              
46             has read_queue => (
47             is => 'ro',
48             isa => ArrayRef [CodeRef],
49             lazy => 1,
50             init_arg => undef,
51             default => sub { [] },
52             handles_via => 'Array',
53             handles => {
54             push_read => 'push',
55             pop_read => 'pop',
56             shift_read => 'shift',
57             unshift_read => 'unshift',
58             },
59             );
60              
61             has password => (
62             is => 'ro',
63             isa => Maybe[Str],
64             lazy => 1,
65             );
66              
67             has port => (
68             is => 'ro',
69             isa => Int,
70             lazy => 1,
71             default => sub { $ENV{MPD_PORT} // 6600 },
72             );
73              
74             has host => (
75             is => 'ro',
76             isa => Str,
77             lazy => 1,
78             default => sub { $ENV{MPD_HOST} // 'localhost' },
79             );
80              
81             has _uri => (
82             is => 'ro',
83             init_arg => undef,
84             lazy => 1,
85             default => sub {
86             my $self = shift;
87             ( $self->password ? $self->password . '@' : q{} )
88             . $self->host
89             . ( $self->port ? ':' . $self->port : q{} )
90             },
91             );
92              
93             has [qw( handle socket )] => ( is => 'rw', init_arg => undef, );
94              
95             {
96             my @buffer;
97             sub _parse_block {
98 0     0   0 my $self = shift;
99             return sub {
100 0     0   0 my ($handle, $line) = @_;
101              
102 0 0       0 if ($line =~ /\w/) {
103 0         0 $log->tracef('< %s', $line);
104 0 0       0 if ($line =~ /^OK/) {
    0          
105 0 0       0 if ($line =~ /OK MPD (.*)/) {
106 0         0 $log->trace('Connection established');
107 0         0 $self->{version} = $1;
108              
109 0 0 0     0 $self->send( password => $self->password )
110             if $self->password and $self->state ne 'ready';
111              
112 0         0 $self->state( 'ready' );
113             }
114             else {
115 0         0 $self->shift_read->( \@buffer );
116 0         0 @buffer = ();
117             }
118             }
119             elsif ($line =~ /^ACK/) {
120 0         0 return $self->emit(error => $line );
121 0         0 @buffer = ();
122             }
123             else {
124 0         0 push @buffer, $line;
125             }
126             }
127              
128 0         0 $handle->push_read( line => $self->_parse_block );
129 0         0 };
130             }
131             }
132              
133             # Set up response parsers for each command
134             my $parsers = { none => sub { @_ } };
135             {
136             my $item = sub {
137             return { map {
138             my ($key, $value) = split /: /, $_, 2;
139             $key => $value;
140             } @{$_[0]} };
141             };
142              
143             my $flat_list = sub { [ map { (split /: /, $_, 2)[1] } @{$_[0]} ] };
144              
145             my $base_list = sub {
146             my @main_keys = @{shift()};
147             my @list_keys = @{shift()};
148             my @lines = @{shift()};
149              
150             my @return;
151             my $item = {};
152              
153             foreach my $line (@lines) {
154             my ($key, $value) = split /: /, $line, 2;
155              
156             if ( grep { /$key/ } @main_keys ) {
157             push @return, $item if defined $item->{$key};
158             $item = { $key => $value };
159             }
160             elsif ( grep { /$key/ } @list_keys ) {
161             unless (defined $item->{$key}) {
162             $item->{$key} = []
163             }
164             push @{$item->{$key}}, $value;
165             }
166             else {
167             $item->{$key} = $value;
168             }
169             }
170             push @return, $item if keys %{$item};
171              
172             return \@return;
173             };
174              
175             my $grouped_list = sub {
176             my @lines = @{shift()};
177              
178             # What we are grouping
179             my ($main) = split /:\s+/, $lines[0], 2;
180              
181             # How we are grouping, from top to bottom
182             my (@categories, %categories);
183             foreach (@lines) {
184             my ($key) = split /:\s+/, $_, 2;
185              
186             if ($key ne $main) {
187             push @categories, $key unless defined $categories{$key};
188             $categories{$key} = 1;
189             }
190             }
191              
192             my $return = {};
193             my $item;
194             foreach my $line (@lines) {
195             my ($key, $value) = split /:\s+/, $line, 2;
196              
197             if (defined $item->{$key}) {
198             # Find the appropriate list of items or create a new one
199             # and populate it
200             my $pointer = $return;
201             foreach my $key (@categories) {
202             my $val = $item->{$key} // q{};
203             $pointer->{$key}{$val} = {} unless defined $pointer->{$key}{$val};
204             $pointer = $pointer->{$key}{$val};
205             }
206             $pointer->{$main} = [] unless defined $pointer->{$main};
207             my $list = $pointer->{$main};
208              
209             push @{$list}, delete $item->{$main};
210              
211             # Start a new item
212             $item = { $key => $value };
213             next;
214             }
215              
216             $item->{$key} = $value;
217             }
218             return $return;
219             };
220              
221             # Untested commands: what do they return?
222             # consume
223             # crossfade
224              
225             my $file_list = sub { $base_list->( [qw( directory file )], [], @_ ) };
226              
227             $parsers->{$_} = $flat_list foreach qw(
228             commands notcommands channels tagtypes urlhandlers listplaylist
229             );
230              
231             $parsers->{$_} = $item foreach qw(
232             currentsong stats idle status addid update
233             readcomments replay_gain_status rescan
234             );
235              
236             $parsers->{$_} = $file_list foreach qw(
237             find playlistinfo listallinfo search find playlistid playlistfind
238             listfiles plchanges listplaylistinfo playlistsearch listfind
239             );
240              
241             $parsers->{list} = $grouped_list;
242              
243             foreach (
244             [ outputs => [qw( outputid )], [] ],
245             [ plchangesposid => [qw( cpos )], [] ],
246             [ listplaylists => [qw( playlist )], [] ],
247             [ listmounts => [qw( mount )], [] ],
248             [ listneighbors => [qw( neighbor )], [] ],
249             [ listall => [qw( directory )], [qw( file )] ],
250             [ readmessages => [qw( channel )], [qw( message )] ],
251             [ lsinfo => [qw( directory file playlist )], [] ],
252             [ decoders => [qw( plugin )], [qw( suffix mime_type )] ],
253             ) {
254              
255             my ($cmd, $header, $list) = @{$_};
256             $parsers->{$cmd} = sub { $base_list->( $header, $list, @_ ) };
257             }
258              
259             $parsers->{playlist} = sub {
260             my $lines = [ map { s/^\w*?://; $_ } @{shift()} ];
261             $flat_list->( $lines, @_ )
262             };
263              
264             $parsers->{count} = sub {
265             my $lines = shift;
266             my ($main) = split /:\s+/, $lines->[0], 2;
267             $base_list->( [ $main ], [qw( )], $lines, @_ )
268             };
269              
270             $parsers->{sticker} = sub {
271             my $lines = shift;
272             return {} unless scalar @{$lines};
273              
274             my $single = ($lines->[0] !~ /^file/);
275              
276             my $base = $base_list->( [qw( file )], [qw( sticker )], $lines, @_ );
277             my $return = [ map {
278             $_->{sticker} = { map { split(/=/, $_, 2) } @{$_->{sticker}} }; $_;
279             } @{$base} ];
280              
281             return $single ? $return->[0] : $return;
282             };
283             }
284              
285             {
286             my $cv;
287              
288             sub idle {
289 0     0 1 0 my ($self, @subsystems) = @_;
290              
291 0         0 $cv = AnyEvent->condvar;
292              
293 0         0 my $idle;
294             $idle = sub {
295 0     0   0 my $o = shift->recv;
296 0         0 $self->emit( $o->{changed} );
297 0 0       0 $self->send( idle => @subsystems, $idle ) unless $cv->ready;
298 0         0 };
299 0         0 $self->send( idle => @subsystems, $idle );
300              
301 0         0 return $cv;
302             }
303              
304             sub noidle {
305 0     0 1 0 my ($self) = @_;
306 0 0       0 $cv->send if $cv;
307 0         0 $self->send( 'noidle' );
308 0         0 return $self;
309             }
310             }
311              
312             sub send {
313 0     0 1 0 my $self = shift;
314 0 0       0 my $opt = ( ref $_[0] eq 'HASH' ) ? shift : {};
315 0 0       0 my $cb = pop if ref $_[-1] eq 'CODE';
316 0         0 my (@commands) = @_;
317              
318             # Normalise input
319 0 0       0 if (ref $commands[0] eq 'ARRAY') {
320             @commands = map {
321 0 0       0 ( ref $_ eq 'ARRAY' ) ? join( q{ }, @{$_} ) : $_;
  0         0  
322 0         0 } @{$commands[0]};
  0         0  
323             }
324             else {
325 0         0 @commands = join q{ }, @commands;
326             }
327              
328 0         0 my $command = '';
329             # Remove underscores from command names
330             @commands = map {
331 0         0 my $args;
  0         0  
332 0         0 ($command, $args) = split /\s/, $_, 2;
333 0 0       0 $command =~ s/_//g unless $command =~ /^replay_gain_/;
334 0   0     0 $args //= q{};
335 0         0 "$command $args";
336             } @commands;
337              
338             # Create block if command list
339 0 0       0 if (scalar @commands > 1) {
340 0         0 unshift @commands, "command_list_begin";
341 0         0 push @commands, "command_list_end";
342             }
343              
344 0   0     0 my $parser = $opt->{parser} // $command;
345             $parser = $parsers->{$parser} // $parsers->{none}
346 0 0 0     0 unless ref $parser eq 'CODE';
347              
348 0 0       0 my $cv = AnyEvent->condvar( $cb ? ( cb => $cb ) : () );
349              
350             $self->push_read( sub {
351 0     0   0 my $response = shift;
352 0         0 $cv->send( $parser->( $response ) );
353 0         0 });
354              
355 0         0 $log->tracef( '> %s', $_ ) foreach @commands;
356 0         0 $self->handle->push_write( join("\n", @commands) . "\n" );
357              
358 0         0 return $cv;
359             }
360              
361 0     0 1 0 sub get { shift->send( @_ )->recv }
362              
363             sub until {
364 0     0 0 0 my ($self, $name, $check, $cb) = @_;
365              
366 0         0 weaken $self;
367 0         0 my $wrapper;
368             $wrapper = sub {
369 0 0   0   0 if ($check->(@_)) {
370 0         0 $self->unsubscribe($name => $wrapper);
371 0         0 $cb->(@_);
372             }
373 0         0 };
374 0         0 $self->on($name => $wrapper);
375              
376 0         0 return $wrapper;
377             }
378              
379             sub BUILD {
380 1     1 0 4668 my ($self, $args) = @_;
381              
382 1         4 $self->socket( $self->_build_socket );
383              
384 1 50       11 $self->connect if $self->auto_connect;
385             }
386              
387             sub _build_socket {
388 1     1   2 my $self = shift;
389              
390             my $socket = tcp_connect $self->host, $self->port, sub {
391 0 0   0   0 my ($fh) = @_
392             or die "MPD connect failed: $!";
393              
394 0         0 $log->debugf('Connecting to %s:%s', $self->host, $self->port);
395             $self->handle(
396             AnyEvent::Handle->new(
397             fh => $fh,
398             on_error => sub {
399 0         0 my ($hdl, $fatal, $msg) = @_;
400 0         0 $self->emit( error => $msg );
401 0         0 $hdl->destroy;
402             },
403             )
404 0         0 );
405              
406             $self->handle->on_read(sub {
407 0         0 $self->handle->push_read( line => $self->_parse_block )
408 0         0 });
409              
410             $self->handle->on_error(sub {
411 0         0 my ($h, $fatal, $message) = @_;
412 0   0     0 $self->emit( error => $message // 'Error' );
413 0         0 $self->handle(undef);
414 0         0 });
415              
416             $self->handle->on_eof(sub {
417 0         0 my ($h, $fatal, $message) = @_;
418 0   0     0 $self->emit( eof => $message // 'EOF' );
419 0         0 $self->handle(undef);
420 0         0 });
421 1         17 };
422              
423 1         4409 return $socket;
424             }
425              
426             sub connect {
427 0     0 1   my ($self) = @_;
428              
429 0 0         return $self if $self->state eq 'ready';
430              
431 0           my $cv = AnyEvent->condvar;
432 0     0     $self->until( state => sub { $_[1] eq 'ready' }, sub {
433 0     0     $cv->send;
434 0           });
435 0           $cv->recv;
436              
437 0           return $self;
438             }
439              
440             1;
441              
442             __END__