1#!/usr/bin/perl -w
2# itshell--itunes shell
3
4=head1 NAME
5
6  itshell - a shell for searching and downloading from an itunes server
7
8=head1 SYNOPSIS
9
10  itshell [servername [serverport]]
11
12=head1 DESCRIPTION
13
14This program connects to an iTunes server and lets you search the
15playlists and songs and download the music files to your local
16filesystem.
17
18The following commands are valid from within the shell:
19
20  db                   view possible databases
21  db id                select a database
22  playlist             view possible playlists
23  playlist id          select a playlist
24  find keyword         search song title/album/artist for keyword
25  dir                  show where files will be saved
26  dir /new/dir         set new location for files to be saved
27  findget keyword      search for and immediately get title/album/artist
28  get id ...           download a song
29  url id ...           view persistent URL for a song or playlist
30  dump filename        dump databases to the filename
31  quit                 leave this shell
32
33=head1 AUTHOR
34
35Nathan Torkington.  Send patches to C<< <dapple AT torkington.com >>.
36Mail C<< daap-dev-subscribe AT develooper.com >> to join the DAAP
37developers mailing list.
38
39=cut
40
41use Net::DAAP::Client;
42
43# standard modules
44
45use sigtrap;
46use Carp;
47use strict;
48use Text::ParseWords;
49use Data::Dumper;
50use Term::ReadLine;
51
52my $Server_host = shift || 'localhost';
53my $Server_port = shift || 3689;
54my $Password    = shift || '';
55
56my $daap = Net::DAAP::Client->new(SERVER_HOST => $Server_host,
57                                  SERVER_PORT => $Server_port,
58                                  PASSWORD    => $Password,
59                                  DEBUG       => 1);
60push @{ $daap->{SONG_ATTRIBUTES} },
61  qw( daap.songbitrate daap.songsamplerate
62      daap.songstarttime daap.songstoptime
63      daap.songtime );
64$daap->connect() or die "Can't connect: ".$daap->error();
65
66
67my $dbs = $daap->databases;
68
69my $term = new Term::ReadLine qw(iTunesHell);
70my $line;
71my $prompt = 'iTunes';
72my $save_dir = '.';
73$| = 1;
74
75while (defined ($line = $term->readline("$prompt>"))) {
76    my ($cmd, @arg) = parse_line(qr{\s+}, 0, $line);
77    if ($cmd eq 'db') {
78        db_cmd(@arg);
79    } elsif ($cmd eq 'playlist') {
80        playlist_cmd(@arg);
81    } elsif ($cmd eq 'find') {
82        find_cmd(@arg);
83    } elsif ($cmd eq 'findget') {
84        findget_cmd(@arg);
85    } elsif ($cmd eq 'get') {
86        get_cmd(@arg);
87    } elsif ($cmd eq 'url') {
88        url_cmd(@arg);
89    } elsif ($cmd eq 'dir') {
90        dir_cmd(@arg);
91    } elsif ($cmd eq 'dump') {
92        dump_cmd(@arg);
93    } elsif (($cmd eq 'quit') || ($cmd eq 'exit')) {
94        last;
95    } else {
96        warn <<EOF ;
97Commands:
98  db                   view possible databases
99  db id                select a database
100  playlist             view possible playlists
101  playlist id          select a playlist
102  find keyword         search song title/album/artist for keyword
103  dir                  show where files will be saved
104  dir /new/dir         set new location for files to be saved
105  findget keyword      search for and immediately get title/album/artist
106  get id ...           download a song
107  url id ...           view persistent URL for a song or playlist
108  dump filename        dump databases to the filename
109  quit                 leave this shell
110EOF
111    }
112}
113
114$daap->disconnect();
115
116sub song_as_text {
117    my $song = shift;
118    return sprintf("%d : %s, %s (%s)\n",
119                   $song->{"dmap.itemid"},
120                   $song->{"dmap.itemname"},
121                   $song->{"daap.songartist"},
122                   $song->{"daap.songalbum"});
123}
124
125sub dir_cmd {
126    my @arg = @_;
127
128    if (@_) {
129        if (! -d $arg[0]) {
130            print "$arg[0] isn't a directory!\n";
131        } else {
132            $save_dir = $arg[0];
133        }
134    } else {
135        print "Files will be saved to $save_dir\n";
136    }
137}
138
139sub dump_cmd {
140    my @arg = @_;
141    if (@arg) {
142        my $filename = shift @arg;
143        open my $fh, ">$filename" or warn("Can't open $filename: $!"),return;
144        print $fh Dumper($dbs);
145        if ($daap->songs) {
146            print $fh "\n\n";
147            print $fh Dumper($daap->songs);
148        }
149        close $fh;
150    } else {
151        warn("usage: dump filename\n");
152    }
153}
154
155sub db_cmd {
156    my @arg = @_;
157    if (@arg) {
158        my $db_id = $arg[0];
159        $daap->db($db_id);
160        if (! $daap->error) {
161            printf "Loading database %s (may take a moment)\n", $daap->databases->{$db_id}{'dmap.itemname'};
162        } else {
163            warn "Database ID $arg[0] not found\n";
164        }
165    } else {
166        my $dbs = $daap->databases;
167        foreach my $id (sort { $a <=> $b } keys %$dbs) {
168            printf("%d : %s\n", $id, $dbs->{$id}{"dmap.itemname"});
169        }
170    }
171}
172
173sub playlist_cmd {
174    my @arg = @_;
175
176    if (! $daap->db) {
177        warn "Select a database with db first\n";
178        return;
179    }
180
181    if (@arg) {
182        my $playlist_id = $arg[0];
183        my $songs = $daap->playlist($arg[0]);
184        if (! $daap->error) {
185            foreach my $playlist_song (@$songs) {
186                my $songs = $daap->songs;
187                my $song = $daap->songs->{$playlist_song->{"dmap.itemid"}};
188                if (defined($song)) {
189                    # deleted items are evidentally left on the main playlist
190                    print song_as_text($song);
191                }
192            }
193        } else {
194            warn "Playlist ID $arg[0] not found\n";
195        }
196    } else {
197        my $playlists = $daap->playlists;
198        foreach my $id (sort { $a <=> $b } keys %$playlists) {
199            printf("%d : %s\n", $id, $playlists->{$id}{"dmap.itemname"});
200        }
201    }
202}
203
204sub findget_cmd {
205    my @arg = @_;
206
207    if (! $daap->db) {
208        warn "Select a database with db first\n";
209        return;
210    }
211
212    my @songs = search_through_songs(@arg);
213    foreach my $song (@songs) {
214        print "$save_dir/$song->{'dmap.itemid'}.$song->{'daap.songformat'} ($song->{'dmap.itemname'}) ... ";
215        $daap->save($save_dir, $song->{'dmap.itemid'});
216        print $daap->error ? "failed" : "done";
217        print "\n";
218    }
219}
220
221sub search_through_songs {
222    my $word = shift;
223    my @hits = ();
224
225    foreach my $song (values %{$daap->songs}) {
226        my (@f) = map { lc } (
227                              $song->{"dmap.itemname"},
228                              $song->{"daap.songartist"},
229                              $song->{"daap.songalbum"}
230                              );
231        my $to_find = lc($word);
232        if (grep { index(lc($_), $to_find) != -1 } @f) {
233            push @hits, $song;
234        }
235    }
236
237    return @hits;
238}
239
240sub find_cmd {
241    my @arg = @_;
242
243    if (! $daap->db) {
244        warn "Select a database with db first\n";
245        return;
246    }
247
248    if (@arg) {
249        my @songs = search_through_songs(@arg);
250        foreach my $song (@songs) {
251            print song_as_text($song);
252        }
253    } else {
254        warn "usage: find [string]\n";
255    }
256}
257
258sub get_cmd {
259    my @arg = @_;
260    my $songs = $daap->songs;
261
262    foreach my $song_id (@arg) {
263        my $song = $songs->{$song_id};
264        if (defined $song) {
265            print "Fetching ", song_as_text($song);
266            $daap->save($save_dir, $song_id);
267            if ($daap->error) {
268                print "Failed: ", $daap->error, "\n";
269            }
270        } else {
271            print "Skipping bogus song number $song_id\n";
272        }
273    }
274}
275
276sub url_cmd {
277  my @arg = @_;
278  my @skipped;
279
280  foreach my $id (@arg) {
281    my $url = $daap->url($id);
282      if ($url) {
283        print "$url\n";
284      } else {
285        push @skipped, $url;
286      }
287  }
288
289  if (@skipped) {
290    print "Skipped: ", join(", ", @skipped), ".\n";
291  }
292}
293
294exit;
295
296