File Coverage

blib/lib/Net/DMAP/Server.pm
Criterion Covered Total %
statement 10 12 83.3
branch n/a
condition n/a
subroutine 4 4 100.0
pod n/a
total 14 16 87.5


line stmt bran cond sub pod time code
1             package Net::DMAP::Server;
2 1     1   823 use strict;
  1         2  
  1         45  
3 1     1   7 use warnings;
  1         2  
  1         37  
4 1     1   918 use POE;
  1         49917  
  1         7  
5 1     1   119070 use POE::Component::Server::HTTP 0.05; # for keep alive
  0            
  0            
6             use POE::Component::Server::HTTP;
7             use Net::Rendezvous::Publish;
8             use Net::DAAP::DMAP qw( dmap_pack );
9             use Sys::Hostname;
10             use base 'Class::Accessor::Fast';
11             __PACKAGE__->mk_accessors(qw( debug port name path db_uuid tracks playlists
12             revision waiting_clients poll_interval ),
13             qw( httpd uri ),
14             # Rendezvous::Publish stuff
15             qw( publisher service ));
16             our $VERSION = '0.05';
17              
18             =head1 NAME
19              
20             Net::DMAP::Server - base class for D[A-Z]AP servers
21              
22             =head1 SYNOPSIS
23              
24             package Net::DZAP::Server;
25             use base qw( Net::DMAP::Server );
26             sub protocol { 'dzap' }
27              
28             1;
29              
30             =head1 NAME
31              
32             Net::DZAP::Server - Digital Zebra Access Protocol (iZoo) Server
33              
34             =cut
35              
36             =head1 DESCRIPTION
37              
38             Net::DMAP::Server is a base class for implementing DMAP servers. It's
39             probably not hugely useful to you directly, and you're better off
40             looking at Net::DPAP::Server or Net::DAAP::Server.
41              
42             =cut
43              
44             sub new {
45             my $class = shift;
46             my $self = $class->SUPER::new( {
47             db_uuid => '13950142391337751523',
48             revision => 42,
49             tracks => {},
50             playlists => {},
51             waiting_clients => [],
52             poll_interval => 20,
53             @_ } );
54             $self->name( ref($self) ." " . hostname . " $$" ) unless $self->name;
55             $self->port( $self->default_port ) unless $self->port;
56             $self->find_tracks;
57             #print Dump $self;
58             $self->httpd( POE::Component::Server::HTTP->new(
59             Port => $self->port,
60             ContentHandler => { '/' => sub { $self->_handler(@_) } },
61             StreamHandler => sub { $self->stream_handler(@_) },
62             ) );
63              
64             my $publisher = Net::Rendezvous::Publish->new
65             or die "couldn't make a Responder object";
66             $self->publisher( $publisher );
67             $self->service( $publisher->publish(
68             name => $self->name,
69             type => '_'.$self->protocol.'._tcp',
70             port => $self->port,
71             txt => "Database ID=".$self->db_uuid."\x{1}Machine Name=".$self->name,,
72             ) );
73              
74             POE::Session->create(
75             inline_states => {
76             _start => sub {
77             $_[KERNEL]->alarm( poll_changed => time + $self->poll_interval );
78             },
79             poll_changed => sub {
80             $self->poll_changed;
81             $_[KERNEL]->yield('_start');
82             },
83             });
84              
85             return $self;
86             }
87              
88             sub stream_handler {
89             my $self = shift;
90             my ($request, $response) = @_;
91             }
92              
93             sub _handler {
94             my $self = shift;
95             my ($request, $response) = @_;
96             # always the same
97             $response->code( RC_OK );
98             $response->content_type( 'application/x-dmap-tagged' );
99              
100             local $self->{uri};
101             $self->uri( $request->uri );
102             print $request->uri, "\n" if $self->debug;
103              
104             # first match wins
105             my @methods = (
106             [ database_item => qr{^/databases/\d+/items/(\d+)\.} ],
107             [ database_items => qr{^/databases/(\d+)/items} ],
108             [ playlist_items => qr{^/databases/(\d+)/containers/(\d+)} ],
109             [ database_playlists => qr{^/databases/(\d+)/containers} ],
110             [ databases => qr{^/databases} ],
111             [ server_info => qr{^/server-info} ],
112             [ content_codes => qr{^/content-codes} ],
113             [ update => qr{^/update} ],
114             [ login => qr{^/login} ],
115             [ logout => qr{^/logout} ],
116             [ ignore => qr{^/this_request_is_simply_to_send_a_close_connection_header} ],
117             );
118              
119             for (@methods) {
120             my ($method, $pattern) = @$_;
121             if (my @matched = ($self->uri->path =~ $pattern)) {
122             #print "dispatching as $method\n" if $self->debug;
123             $self->$method( $request, $response, @matched );
124             return $response->code;
125             }
126             }
127              
128             print "Can't handle ".$self->uri->path."\n" if $self->debug;
129             $response->code( 500 );
130             return 500;
131             }
132              
133              
134             sub _dmap_pack {
135             my $self = shift;
136             my $dmap = shift;
137             return dmap_pack $dmap;
138             }
139              
140             sub find_tracks {
141             die "override me";
142             }
143              
144             sub database_item {
145             my ($self, $request, $response) = @_;
146             my $id = shift;
147             $response->content( $self->tracks->{$1}->data );
148             }
149              
150             sub content_codes {
151             my ($self, $request, $response) = @_;
152             $response->content($self->_dmap_pack(
153             [[ 'dmap.contentcodesresponse' => [
154             [ 'dmap.status' => 200 ],
155             map { [ 'dmap.dictionary' => [
156             [ 'dmap.contentcodesnumber' => $_->{ID} ],
157             [ 'dmap.contentcodesname' => $_->{NAME} ],
158             [ 'dmap.contentcodestype' => $_->{TYPE} ],
159             ] ] } values %$Net::DAAP::DMAP::Types,
160             ]]] ));
161             }
162              
163             sub login {
164             my ($self, $request, $response) = @_;
165             $response->content( $self->_dmap_pack(
166             [[ 'dmap.loginresponse' => [
167             [ 'dmap.status' => 200 ],
168             [ 'dmap.sessionid' => 42 ],
169             ]]] ));
170             }
171              
172             sub logout { }
173              
174             sub ignore { }
175              
176             sub update {
177             my ($self, $request, $response) = @_;
178             if ($self->uri =~ m{revision-number=(\d+)} && $1 >= $self->revision) {
179             print "queueing $response\n" if $self->debug;
180             push @{ $self->waiting_clients }, $response;
181             $response->code( RC_WAIT );
182             return;
183             }
184             $self->update_answer( $request, $response );
185             }
186              
187             sub has_changed { 0 }
188              
189             sub poll_changed {
190             my $self = shift;
191             if ($self->has_changed) {
192             $self->revision( $self->revision + 1 );
193             for my $response (@{ $self->waiting_clients }) {
194             print "continuing $response\n" if $self->debug;
195             $self->update_answer( undef, $response );
196             $response->code( RC_OK );
197             $response->continue;
198             }
199             $self->waiting_clients([]);
200             }
201             }
202              
203             sub update_answer {
204             my ($self, $request, $response) = @_;
205              
206             $response->content( $self->_dmap_pack(
207             [[ 'dmap.updateresponse' => [
208             [ 'dmap.status' => 200 ],
209             [ 'dmap.serverrevision' => $self->revision ],
210             ]]] ));
211             }
212              
213             sub databases {
214             my ($self, $request, $response) = @_;
215              
216             $response->content( $self->_dmap_pack(
217             [[ 'daap.serverdatabases' => [
218             [ 'dmap.status' => 200 ],
219             [ 'dmap.updatetype' => 0 ],
220             [ 'dmap.specifiedtotalcount' => 1 ],
221             [ 'dmap.returnedcount' => 1 ],
222             [ 'dmap.listing' => [
223             [ 'dmap.listingitem' => [
224             [ 'dmap.itemid' => 35 ],
225             [ 'dmap.persistentid' => $self->db_uuid ],
226             [ 'dmap.itemname' => $self->name ],
227             [ 'dmap.itemcount' => scalar keys %{ $self->tracks } ],
228             [ 'dmap.containercount' => 1 ],
229             ],
230             ],
231             ],
232             ],
233             ]]] ));
234             }
235              
236             sub database_items {
237             my ($self, $request, $response, $database_id) = @_;
238             my $tracks = $self->_all_tracks;
239             $response->content( $self->_dmap_pack(
240             [[ 'daap.databasesongs' => [
241             [ 'dmap.status' => 200 ],
242             [ 'dmap.updatetype' => 0 ],
243             [ 'dmap.specifiedtotalcount' => scalar @$tracks ],
244             [ 'dmap.returnedcount' => scalar @$tracks ],
245             [ 'dmap.listing' => $tracks ]
246             ]]] ));
247             }
248              
249             sub database_playlists {
250             my ($self, $request, $response, $database_id) = @_;
251              
252             my $tracks = $self->_all_tracks;
253             my $playlists = [
254             [ 'dmap.listingitem' => [
255             [ 'dmap.itemid' => 39 ],
256             [ 'dmap.persistentid' => '13950142391337751524' ],
257             [ 'dmap.itemname' => $self->name ],
258             [ 'com.apple.itunes.smart-playlist' => 0 ],
259             [ 'dmap.itemcount' => scalar @$tracks ],
260             ],
261             ],
262             map {
263             [ 'dmap.listingitem' => [
264             [ 'dmap.itemid' => $_->dmap_itemid ],
265             [ 'dmap.persistentid' => $_->dmap_persistentid ],
266             [ 'dmap.itemname' => $_->dmap_itemname ],
267             [ 'com.apple.itunes.smart-playlist' => 0 ],
268             [ 'dmap.itemcount' => scalar @{ $_->items } ],
269             ],
270             ],
271             } values %{ $self->playlists },
272             ];
273             $response->content( $self->_dmap_pack(
274             [[ 'daap.databaseplaylists' => [
275             [ 'dmap.status' => 200 ],
276             [ 'dmap.updatetype' => 0 ],
277             [ 'dmap.specifiedtotalcount' => 1 ],
278             [ 'dmap.returnedcount' => 1 ],
279             [ 'dmap.listing' => $playlists ],
280             ]]] ));
281             }
282              
283             sub playlist_items {
284             my ($self, $request, $response, $database_id, $playlist_id) = @_;
285              
286             my $playlist = $self->playlists->{ $playlist_id };
287              
288             my $tracks = $self->_all_tracks( $playlist ? @{ $playlist->items } : () );
289             $response->content( $self->_dmap_pack(
290             [[ 'daap.playlistsongs' => [
291             [ 'dmap.status' => 200 ],
292             [ 'dmap.updatetype' => 0 ],
293             [ 'dmap.specifiedtotalcount' => scalar @$tracks ],
294             [ 'dmap.returnedcount' => scalar @$tracks ],
295             [ 'dmap.listing' => $tracks ]
296             ]]] ));
297             }
298              
299              
300              
301             sub item_field {
302             my $self = shift;
303             my $track = shift;
304             my $field = shift;
305              
306             (my $method = $field) =~ s{[.-]}{_}g;
307             # kludge
308             if ($field =~ /dpap\.(thumb|hires)/) {
309             $field = 'dpap.picturedata';
310             }
311              
312             [ $field => eval { $track->$method() } ]
313             }
314              
315             sub uniq {
316             my %seen;
317             return grep { !$seen{$_}++ } @_;
318             }
319              
320             # some things are always present in the listings returned, whether you
321             # ask for them or not
322             sub _always_answer {
323             qw( dmap.itemkind dmap.itemid dmap.itemname );
324             }
325              
326             sub _response_fields {
327             my $self = shift;
328              
329             my $meta = { $self->_uri_arguments }->{meta} || '';
330             my @fields = uniq $self->_always_answer, split /(?:,|%2C)/, $meta;
331             return @fields;
332             }
333              
334             sub _uri_arguments {
335             my $self = shift;
336             my @chunks = split /&/, $self->uri->query || '';
337             return map { split /=/, $_, 2 } @chunks;
338             }
339              
340             sub _all_tracks {
341             my $self = shift;
342              
343             # cheat for playlist support
344             my @tracks;
345             if (@_) {
346             @tracks = @_;
347             }
348             else {
349             # sometimes, all isn't really all (DPAP)
350             my $query = { $self->_uri_arguments }->{query} || '';
351             @tracks = $query =~ /dmap\.itemid/
352             ? map { $self->tracks->{$_} } $query =~ /dmap\.itemid:(\d+)/g
353             : values %{ $self->tracks };
354             }
355              
356             my @fields = $self->_response_fields;
357             my @results;
358             for my $track (@tracks) {
359             push @results, [ 'dmap.listingitem' => [
360             map { $self->item_field( $track => $_ ) } @fields ] ];
361             }
362             return \@results;
363             }
364              
365              
366              
367             =head1 BUGS
368              
369             The Digital Zebra Access Protocol does not exist, so you'll have to
370             manually acquire your own horses and paint them.
371              
372              
373             =head1 AUTHOR
374              
375             Richard Clamp
376              
377             =head1 COPYRIGHT
378              
379             Copyright 2004, 2005, 2006 Richard Clamp. All Rights Reserved.
380              
381             This program is free software; you can redistribute it and/or modify
382             it under the same terms as Perl itself.
383              
384             =head1 SEE ALSO
385              
386             Net::DAAP::Server, Net::DPAP::Server
387              
388             =cut
389              
390             1;