File Coverage

blib/lib/AnyEvent/DAAP/Server.pm
Criterion Covered Total %
statement 4 6 66.6
branch n/a
condition n/a
subroutine 2 2 100.0
pod n/a
total 6 8 75.0


line stmt bran cond sub pod time code
1             package AnyEvent::DAAP::Server;
2 2     2   2009 use Any::Moose;
  2         87838  
  2         15  
3 2     2   2745 use AnyEvent::DAAP::Server::Connection;
  0            
  0            
4             use AnyEvent::Socket;
5             use AnyEvent::Handle;
6             use Net::Rendezvous::Publish;
7             use Net::DAAP::DMAP qw(dmap_pack);
8             use HTTP::Request;
9             use Router::Simple;
10             use URI::QueryParam;
11              
12             our $VERSION = '0.01';
13              
14             has name => (
15             is => 'rw',
16             isa => 'Str',
17             default => sub { ref $_[0] },
18             );
19              
20             has port => (
21             is => 'rw',
22             isa => 'Int',
23             default => 3689,
24             );
25              
26             has rendezvous_publisher => (
27             is => 'rw',
28             isa => 'Net::Rendezvous::Publish',
29             default => sub { Net::Rendezvous::Publish->new },
30             );
31              
32             has rendezvous_service => (
33             is => 'rw',
34             isa => 'Net::Rendezvous::Publish::Service',
35             lazy_build => 1,
36             );
37              
38             sub _build_rendezvous_service {
39             my $self = shift;
40             return $self->rendezvous_publisher->publish(
41             port => $self->port,
42             name => $self->name,
43             type => '_daap._tcp',
44             );
45             }
46              
47             has db_id => (
48             is => 'rw',
49             default => '13950142391337751523', # XXX magic value (from Net::DAAP::Server)
50             );
51              
52             has tracks => (
53             is => 'rw',
54             isa => 'HashRef[AnyEvent::DAAP::Server::Track]',
55             default => sub { +{} },
56             );
57              
58             has global_playlist => (
59             is => 'rw',
60             isa => 'AnyEvent::DAAP::Server::Playlist',
61             default => sub { AnyEvent::DAAP::Server::Playlist->new },
62             );
63              
64             has playlists => (
65             is => 'rw',
66             isa => 'HashRef[AnyEvent::DAAP::Server::Playlist]',
67             default => sub { +{} },
68             );
69              
70             has revision => (
71             is => 'rw',
72             isa => 'Int',
73             default => 1,
74             );
75              
76             has connections => (
77             is => 'rw',
78             isa => 'ArrayRef[AnyEvent::DAAP::Server::Connection]',
79             default => sub { +[] },
80             );
81              
82             has router => (
83             is => 'rw',
84             isa => 'Router::Simple',
85             default => sub { Router::Simple->new },
86             );
87              
88             __PACKAGE__->meta->make_immutable;
89              
90             no Any::Moose;
91              
92             sub BUILD {
93             my $self = shift;
94             $self->add_playlist($self->global_playlist);
95             }
96              
97             sub publish {
98             my $self = shift;
99             $self->rendezvous_service; # build
100             }
101              
102             sub setup {
103             my $self = shift;
104              
105             my @route = (
106             '/databases/{database_id}/items' => '_database_items',
107             '/databases/{database_id}/containers' => '_database_containers',
108             '/databases/{database_id}/containers/{container_id}/items' => '_database_container_items',
109             '/databases/{database_id}/items/{item_id}.*' => '_database_item',
110             );
111              
112             while (my ($route, $method) = splice @route, 0, 2) {
113             $self->router->connect($route => { method => $method });
114             }
115              
116             $self->publish;
117              
118             tcp_server undef, $self->port, sub {
119             my ($fh, $host, $port) = @_;
120             my $connection = AnyEvent::DAAP::Server::Connection->new(server => $self, fh => $fh);
121             $connection->handle->on_read(sub {
122             my ($handle) = @_;
123             $handle->push_read(
124             regex => qr<\r\n\r\n>, sub {
125             my ($handle, $data) = @_;
126             my $request = HTTP::Request->parse($data);
127             my $path = $request->uri->path;
128             my $p = $self->router->match($path) || {};
129             my $method = $p->{method} || $path;
130             $method =~ s<[/-]><_>g;
131             $self->$method($connection, $request, $p);
132             }
133             );
134             });
135             push @{ $self->connections }, $connection;
136             };
137             }
138              
139             sub database_updated {
140             my $self = shift;
141             $self->{revision}++;
142             foreach my $connection (@{ $self->connections }) {
143             $connection->pause_cv->send if $connection->pause_cv;
144             }
145             }
146              
147             # XXX dmap_itemid is used as only its lower 3 bytes
148              
149             sub add_track {
150             my ($self, $track) = @_;
151             $self->tracks->{ $track->dmap_itemid & 0xFFFFFF } = $track;
152             $self->global_playlist->add_track($track);
153             }
154              
155             sub add_playlist {
156             my ($self, $playlist) = @_;
157             $self->playlists->{ $playlist->dmap_itemid & 0xFFFFFF } = $playlist;
158             }
159              
160             ### Handlers
161              
162             sub _server_info {
163             my ($self, $connection) = @_;
164             $connection->respond_dmap([[
165             'dmap.serverinforesponse' => [
166             [ 'dmap.status' => 200 ],
167             [ 'dmap.protocolversion' => 2 ],
168             [ 'daap.protocolversion' => '3.11' ],
169             [ 'dmap.itemname' => $self->name ],
170             [ 'dmap.loginrequired' => 1 ],
171             [ 'dmap.timeoutinterval' => 1800 ],
172             [ 'dmap.supportsautologout' => 0 ],
173             [ 'dmap.supportsupdate' => 1 ],
174             [ 'dmap.supportspersistentids' => 0 ],
175             [ 'dmap.supportsextensions' => 0 ],
176             [ 'dmap.supportsbrowse' => 0 ],
177             [ 'dmap.supportsquery' => 0 ],
178             [ 'dmap.supportsindex' => 0 ],
179             [ 'dmap.supportsresolve' => 0 ],
180             [ 'dmap.databasescount' => 1 ],
181             ]
182             ]]);
183             }
184              
185             sub _login {
186             my ($self, $connection) = @_;
187             $connection->respond_dmap([[
188             'dmap.loginresponse' => [
189             [ 'dmap.status' => 200 ],
190             [ 'dmap.sessionid' => 42 ], # XXX does not have session, magic number
191             ]
192             ]]);
193             }
194              
195             sub _update {
196             my ($self, $connection, $req) = @_;
197              
198             if ($req->uri->query_param('delta')) {
199             my $cv = $connection->pause(sub {
200             $connection->respond_dmap([[
201             'dmap.updateresponse' => [
202             [ 'dmap.status' => 200 ],
203             [ 'dmap.serverrevision' => $self->revision ],
204             ]
205             ]]);
206             });
207             my $w; $w = AE::timer 60, 0, sub { undef $w; $cv->send };
208             } else {
209             $connection->respond_dmap([[
210             'dmap.updateresponse' => [
211             [ 'dmap.status' => 200 ],
212             [ 'dmap.serverrevision' => $self->revision ],
213             ]
214             ]]);
215             }
216             }
217              
218             sub _databases {
219             my ($self, $connection) = @_;
220              
221             $connection->respond_dmap([[
222             'daap.serverdatabases' => [
223             [ 'dmap.status' => 200 ],
224             [ 'dmap.updatetype' => 0 ],
225             [ 'dmap.specifiedtotalcount' => 1 ],
226             [ 'dmap.returnedcount' => 1 ],
227             [ 'dmap.listing' => [
228             [ 'dmap.listingitem' => [
229             [ 'dmap.itemid' => 1 ], # XXX magic
230             [ 'dmap.persistentid' => $self->db_id ],
231             [ 'dmap.itemname' => $self->name ],
232             [ 'dmap.itemcount' => scalar keys %{ $self->tracks } ],
233             [ 'dmap.containercount' => 1 ],
234             ] ],
235             ] ],
236             ]
237             ]]);
238             }
239              
240             sub _database_items {
241             my ($self, $connection, $req, $args) = @_;
242             # $args->{database_id};
243              
244             my $tracks = $self->__format_tracks_as_dmap($req, [ values %{ $self->tracks } ]);
245             $connection->respond_dmap([[
246             'daap.databasesongs' => [
247             [ 'dmap.status' => 200 ],
248             [ 'dmap.updatetype' => 0 ],
249             [ 'dmap.specifiedtotalcount' => scalar @$tracks ],
250             [ 'dmap.returnedcount' => scalar @$tracks ],
251             [ 'dmap.listing' => $tracks ]
252             ]
253             ]]);
254             }
255              
256             sub _database_containers {
257             my ($self, $connection, $req, $args) = @_;
258             # $args->{database_id};
259              
260             my @playlists = map { $_->as_dmap_struct } $self->global_playlist, values %{ $self->playlists };
261              
262             $connection->respond_dmap([[
263             'daap.databaseplaylists' => [
264             [ 'dmap.status' => 200 ],
265             [ 'dmap.updatetype' => 0 ],
266             [ 'dmap.specifiedtotalcount' => 1 ],
267             [ 'dmap.returnedcount' => 1 ],
268             [ 'dmap.listing' => \@playlists ],
269             ]
270             ]]);
271             }
272              
273             sub _database_container_items {
274             my ($self, $connection, $req, $args) = @_;
275             # $args->{database_id}, $args->{container_id}
276              
277             my $playlist = $self->playlists->{ $args->{container_id} }
278             or return $connection->respond(404);
279              
280             my $tracks = $self->__format_tracks_as_dmap($req, scalar $playlist->tracks);
281             $connection->respond_dmap([[
282             'daap.playlistsongs' => [
283             [ 'dmap.status' => 200 ],
284             [ 'dmap.updatetype' => 0 ],
285             [ 'dmap.specifiedtotalcount' => scalar @$tracks ],
286             [ 'dmap.returnedcount' => scalar @$tracks ],
287             [ 'dmap.listing' => $tracks ]
288             ]
289             ]]);
290             }
291              
292             sub _database_item {
293             my ($self, $connection, $req, $args) = @_;
294             # $args->{database_id}, $args->{item_id}
295              
296             my $track = $self->tracks->{ $args->{item_id} }
297             or return $connection->respond(404);
298              
299             $track->stream($connection, $req, $args);
300             }
301              
302             sub __format_tracks_as_dmap {
303             my ($self, $req, $tracks) = @_;
304              
305             my @fields = ( qw(dmap.itemkind dmap.itemid dmap.itemname), split /,|%2C/i, scalar $req->uri->query_param('meta') || '' );
306              
307             my @tracks;
308             foreach my $track (@$tracks) {
309             push @tracks, [
310             'dmap.listingitem' => [ map { [ $_ => $track->_dmap_field($_) ] } @fields ]
311             ]
312             }
313              
314             return \@tracks;
315             }
316              
317             1;
318              
319             __END__