line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
1
|
1
|
|
|
1
|
|
919
|
use strict; |
|
1
|
|
|
|
|
2
|
|
|
1
|
|
|
|
|
47
|
|
2
|
|
|
|
|
|
|
package Net::DAAP::Client; |
3
|
1
|
|
|
1
|
|
522
|
use Net::DAAP::Client::v2; |
|
1
|
|
|
|
|
2
|
|
|
1
|
|
|
|
|
28
|
|
4
|
1
|
|
|
1
|
|
528
|
use Net::DAAP::Client::v3; |
|
1
|
|
|
|
|
3
|
|
|
1
|
|
|
|
|
36
|
|
5
|
1
|
|
|
1
|
|
518
|
use Net::DAAP::DMAP 1.22; |
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
6
|
|
|
|
|
|
|
use Net::DAAP::DMAP qw(:all); |
7
|
|
|
|
|
|
|
use LWP; |
8
|
|
|
|
|
|
|
use HTTP::Request::Common; |
9
|
|
|
|
|
|
|
use Carp; |
10
|
|
|
|
|
|
|
use sigtrap qw(die untrapped normal-signals); |
11
|
|
|
|
|
|
|
use vars qw( $VERSION ); |
12
|
|
|
|
|
|
|
$VERSION = '0.42'; |
13
|
|
|
|
|
|
|
|
14
|
|
|
|
|
|
|
=head1 NAME |
15
|
|
|
|
|
|
|
|
16
|
|
|
|
|
|
|
Net::DAAP::Client - client for Apple iTunes DAAP service |
17
|
|
|
|
|
|
|
|
18
|
|
|
|
|
|
|
=head1 SYNOPSIS |
19
|
|
|
|
|
|
|
|
20
|
|
|
|
|
|
|
my $daap; # see WARNING below |
21
|
|
|
|
|
|
|
$daap = Net::DAAP::Client->new(SERVER_HOST => $hostname, |
22
|
|
|
|
|
|
|
SERVER_PORT => $portnum, |
23
|
|
|
|
|
|
|
PASSWORD => $password); |
24
|
|
|
|
|
|
|
$dsn = $daap->connect; |
25
|
|
|
|
|
|
|
|
26
|
|
|
|
|
|
|
$dbs_hash = $daap->databases; |
27
|
|
|
|
|
|
|
$current_db = $daap->db; |
28
|
|
|
|
|
|
|
$daap_db($new_db_id); |
29
|
|
|
|
|
|
|
|
30
|
|
|
|
|
|
|
$songs_hash = $daap->songs; |
31
|
|
|
|
|
|
|
$playlists_hash = $daap->playlists; |
32
|
|
|
|
|
|
|
$array_of_songs_in_playlist = $daap->playlist($playlist_id); |
33
|
|
|
|
|
|
|
|
34
|
|
|
|
|
|
|
$url = $daap->url($song_or_playlist_id); |
35
|
|
|
|
|
|
|
|
36
|
|
|
|
|
|
|
$binary_audio_data = $obj->get($song_id); |
37
|
|
|
|
|
|
|
$binary_audio_data = $obj->get(@song_ids); |
38
|
|
|
|
|
|
|
$song_id = $obj->save($dir, $song_id); |
39
|
|
|
|
|
|
|
@song_ids = $obj->get($dir, @song_ids); |
40
|
|
|
|
|
|
|
|
41
|
|
|
|
|
|
|
$daap->disconnect; |
42
|
|
|
|
|
|
|
|
43
|
|
|
|
|
|
|
if ($daap->error) { |
44
|
|
|
|
|
|
|
warn $daap->error; # returns error string |
45
|
|
|
|
|
|
|
} |
46
|
|
|
|
|
|
|
|
47
|
|
|
|
|
|
|
=head1 DESCRIPTION |
48
|
|
|
|
|
|
|
|
49
|
|
|
|
|
|
|
Net::DAAP::Client provides objects representing connections to DAAP |
50
|
|
|
|
|
|
|
servers. You can fetch databases, playlists, and songs. This module |
51
|
|
|
|
|
|
|
was written based on a reverse engineering of Apple's iTunes 4 sharing |
52
|
|
|
|
|
|
|
implementation. As a result, features that iTunes 4 doesn't support |
53
|
|
|
|
|
|
|
(browsing, searching) aren't supported here. |
54
|
|
|
|
|
|
|
|
55
|
|
|
|
|
|
|
Each connection object has a destructor, so that you can forget to |
56
|
|
|
|
|
|
|
C without leaving the server expecting you to call back. |
57
|
|
|
|
|
|
|
|
58
|
|
|
|
|
|
|
=head2 WARNING |
59
|
|
|
|
|
|
|
|
60
|
|
|
|
|
|
|
If you store your object in a global variable, Perl can't seem to |
61
|
|
|
|
|
|
|
disconnect gracefully from the server. Until I figure out why, always |
62
|
|
|
|
|
|
|
store your object in a lexical (C) variable. |
63
|
|
|
|
|
|
|
|
64
|
|
|
|
|
|
|
=head1 METHODS |
65
|
|
|
|
|
|
|
|
66
|
|
|
|
|
|
|
=cut |
67
|
|
|
|
|
|
|
|
68
|
|
|
|
|
|
|
my $DAAP_Port = 3689; |
69
|
|
|
|
|
|
|
my @User_Columns = qw( SERVER_HOST SERVER_PORT PASSWORD DEBUG SONG_ATTRIBUTES ); |
70
|
|
|
|
|
|
|
my %Defaults = ( |
71
|
|
|
|
|
|
|
# user-specified |
72
|
|
|
|
|
|
|
SERVER_HOST => "", |
73
|
|
|
|
|
|
|
SERVER_PORT => $DAAP_Port, |
74
|
|
|
|
|
|
|
PASSWORD => "", |
75
|
|
|
|
|
|
|
DEBUG => 0, |
76
|
|
|
|
|
|
|
SONG_ATTRIBUTES => [ qw(dmap.itemid dmap.itemname dmap.persistentid |
77
|
|
|
|
|
|
|
daap.songalbum daap.songartist daap.songformat |
78
|
|
|
|
|
|
|
daap.songsize) ], |
79
|
|
|
|
|
|
|
|
80
|
|
|
|
|
|
|
# private |
81
|
|
|
|
|
|
|
ERROR => "", |
82
|
|
|
|
|
|
|
CONNECTED => 0, |
83
|
|
|
|
|
|
|
DATABASE_LIST => undef, |
84
|
|
|
|
|
|
|
DATABASE => undef, |
85
|
|
|
|
|
|
|
SONGS => undef, |
86
|
|
|
|
|
|
|
PLAYLISTS => undef, |
87
|
|
|
|
|
|
|
VALIDATOR => undef, |
88
|
|
|
|
|
|
|
); |
89
|
|
|
|
|
|
|
|
90
|
|
|
|
|
|
|
|
91
|
|
|
|
|
|
|
sub new { |
92
|
|
|
|
|
|
|
my $class = shift; |
93
|
|
|
|
|
|
|
my $self = bless { %Defaults } => $class; |
94
|
|
|
|
|
|
|
|
95
|
|
|
|
|
|
|
if (@_ > 1) { |
96
|
|
|
|
|
|
|
$self->_init(@_); |
97
|
|
|
|
|
|
|
} elsif (@_) { |
98
|
|
|
|
|
|
|
$self->{SERVER_HOST} = shift; |
99
|
|
|
|
|
|
|
} else { |
100
|
|
|
|
|
|
|
warn "Why are you calling new with no arguments?"; |
101
|
|
|
|
|
|
|
die "Need to implement get/set for hostname and port"; |
102
|
|
|
|
|
|
|
} |
103
|
|
|
|
|
|
|
|
104
|
|
|
|
|
|
|
return $self; |
105
|
|
|
|
|
|
|
} |
106
|
|
|
|
|
|
|
|
107
|
|
|
|
|
|
|
=head2 * new() |
108
|
|
|
|
|
|
|
|
109
|
|
|
|
|
|
|
$obj = Net::DAAP::Client->new(OPTNAME => $value, ...); |
110
|
|
|
|
|
|
|
|
111
|
|
|
|
|
|
|
The allowed options are: |
112
|
|
|
|
|
|
|
|
113
|
|
|
|
|
|
|
=over 4 |
114
|
|
|
|
|
|
|
|
115
|
|
|
|
|
|
|
=item SERVER_NAME |
116
|
|
|
|
|
|
|
|
117
|
|
|
|
|
|
|
The hostname or IP address of the server. |
118
|
|
|
|
|
|
|
|
119
|
|
|
|
|
|
|
=item SERVER_PORT |
120
|
|
|
|
|
|
|
|
121
|
|
|
|
|
|
|
The port number of the server. |
122
|
|
|
|
|
|
|
|
123
|
|
|
|
|
|
|
=item PASSWORD |
124
|
|
|
|
|
|
|
|
125
|
|
|
|
|
|
|
The password to use when authenticating. |
126
|
|
|
|
|
|
|
|
127
|
|
|
|
|
|
|
=item DEBUG |
128
|
|
|
|
|
|
|
|
129
|
|
|
|
|
|
|
Print some debugging output |
130
|
|
|
|
|
|
|
|
131
|
|
|
|
|
|
|
|
132
|
|
|
|
|
|
|
=item SONG_ATTRIBUTES |
133
|
|
|
|
|
|
|
|
134
|
|
|
|
|
|
|
The attributes to retrieve for a song as an array reference. The |
135
|
|
|
|
|
|
|
default list is: |
136
|
|
|
|
|
|
|
|
137
|
|
|
|
|
|
|
[qw( dmap.itemid dmap.itemname dmap.persistentid daap.songalbum |
138
|
|
|
|
|
|
|
daap.songartist daap.songformat daap.songsize )] |
139
|
|
|
|
|
|
|
|
140
|
|
|
|
|
|
|
=back |
141
|
|
|
|
|
|
|
|
142
|
|
|
|
|
|
|
=cut |
143
|
|
|
|
|
|
|
|
144
|
|
|
|
|
|
|
sub _init { |
145
|
|
|
|
|
|
|
my $self = shift; |
146
|
|
|
|
|
|
|
my %opts = @_; |
147
|
|
|
|
|
|
|
|
148
|
|
|
|
|
|
|
foreach my $key (@User_Columns) { |
149
|
|
|
|
|
|
|
$self->{$key} = $opts{$key} || $Defaults{$key}; |
150
|
|
|
|
|
|
|
} |
151
|
|
|
|
|
|
|
} |
152
|
|
|
|
|
|
|
|
153
|
|
|
|
|
|
|
sub _debug { |
154
|
|
|
|
|
|
|
my $self = shift; |
155
|
|
|
|
|
|
|
warn "$_[0]\n" if $self->{DEBUG}; |
156
|
|
|
|
|
|
|
} |
157
|
|
|
|
|
|
|
|
158
|
|
|
|
|
|
|
=head2 * connect() |
159
|
|
|
|
|
|
|
|
160
|
|
|
|
|
|
|
$name = $obj->connect |
161
|
|
|
|
|
|
|
or die $obj->error; |
162
|
|
|
|
|
|
|
|
163
|
|
|
|
|
|
|
Attempts to fetch the server information, log in, and learn the latest |
164
|
|
|
|
|
|
|
revision number. It returns the name of the server we've connected to |
165
|
|
|
|
|
|
|
(as that server reported it). It returns C if any of the steps |
166
|
|
|
|
|
|
|
fail. If it fails fetching the revision number, it logs out before |
167
|
|
|
|
|
|
|
returning C. |
168
|
|
|
|
|
|
|
|
169
|
|
|
|
|
|
|
=cut |
170
|
|
|
|
|
|
|
|
171
|
|
|
|
|
|
|
|
172
|
|
|
|
|
|
|
sub connect { |
173
|
|
|
|
|
|
|
my $self = shift; |
174
|
|
|
|
|
|
|
my $ua = ($self->{UA} ||= Net::DAAP::Client::UA->new(keep_alive => 1) ); |
175
|
|
|
|
|
|
|
my ($dmap, $id); |
176
|
|
|
|
|
|
|
|
177
|
|
|
|
|
|
|
$self->_devine_validator; |
178
|
|
|
|
|
|
|
|
179
|
|
|
|
|
|
|
|
180
|
|
|
|
|
|
|
$self->error(""); |
181
|
|
|
|
|
|
|
$self->{DATABASE_LIST} = undef; |
182
|
|
|
|
|
|
|
|
183
|
|
|
|
|
|
|
# get content codes |
184
|
|
|
|
|
|
|
$dmap = $self->_do_get("content-codes") or return; |
185
|
|
|
|
|
|
|
update_content_codes(dmap_unpack($dmap)); |
186
|
|
|
|
|
|
|
|
187
|
|
|
|
|
|
|
# check server name/version |
188
|
|
|
|
|
|
|
$dmap = $self->_do_get("server-info") or return; |
189
|
|
|
|
|
|
|
|
190
|
|
|
|
|
|
|
my %hash = dmap_flat_list( dmap_unpack ($dmap) ); |
191
|
|
|
|
|
|
|
my $data_source_name = $hash{'/dmap.serverinforesponse/dmap.itemname'}; |
192
|
|
|
|
|
|
|
$self->{DSN} = $data_source_name; |
193
|
|
|
|
|
|
|
$self->_debug("Connected to iTunes share '$data_source_name'"); |
194
|
|
|
|
|
|
|
|
195
|
|
|
|
|
|
|
# log in |
196
|
|
|
|
|
|
|
$dmap = $self->_do_get("login") or return; |
197
|
|
|
|
|
|
|
$id = dmap_seek(dmap_unpack($dmap), "dmap.loginresponse/dmap.sessionid"); |
198
|
|
|
|
|
|
|
$self->{ID} = $id; |
199
|
|
|
|
|
|
|
$self->_debug("my id is $id"); |
200
|
|
|
|
|
|
|
|
201
|
|
|
|
|
|
|
$self->{CONNECTED} = 1; |
202
|
|
|
|
|
|
|
|
203
|
|
|
|
|
|
|
# fetch databases |
204
|
|
|
|
|
|
|
my $dbs = $self->databases() |
205
|
|
|
|
|
|
|
or return; |
206
|
|
|
|
|
|
|
|
207
|
|
|
|
|
|
|
# autoselect if only one database present |
208
|
|
|
|
|
|
|
if (keys(%$dbs) == 1) { |
209
|
|
|
|
|
|
|
$self->db((keys %$dbs)[0]) |
210
|
|
|
|
|
|
|
or return; |
211
|
|
|
|
|
|
|
} |
212
|
|
|
|
|
|
|
|
213
|
|
|
|
|
|
|
return $self->{DSN}; |
214
|
|
|
|
|
|
|
} |
215
|
|
|
|
|
|
|
|
216
|
|
|
|
|
|
|
=head2 * databases() |
217
|
|
|
|
|
|
|
|
218
|
|
|
|
|
|
|
$dbs = $self->databases(); |
219
|
|
|
|
|
|
|
|
220
|
|
|
|
|
|
|
Returns a hash reference. Sample: |
221
|
|
|
|
|
|
|
|
222
|
|
|
|
|
|
|
=cut |
223
|
|
|
|
|
|
|
|
224
|
|
|
|
|
|
|
sub databases { |
225
|
|
|
|
|
|
|
my $self = shift; |
226
|
|
|
|
|
|
|
|
227
|
|
|
|
|
|
|
$self->error(""); |
228
|
|
|
|
|
|
|
|
229
|
|
|
|
|
|
|
unless ($self->{CONNECTED}) { |
230
|
|
|
|
|
|
|
$self->error("Not connected--can't fetch databases list"); |
231
|
|
|
|
|
|
|
return; |
232
|
|
|
|
|
|
|
} |
233
|
|
|
|
|
|
|
|
234
|
|
|
|
|
|
|
my $res = $self->_do_get("databases"); |
235
|
|
|
|
|
|
|
my $listing = dmap_seek(dmap_unpack($res), |
236
|
|
|
|
|
|
|
"daap.serverdatabases/dmap.listing"); |
237
|
|
|
|
|
|
|
|
238
|
|
|
|
|
|
|
unless ($listing) { |
239
|
|
|
|
|
|
|
$self->error("databases query didn't return a list of databases"); |
240
|
|
|
|
|
|
|
return; |
241
|
|
|
|
|
|
|
} |
242
|
|
|
|
|
|
|
|
243
|
|
|
|
|
|
|
my $struct = $self->_unpack_listing_to_hash($listing); |
244
|
|
|
|
|
|
|
|
245
|
|
|
|
|
|
|
$self->{DATABASE_LIST} = $struct; |
246
|
|
|
|
|
|
|
return $struct; |
247
|
|
|
|
|
|
|
} |
248
|
|
|
|
|
|
|
|
249
|
|
|
|
|
|
|
=head2 * db() |
250
|
|
|
|
|
|
|
|
251
|
|
|
|
|
|
|
$db_id = $obj->db; # learn current database ID |
252
|
|
|
|
|
|
|
$obj->db($db_id); # set current database |
253
|
|
|
|
|
|
|
|
254
|
|
|
|
|
|
|
A database ID is a key from the hash returned by |
255
|
|
|
|
|
|
|
C<< $obj->databases >>. |
256
|
|
|
|
|
|
|
|
257
|
|
|
|
|
|
|
Setting the database loads the playlists and song list for that |
258
|
|
|
|
|
|
|
database. This can take some time if there are a lot of songs in |
259
|
|
|
|
|
|
|
either list. |
260
|
|
|
|
|
|
|
|
261
|
|
|
|
|
|
|
This method returns true if an error occurred, false otherwise. |
262
|
|
|
|
|
|
|
If an error occurs, you can't rely on the song list or play list |
263
|
|
|
|
|
|
|
having been loaded. |
264
|
|
|
|
|
|
|
|
265
|
|
|
|
|
|
|
=cut |
266
|
|
|
|
|
|
|
|
267
|
|
|
|
|
|
|
sub db { |
268
|
|
|
|
|
|
|
my ($self, $db_id) = @_; |
269
|
|
|
|
|
|
|
my $db; |
270
|
|
|
|
|
|
|
|
271
|
|
|
|
|
|
|
unless ($self->{DATABASE_LIST}) { |
272
|
|
|
|
|
|
|
$self->error("You haven't fetched the list of databases yet"); |
273
|
|
|
|
|
|
|
return; |
274
|
|
|
|
|
|
|
} |
275
|
|
|
|
|
|
|
|
276
|
|
|
|
|
|
|
unless (defined $db_id) { |
277
|
|
|
|
|
|
|
return $self->{DATABASE}; |
278
|
|
|
|
|
|
|
} |
279
|
|
|
|
|
|
|
|
280
|
|
|
|
|
|
|
$db = $self->{DATABASE_LIST}{$db_id}; |
281
|
|
|
|
|
|
|
if (defined $db) { |
282
|
|
|
|
|
|
|
$self->{DATABASE} = $db_id; |
283
|
|
|
|
|
|
|
$self->_debug("Loading songs from database $db->{'dmap.itemname'}"); |
284
|
|
|
|
|
|
|
$self->{SONGS} = $self->_get_songs($db_id) |
285
|
|
|
|
|
|
|
or return; |
286
|
|
|
|
|
|
|
$self->_debug("Loading playlists from database $db->{'dmap.itemname'}"); |
287
|
|
|
|
|
|
|
$self->{PLAYLISTS} = $self->_get_playlists($db_id) |
288
|
|
|
|
|
|
|
or return; |
289
|
|
|
|
|
|
|
} else { |
290
|
|
|
|
|
|
|
$self->error("Database ID $db_id not found"); |
291
|
|
|
|
|
|
|
return; |
292
|
|
|
|
|
|
|
} |
293
|
|
|
|
|
|
|
|
294
|
|
|
|
|
|
|
return $self; |
295
|
|
|
|
|
|
|
} |
296
|
|
|
|
|
|
|
|
297
|
|
|
|
|
|
|
=head2 * songs() |
298
|
|
|
|
|
|
|
|
299
|
|
|
|
|
|
|
$songs = $obj->songs(); |
300
|
|
|
|
|
|
|
|
301
|
|
|
|
|
|
|
Returns a hash reference. Keys are song IDs, values are hashes with |
302
|
|
|
|
|
|
|
information on the song. Information fetched is specified by |
303
|
|
|
|
|
|
|
SONG_ATTRIBUTES, the default set is: |
304
|
|
|
|
|
|
|
|
305
|
|
|
|
|
|
|
=over |
306
|
|
|
|
|
|
|
|
307
|
|
|
|
|
|
|
=item dmap.itemid |
308
|
|
|
|
|
|
|
|
309
|
|
|
|
|
|
|
Unique ID for the song. |
310
|
|
|
|
|
|
|
|
311
|
|
|
|
|
|
|
=item dmap.itemname |
312
|
|
|
|
|
|
|
|
313
|
|
|
|
|
|
|
Title of the track. |
314
|
|
|
|
|
|
|
|
315
|
|
|
|
|
|
|
=item dmap.persistentid |
316
|
|
|
|
|
|
|
|
317
|
|
|
|
|
|
|
XXX [add useful explanation here] |
318
|
|
|
|
|
|
|
|
319
|
|
|
|
|
|
|
=item daap.songalbum |
320
|
|
|
|
|
|
|
|
321
|
|
|
|
|
|
|
Album name that the track came from. |
322
|
|
|
|
|
|
|
|
323
|
|
|
|
|
|
|
=item daap.songartist |
324
|
|
|
|
|
|
|
|
325
|
|
|
|
|
|
|
Artist who recorded the track. |
326
|
|
|
|
|
|
|
|
327
|
|
|
|
|
|
|
=item daap.songformat |
328
|
|
|
|
|
|
|
|
329
|
|
|
|
|
|
|
A string, "mp3", "aiff", etc. |
330
|
|
|
|
|
|
|
|
331
|
|
|
|
|
|
|
=item daap.songsize |
332
|
|
|
|
|
|
|
|
333
|
|
|
|
|
|
|
Size in bytes of the file. |
334
|
|
|
|
|
|
|
|
335
|
|
|
|
|
|
|
=back |
336
|
|
|
|
|
|
|
|
337
|
|
|
|
|
|
|
A sample record: |
338
|
|
|
|
|
|
|
|
339
|
|
|
|
|
|
|
'127' => { |
340
|
|
|
|
|
|
|
'daap.songsize' => 2597221, |
341
|
|
|
|
|
|
|
'daap.songalbum' => 'Live (Disc 2)', |
342
|
|
|
|
|
|
|
'dmap.persistentid' => '4081440092921832180', |
343
|
|
|
|
|
|
|
'dmap.itemname' => 'Down To The River To Pray', |
344
|
|
|
|
|
|
|
'daap.songartist' => 'Alison Krauss + Union Station', |
345
|
|
|
|
|
|
|
'dmap.itemid' => 127, |
346
|
|
|
|
|
|
|
'daap.songformat' => 'mp3' |
347
|
|
|
|
|
|
|
}, |
348
|
|
|
|
|
|
|
|
349
|
|
|
|
|
|
|
To find out what other attributes you can request consult the DAAP |
350
|
|
|
|
|
|
|
spec at http://tapjam.net/daap/draft.html |
351
|
|
|
|
|
|
|
|
352
|
|
|
|
|
|
|
=cut |
353
|
|
|
|
|
|
|
|
354
|
|
|
|
|
|
|
sub songs { |
355
|
|
|
|
|
|
|
my $self = shift; |
356
|
|
|
|
|
|
|
|
357
|
|
|
|
|
|
|
return $self->{SONGS}; |
358
|
|
|
|
|
|
|
} |
359
|
|
|
|
|
|
|
|
360
|
|
|
|
|
|
|
=head2 * playlists() |
361
|
|
|
|
|
|
|
|
362
|
|
|
|
|
|
|
$songlist = $obj->playlists(); |
363
|
|
|
|
|
|
|
|
364
|
|
|
|
|
|
|
Returns a hash reference. Keys are playlist IDs, values are hashes |
365
|
|
|
|
|
|
|
with information on the playlist. |
366
|
|
|
|
|
|
|
|
367
|
|
|
|
|
|
|
XXX: explain keys |
368
|
|
|
|
|
|
|
|
369
|
|
|
|
|
|
|
A sample record: |
370
|
|
|
|
|
|
|
|
371
|
|
|
|
|
|
|
'2583' => { |
372
|
|
|
|
|
|
|
'dmap.itemcount' => 335, |
373
|
|
|
|
|
|
|
'dmap.persistentid' => '4609413108325671202', |
374
|
|
|
|
|
|
|
'dmap.itemname' => 'Recently Played', |
375
|
|
|
|
|
|
|
'com.apple.itunes.smart-playlist' => 0, |
376
|
|
|
|
|
|
|
'dmap.itemid' => 2583 |
377
|
|
|
|
|
|
|
} |
378
|
|
|
|
|
|
|
|
379
|
|
|
|
|
|
|
=cut |
380
|
|
|
|
|
|
|
|
381
|
|
|
|
|
|
|
sub playlists { |
382
|
|
|
|
|
|
|
my $self = shift; |
383
|
|
|
|
|
|
|
|
384
|
|
|
|
|
|
|
return $self->{PLAYLISTS}; |
385
|
|
|
|
|
|
|
} |
386
|
|
|
|
|
|
|
|
387
|
|
|
|
|
|
|
sub _get_songs { |
388
|
|
|
|
|
|
|
my ($self, $db_id) = @_; |
389
|
|
|
|
|
|
|
|
390
|
|
|
|
|
|
|
my $path = "databases/$db_id/items?type=music&meta=" . |
391
|
|
|
|
|
|
|
join ",", @{ $self->{SONG_ATTRIBUTES} }; |
392
|
|
|
|
|
|
|
my $res = $self->_do_get($path) or return; |
393
|
|
|
|
|
|
|
|
394
|
|
|
|
|
|
|
my $listing = dmap_seek(dmap_unpack($res), |
395
|
|
|
|
|
|
|
"daap.databasesongs/dmap.listing"); |
396
|
|
|
|
|
|
|
if (!$listing) { |
397
|
|
|
|
|
|
|
$self->error("no song database in response from server"); |
398
|
|
|
|
|
|
|
return; |
399
|
|
|
|
|
|
|
} |
400
|
|
|
|
|
|
|
|
401
|
|
|
|
|
|
|
my $struct = $self->_unpack_listing_to_hash($listing); |
402
|
|
|
|
|
|
|
delete @{%$struct}{ grep { $struct->{$_}{'daap.songsize'} == 0 } keys %$struct }; # remove deleted songs |
403
|
|
|
|
|
|
|
|
404
|
|
|
|
|
|
|
return $struct; |
405
|
|
|
|
|
|
|
} |
406
|
|
|
|
|
|
|
|
407
|
|
|
|
|
|
|
sub _get_playlists { |
408
|
|
|
|
|
|
|
my ($self, $db_id) = @_; |
409
|
|
|
|
|
|
|
|
410
|
|
|
|
|
|
|
my $res = $self->_do_get("databases/$db_id/containers?meta=dmap.itemid,dmap.itemname,dmap.persistentid,com.apple.itunes.smart-playlist") |
411
|
|
|
|
|
|
|
or return; |
412
|
|
|
|
|
|
|
|
413
|
|
|
|
|
|
|
my $listing = dmap_seek(dmap_unpack($res), |
414
|
|
|
|
|
|
|
"daap.databaseplaylists/dmap.listing"); |
415
|
|
|
|
|
|
|
if (!$listing) { |
416
|
|
|
|
|
|
|
$self->error("no playlist in response from server"); |
417
|
|
|
|
|
|
|
return; |
418
|
|
|
|
|
|
|
} |
419
|
|
|
|
|
|
|
|
420
|
|
|
|
|
|
|
return $self->_unpack_listing_to_hash($listing); |
421
|
|
|
|
|
|
|
} |
422
|
|
|
|
|
|
|
|
423
|
|
|
|
|
|
|
=head2 * playlist |
424
|
|
|
|
|
|
|
|
425
|
|
|
|
|
|
|
$playlist = $obj->playlist($playlist_id); |
426
|
|
|
|
|
|
|
|
427
|
|
|
|
|
|
|
A playlist ID is a key from the hash returned from the C |
428
|
|
|
|
|
|
|
method. Returns an array of song records. |
429
|
|
|
|
|
|
|
|
430
|
|
|
|
|
|
|
=cut |
431
|
|
|
|
|
|
|
|
432
|
|
|
|
|
|
|
sub playlist { |
433
|
|
|
|
|
|
|
my ($self, $playlist_id) = @_; |
434
|
|
|
|
|
|
|
|
435
|
|
|
|
|
|
|
my $db_id = $self->{DATABASE}; |
436
|
|
|
|
|
|
|
if (!$db_id) { |
437
|
|
|
|
|
|
|
$self->error("No database selected so can't fetch playlist"); |
438
|
|
|
|
|
|
|
return; |
439
|
|
|
|
|
|
|
} |
440
|
|
|
|
|
|
|
|
441
|
|
|
|
|
|
|
if (!exists $self->{PLAYLISTS}->{$playlist_id}) { |
442
|
|
|
|
|
|
|
$self->error("No such playlist $playlist_id"); |
443
|
|
|
|
|
|
|
return; |
444
|
|
|
|
|
|
|
} |
445
|
|
|
|
|
|
|
|
446
|
|
|
|
|
|
|
my $res = $self->_do_get("databases/$db_id/containers/$playlist_id/items?type=music&meta=dmap.itemkind,dmap.itemid,dmap.containeritemid") |
447
|
|
|
|
|
|
|
or return; |
448
|
|
|
|
|
|
|
|
449
|
|
|
|
|
|
|
my $listing = dmap_seek(dmap_unpack($res), |
450
|
|
|
|
|
|
|
"daap.playlistsongs/dmap.listing"); |
451
|
|
|
|
|
|
|
if (!$listing) { |
452
|
|
|
|
|
|
|
$self->error("Couldn't fetch playlist $playlist_id"); |
453
|
|
|
|
|
|
|
} |
454
|
|
|
|
|
|
|
|
455
|
|
|
|
|
|
|
my $struct = []; |
456
|
|
|
|
|
|
|
|
457
|
|
|
|
|
|
|
foreach my $item (@$listing) { |
458
|
|
|
|
|
|
|
my $record = {}; |
459
|
|
|
|
|
|
|
my $field_array_ref = $item->[1]; |
460
|
|
|
|
|
|
|
foreach my $field_pair_ref (@$field_array_ref) { |
461
|
|
|
|
|
|
|
my ($field, $value) = @$field_pair_ref; |
462
|
|
|
|
|
|
|
$record->{$field} = $value; |
463
|
|
|
|
|
|
|
} |
464
|
|
|
|
|
|
|
push @$struct, $self->{SONGS}->{ $record->{"dmap.itemid"} }; |
465
|
|
|
|
|
|
|
} |
466
|
|
|
|
|
|
|
|
467
|
|
|
|
|
|
|
return $struct; |
468
|
|
|
|
|
|
|
} |
469
|
|
|
|
|
|
|
|
470
|
|
|
|
|
|
|
sub _unpack_listing_to_hash { |
471
|
|
|
|
|
|
|
my ($self, $listing) = @_; |
472
|
|
|
|
|
|
|
|
473
|
|
|
|
|
|
|
my $struct = {}; |
474
|
|
|
|
|
|
|
|
475
|
|
|
|
|
|
|
foreach my $item (@$listing) { |
476
|
|
|
|
|
|
|
my $record = {}; |
477
|
|
|
|
|
|
|
my $field_array_ref = $item->[1]; |
478
|
|
|
|
|
|
|
foreach my $field_pair_ref (@$field_array_ref) { |
479
|
|
|
|
|
|
|
my ($field, $value) = @$field_pair_ref; |
480
|
|
|
|
|
|
|
$record->{$field} = $value; |
481
|
|
|
|
|
|
|
} |
482
|
|
|
|
|
|
|
$struct->{$record->{'dmap.itemid'}} = $record; |
483
|
|
|
|
|
|
|
} |
484
|
|
|
|
|
|
|
|
485
|
|
|
|
|
|
|
return $struct; |
486
|
|
|
|
|
|
|
} |
487
|
|
|
|
|
|
|
|
488
|
|
|
|
|
|
|
=head2 * url |
489
|
|
|
|
|
|
|
|
490
|
|
|
|
|
|
|
$url = $obj->url($song_id); |
491
|
|
|
|
|
|
|
$url = $obj->url($playlist_id); |
492
|
|
|
|
|
|
|
|
493
|
|
|
|
|
|
|
Returns the persistent URL for the track or playlist. |
494
|
|
|
|
|
|
|
|
495
|
|
|
|
|
|
|
=cut |
496
|
|
|
|
|
|
|
|
497
|
|
|
|
|
|
|
### |
498
|
|
|
|
|
|
|
### XXX: I go from Math::BigInt to |
499
|
|
|
|
|
|
|
### string to Math::BigInt again. Some of these helper methods are surely |
500
|
|
|
|
|
|
|
### not necessary? |
501
|
|
|
|
|
|
|
### |
502
|
|
|
|
|
|
|
|
503
|
|
|
|
|
|
|
sub url { |
504
|
|
|
|
|
|
|
my ($self, @arg) = @_; |
505
|
|
|
|
|
|
|
|
506
|
|
|
|
|
|
|
$self->error(""); |
507
|
|
|
|
|
|
|
|
508
|
|
|
|
|
|
|
if (!$self->{CONNECTED}) { |
509
|
|
|
|
|
|
|
$self->error("Can't fetch URL when not connected"); |
510
|
|
|
|
|
|
|
return; |
511
|
|
|
|
|
|
|
} |
512
|
|
|
|
|
|
|
|
513
|
|
|
|
|
|
|
my $song_list = $self->{SONGS}; |
514
|
|
|
|
|
|
|
my $playlists = $self->{PLAYLISTS}; |
515
|
|
|
|
|
|
|
my $db = $self->{DATABASE_LIST}{$self->{DATABASE}}{"dmap.persistentid"}; |
516
|
|
|
|
|
|
|
my @urls = (); |
517
|
|
|
|
|
|
|
my @skipped = (); |
518
|
|
|
|
|
|
|
|
519
|
|
|
|
|
|
|
foreach my $id (@arg) { |
520
|
|
|
|
|
|
|
if (exists $song_list->{$id}) { |
521
|
|
|
|
|
|
|
my $song = $song_list->{$id}; |
522
|
|
|
|
|
|
|
push @urls, $self-> |
523
|
|
|
|
|
|
|
_build_resolve_url(database => $db, |
524
|
|
|
|
|
|
|
song => $song->{"dmap.persistentid"}); |
525
|
|
|
|
|
|
|
} elsif (exists $playlists->{$id}) { |
526
|
|
|
|
|
|
|
my $playlist = $playlists->{$id}; |
527
|
|
|
|
|
|
|
push @urls, $self-> |
528
|
|
|
|
|
|
|
_build_resolve_url(database => $db, |
529
|
|
|
|
|
|
|
playlist => $playlist->{"dmap.persistentid"}); |
530
|
|
|
|
|
|
|
} else { |
531
|
|
|
|
|
|
|
push @skipped, $id; |
532
|
|
|
|
|
|
|
} |
533
|
|
|
|
|
|
|
} |
534
|
|
|
|
|
|
|
|
535
|
|
|
|
|
|
|
if (@skipped) { |
536
|
|
|
|
|
|
|
$self->error("skipped: @skipped"); |
537
|
|
|
|
|
|
|
} |
538
|
|
|
|
|
|
|
|
539
|
|
|
|
|
|
|
if (wantarray) { |
540
|
|
|
|
|
|
|
return @urls; |
541
|
|
|
|
|
|
|
} else { |
542
|
|
|
|
|
|
|
return $urls[0]; |
543
|
|
|
|
|
|
|
} |
544
|
|
|
|
|
|
|
} |
545
|
|
|
|
|
|
|
|
546
|
|
|
|
|
|
|
sub _build_resolve_url { |
547
|
|
|
|
|
|
|
my ($self, %specs) = @_; |
548
|
|
|
|
|
|
|
|
549
|
|
|
|
|
|
|
return "daap://$self->{SERVER_HOST}:$self->{SERVER_PORT}/resolve?" . |
550
|
|
|
|
|
|
|
join('&', map {my $id = $self->_persistentid_as_text($specs{$_}); |
551
|
|
|
|
|
|
|
"$_-spec='dmap.persistentid:$id'"} keys %specs); |
552
|
|
|
|
|
|
|
} |
553
|
|
|
|
|
|
|
|
554
|
|
|
|
|
|
|
sub _persistentid_as_text { |
555
|
|
|
|
|
|
|
my ($self, $id) = @_; |
556
|
|
|
|
|
|
|
|
557
|
|
|
|
|
|
|
$id = new Math::BigInt($id); |
558
|
|
|
|
|
|
|
|
559
|
|
|
|
|
|
|
return sprintf("0x%08x%08x", $id->brsft(32), $id->band(0xffffffff)); |
560
|
|
|
|
|
|
|
} |
561
|
|
|
|
|
|
|
|
562
|
|
|
|
|
|
|
|
563
|
|
|
|
|
|
|
=head2 * get |
564
|
|
|
|
|
|
|
|
565
|
|
|
|
|
|
|
@tracks = $obj->get(@song_ids); |
566
|
|
|
|
|
|
|
|
567
|
|
|
|
|
|
|
Returns the binary data of the song. A song ID is a key from |
568
|
|
|
|
|
|
|
the hash returned by C, or the C from one of |
569
|
|
|
|
|
|
|
the elements in the array returned by C. |
570
|
|
|
|
|
|
|
|
571
|
|
|
|
|
|
|
=cut |
572
|
|
|
|
|
|
|
|
573
|
|
|
|
|
|
|
sub get { |
574
|
|
|
|
|
|
|
my ($self, @arg) = @_; |
575
|
|
|
|
|
|
|
$self->_download_songs(undef, @arg); |
576
|
|
|
|
|
|
|
} |
577
|
|
|
|
|
|
|
|
578
|
|
|
|
|
|
|
sub _download_songs { |
579
|
|
|
|
|
|
|
my ($self, $dir, @arg) = @_; |
580
|
|
|
|
|
|
|
my $song_list = $self->{SONGS}; |
581
|
|
|
|
|
|
|
my @songs; |
582
|
|
|
|
|
|
|
my @skipped; |
583
|
|
|
|
|
|
|
|
584
|
|
|
|
|
|
|
foreach my $song_id (@arg) { |
585
|
|
|
|
|
|
|
my $song = $song_list->{$song_id}; |
586
|
|
|
|
|
|
|
|
587
|
|
|
|
|
|
|
if (!defined $song) { # ok to blur defined() and exists() here |
588
|
|
|
|
|
|
|
push @skipped, $song_id; |
589
|
|
|
|
|
|
|
next; |
590
|
|
|
|
|
|
|
} |
591
|
|
|
|
|
|
|
my $response = $self->_get_song($self->{DATABASE}, $song, $dir); |
592
|
|
|
|
|
|
|
if (!$response) { |
593
|
|
|
|
|
|
|
push @skipped, $song_id; |
594
|
|
|
|
|
|
|
} else { |
595
|
|
|
|
|
|
|
push @songs, $dir ? $song_id : $response; |
596
|
|
|
|
|
|
|
} |
597
|
|
|
|
|
|
|
} |
598
|
|
|
|
|
|
|
|
599
|
|
|
|
|
|
|
if (@skipped) { |
600
|
|
|
|
|
|
|
$self->error("skipped: @skipped"); |
601
|
|
|
|
|
|
|
} |
602
|
|
|
|
|
|
|
if (wantarray) { |
603
|
|
|
|
|
|
|
return @songs; |
604
|
|
|
|
|
|
|
} else { |
605
|
|
|
|
|
|
|
return $songs[0]; |
606
|
|
|
|
|
|
|
} |
607
|
|
|
|
|
|
|
} |
608
|
|
|
|
|
|
|
|
609
|
|
|
|
|
|
|
sub _get_song { |
610
|
|
|
|
|
|
|
my ($self, $db_id, $song, $dir) = @_; |
611
|
|
|
|
|
|
|
my ($song_id, $format) = |
612
|
|
|
|
|
|
|
($song->{"dmap.itemid"}, $song->{"daap.songformat"}); |
613
|
|
|
|
|
|
|
my $filename = "$song_id.$format"; |
614
|
|
|
|
|
|
|
|
615
|
|
|
|
|
|
|
++$self->{REQUEST_ID}; |
616
|
|
|
|
|
|
|
|
617
|
|
|
|
|
|
|
if ($dir) { |
618
|
|
|
|
|
|
|
return $self->_do_get("databases/$db_id/items/$filename", |
619
|
|
|
|
|
|
|
"$dir/$filename"); |
620
|
|
|
|
|
|
|
} else { |
621
|
|
|
|
|
|
|
return $self->_do_get("databases/$db_id/items/$filename"); |
622
|
|
|
|
|
|
|
} |
623
|
|
|
|
|
|
|
} |
624
|
|
|
|
|
|
|
|
625
|
|
|
|
|
|
|
=head2 * save |
626
|
|
|
|
|
|
|
|
627
|
|
|
|
|
|
|
$tracks_saved = $obj->save($dir, @song_ids); |
628
|
|
|
|
|
|
|
|
629
|
|
|
|
|
|
|
Saves the binary data of the song to the directory. Returns the |
630
|
|
|
|
|
|
|
number of songs saved. |
631
|
|
|
|
|
|
|
|
632
|
|
|
|
|
|
|
=cut |
633
|
|
|
|
|
|
|
|
634
|
|
|
|
|
|
|
sub save { |
635
|
|
|
|
|
|
|
my ($self, @arg) = @_; |
636
|
|
|
|
|
|
|
$self->_download_songs(@arg); |
637
|
|
|
|
|
|
|
} |
638
|
|
|
|
|
|
|
|
639
|
|
|
|
|
|
|
=head2 * disconnect() |
640
|
|
|
|
|
|
|
|
641
|
|
|
|
|
|
|
$obj->disconnect; |
642
|
|
|
|
|
|
|
|
643
|
|
|
|
|
|
|
Logs out of the database. Returns C if an error occurred, a |
644
|
|
|
|
|
|
|
true value otherwise. If an error does occur, there's probably not |
645
|
|
|
|
|
|
|
much you can do about it. |
646
|
|
|
|
|
|
|
|
647
|
|
|
|
|
|
|
=cut |
648
|
|
|
|
|
|
|
|
649
|
|
|
|
|
|
|
sub disconnect { |
650
|
|
|
|
|
|
|
my $self = shift; |
651
|
|
|
|
|
|
|
|
652
|
|
|
|
|
|
|
$self->error(""); |
653
|
|
|
|
|
|
|
if ($self->{CONNECTED}) { |
654
|
|
|
|
|
|
|
(undef) = $self->_do_get("logout"); |
655
|
|
|
|
|
|
|
} |
656
|
|
|
|
|
|
|
undef $self->{CONNECTED}; |
657
|
|
|
|
|
|
|
return $self->error; |
658
|
|
|
|
|
|
|
} |
659
|
|
|
|
|
|
|
|
660
|
|
|
|
|
|
|
sub DESTROY { |
661
|
|
|
|
|
|
|
my $self = shift; |
662
|
|
|
|
|
|
|
$self->_debug("Destroying $self->{ID} to $self->{SERVER_HOST}"); |
663
|
|
|
|
|
|
|
$self->disconnect; |
664
|
|
|
|
|
|
|
} |
665
|
|
|
|
|
|
|
|
666
|
|
|
|
|
|
|
=head2 * error() |
667
|
|
|
|
|
|
|
|
668
|
|
|
|
|
|
|
$string = $obj->error; |
669
|
|
|
|
|
|
|
|
670
|
|
|
|
|
|
|
Returns the most recent error code. Empty string if no error occurred. |
671
|
|
|
|
|
|
|
|
672
|
|
|
|
|
|
|
=cut |
673
|
|
|
|
|
|
|
|
674
|
|
|
|
|
|
|
sub error { |
675
|
|
|
|
|
|
|
my $self = shift; |
676
|
|
|
|
|
|
|
if ($self->{DEBUG} and defined($_[0]) and length($_[0])) { |
677
|
|
|
|
|
|
|
warn "Setting error to $_[0]\n"; |
678
|
|
|
|
|
|
|
} |
679
|
|
|
|
|
|
|
if (@_) { $self->{ERROR} = shift } else { $self->{ERROR} } |
680
|
|
|
|
|
|
|
} |
681
|
|
|
|
|
|
|
|
682
|
|
|
|
|
|
|
sub _devine_validator { |
683
|
|
|
|
|
|
|
my $self = shift; |
684
|
|
|
|
|
|
|
$self->{VALIDATOR} = undef; |
685
|
|
|
|
|
|
|
$self->{M4p_evil} = 0; |
686
|
|
|
|
|
|
|
|
687
|
|
|
|
|
|
|
my $response = $self->{UA}->get( $self->_server_url.'/server-info' ); |
688
|
|
|
|
|
|
|
my $server = $response->header('DAAP-Server'); |
689
|
|
|
|
|
|
|
|
690
|
|
|
|
|
|
|
if ($server =~ m{^iTunes/4.2 }) { |
691
|
|
|
|
|
|
|
$self->{VALIDATOR} = __PACKAGE__."::v2"; |
692
|
|
|
|
|
|
|
return; |
693
|
|
|
|
|
|
|
} |
694
|
|
|
|
|
|
|
|
695
|
|
|
|
|
|
|
if ($server =~ m{^iTunes/}) { |
696
|
|
|
|
|
|
|
$self->{M4p_evil} = 1; |
697
|
|
|
|
|
|
|
$self->{VALIDATOR} = __PACKAGE__."::v3" |
698
|
|
|
|
|
|
|
} |
699
|
|
|
|
|
|
|
} |
700
|
|
|
|
|
|
|
|
701
|
|
|
|
|
|
|
|
702
|
|
|
|
|
|
|
sub _validation_cookie { |
703
|
|
|
|
|
|
|
my $self = shift; |
704
|
|
|
|
|
|
|
return unless $self->{VALIDATOR}; |
705
|
|
|
|
|
|
|
return ( "Client-DAAP-Validation" => $self->{VALIDATOR}->validate( @_ ) ); |
706
|
|
|
|
|
|
|
} |
707
|
|
|
|
|
|
|
|
708
|
|
|
|
|
|
|
sub _server_url { |
709
|
|
|
|
|
|
|
my $self = shift; |
710
|
|
|
|
|
|
|
sprintf("http://%s:%d", $self->{SERVER_HOST}, $self->{SERVER_PORT}); |
711
|
|
|
|
|
|
|
} |
712
|
|
|
|
|
|
|
|
713
|
|
|
|
|
|
|
# quite the fugly hack |
714
|
|
|
|
|
|
|
my @credentials; |
715
|
|
|
|
|
|
|
{ |
716
|
|
|
|
|
|
|
package Net::DAAP::Client::UA; |
717
|
|
|
|
|
|
|
use base qw( LWP::UserAgent ); |
718
|
|
|
|
|
|
|
sub get_basic_credentials { return @credentials } |
719
|
|
|
|
|
|
|
|
720
|
|
|
|
|
|
|
} |
721
|
|
|
|
|
|
|
|
722
|
|
|
|
|
|
|
sub _do_get { |
723
|
|
|
|
|
|
|
my ($self, $req, $file) = @_; |
724
|
|
|
|
|
|
|
if (!defined wantarray) { carp "_do_get's result is being ignored" } |
725
|
|
|
|
|
|
|
|
726
|
|
|
|
|
|
|
my $id = $self->{ID}; |
727
|
|
|
|
|
|
|
my $revision = $self->{REVISION}; |
728
|
|
|
|
|
|
|
my $ua = $self->{UA}; |
729
|
|
|
|
|
|
|
|
730
|
|
|
|
|
|
|
my $url = $self->_server_url . "/$req"; |
731
|
|
|
|
|
|
|
my $res; |
732
|
|
|
|
|
|
|
|
733
|
|
|
|
|
|
|
# append session-id and revision-number query args automatically |
734
|
|
|
|
|
|
|
if ($self->{ID}) { |
735
|
|
|
|
|
|
|
$url .= $req =~ m{ \? }x ? "&" : "?"; |
736
|
|
|
|
|
|
|
$url .= "session-id=$id"; |
737
|
|
|
|
|
|
|
} |
738
|
|
|
|
|
|
|
|
739
|
|
|
|
|
|
|
if ($revision && $req ne 'logout') { |
740
|
|
|
|
|
|
|
$url .= "&revision-number=$revision"; |
741
|
|
|
|
|
|
|
} |
742
|
|
|
|
|
|
|
|
743
|
|
|
|
|
|
|
# fetch into memory or save to disk as needed |
744
|
|
|
|
|
|
|
|
745
|
|
|
|
|
|
|
$self->_debug($url); |
746
|
|
|
|
|
|
|
|
747
|
|
|
|
|
|
|
# form the request ourself so we have magic headers. |
748
|
|
|
|
|
|
|
my $path = $url; |
749
|
|
|
|
|
|
|
$path =~ s{http://.*?/}{/}; |
750
|
|
|
|
|
|
|
|
751
|
|
|
|
|
|
|
my $reqid = $self->{REQUEST_ID}; |
752
|
|
|
|
|
|
|
my $request = HTTP::Request::Common::GET( |
753
|
|
|
|
|
|
|
$url, |
754
|
|
|
|
|
|
|
"Client-DAAP-Version" => '3.0', |
755
|
|
|
|
|
|
|
"Client-DAAP-Access-Index" => 2, |
756
|
|
|
|
|
|
|
$reqid ? ( "Client-DAAP-Request-ID" => $reqid ) : (), |
757
|
|
|
|
|
|
|
$self->_validation_cookie( $path, 2, $reqid ), |
758
|
|
|
|
|
|
|
); |
759
|
|
|
|
|
|
|
|
760
|
|
|
|
|
|
|
#print ">>>>\n", $request->as_string, ">>>>>\n"; |
761
|
|
|
|
|
|
|
|
762
|
|
|
|
|
|
|
# It would seem that 4.{5,6} are using their internal MD5/M4p for |
763
|
|
|
|
|
|
|
# their digest auth, or some other form of evil, certainly the |
764
|
|
|
|
|
|
|
# regular Digest auth that works with 4.2 gets refused. |
765
|
|
|
|
|
|
|
|
766
|
|
|
|
|
|
|
#local *Digest::MD5::new = sub { shift; Digest::MD5::M4p->new( @_ ) } |
767
|
|
|
|
|
|
|
# if $self->{M4p_evil}; |
768
|
|
|
|
|
|
|
|
769
|
|
|
|
|
|
|
@credentials = $self->{PASSWORD} ? ('iTunes_4.6', $self->{PASSWORD}) : (); |
770
|
|
|
|
|
|
|
|
771
|
|
|
|
|
|
|
if ($file) { |
772
|
|
|
|
|
|
|
$res = $ua->request($request, $file); |
773
|
|
|
|
|
|
|
} else { |
774
|
|
|
|
|
|
|
$res = $ua->request($request); |
775
|
|
|
|
|
|
|
} |
776
|
|
|
|
|
|
|
# complain if the server sent back the wrong response |
777
|
|
|
|
|
|
|
unless ($res->is_success) { |
778
|
|
|
|
|
|
|
$self->error("$url\n" . $res->as_string); |
779
|
|
|
|
|
|
|
return; |
780
|
|
|
|
|
|
|
} |
781
|
|
|
|
|
|
|
|
782
|
|
|
|
|
|
|
my $content_type = $res->header("Content-Type"); |
783
|
|
|
|
|
|
|
if ($req !~ m{(?:/items/\d+\.|logout)} && $content_type !~ /dmap/) { |
784
|
|
|
|
|
|
|
$self->error("Broken response (content type $content_type) on $url"); |
785
|
|
|
|
|
|
|
return; |
786
|
|
|
|
|
|
|
} |
787
|
|
|
|
|
|
|
|
788
|
|
|
|
|
|
|
if ($file) { |
789
|
|
|
|
|
|
|
return $res; # return obj to avoid copying huge string |
790
|
|
|
|
|
|
|
} else { |
791
|
|
|
|
|
|
|
return $res->content; |
792
|
|
|
|
|
|
|
} |
793
|
|
|
|
|
|
|
} |
794
|
|
|
|
|
|
|
|
795
|
|
|
|
|
|
|
1; |
796
|
|
|
|
|
|
|
|
797
|
|
|
|
|
|
|
__END__ |