1###__PERLBIN__### 2# Copyright (C) 2002-2007 Adrian Ulrich <pab at blinkenlights.ch> 3# Part of the gnupod-tools collection 4# 5# URL: http://www.gnu.org/software/gnupod/ 6# 7# GNUpod is free software; you can redistribute it and/or modify 8# it under the terms of the GNU General Public License as published by 9# the Free Software Foundation; either version 3 of the License, or 10# (at your option) any later version. 11# 12# GNUpod is distributed in the hope that it will be useful, 13# but WITHOUT ANY WARRANTY; without even the implied warranty of 14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15# GNU General Public License for more details. 16# 17# You should have received a copy of the GNU General Public License 18# along with this program. If not, see <http://www.gnu.org/licenses/>.# 19# 20# iTunes and iPod are trademarks of Apple 21# 22# This product is not supported/written/published by Apple! 23 24use strict; 25use GNUpod::XMLhelper; 26use GNUpod::FooBar; 27use GNUpod::ArtworkDB; 28use Getopt::Long; 29 30use vars qw(%opts @keeplist %rename_tags); 31 32use constant DEFAULT_SPACE => 32; 33 34my $dbid = undef; # Artwork DB-ID 35my $dirty = 0; # Do we need to re-write the XML version? 36 37$opts{mount} = $ENV{IPOD_MOUNTPOINT}; 38 39 40 41print "gnupod_search.pl Version ###__VERSION__### (C) Adrian Ulrich\n"; 42 43# WARNING: If you add new options wich don't do matching, change newfile() 44# 45GetOptions(\%opts, "version", "help|h", "mount|m=s", "artist|a=s", 46 "album|l=s", "title|t=s", "id|i=s", "rename=s@", "artwork=s", 47 "playcount|c=s", "rating|s=s", "podcastrss|R=s", "podcastguid|U=s", 48 "bitrate|b=s", 49 "view=s","genre|g=s", "match-once|o", "delete"); 50GNUpod::FooBar::GetConfig(\%opts, {view=>'s', mount=>'s', 'match-once'=>'b', 'automktunes'=>'b', model=>'s'}, "gnupod_search"); 51 52$opts{view} ||= 'ialt'; #Default view 53 54usage() if $opts{help}; 55version() if $opts{version}; 56#Check if input makes sense: 57die "You can't use --delete and --rename together\n" if($opts{delete} && $opts{rename}); 58 59# -> Connect the iPod 60my $connection = GNUpod::FooBar::connect(\%opts); 61usage($connection->{status}."\n") if $connection->{status}; 62 63my $AWDB = GNUpod::ArtworkDB->new(Connection=>$connection, DropUnseen=>1); 64 65main($connection); 66 67#################################################### 68# Worker 69sub main { 70 my($con) = @_; 71 72 #Build %rename_tags 73 foreach(@{$opts{rename}}) { 74 my($key,$val) = split(/=/,$_,2); 75 next unless $key && defined($val); 76 #$key =~ s/^\s*-+//g; # -- is not valid for xml tags! 77 next if lc($key) eq "id";#Dont allow something like THIS 78 $rename_tags{lc($key)} = $val; 79 } 80 81 if($opts{artwork}) { 82 if( $AWDB->PrepareImage(File=>$opts{artwork}, Model=>$opts{model}) ) { 83 $AWDB->LoadArtworkDb or die "Failed to load artwork database\n"; 84 } 85 else { 86 warn "$0: Could not load $opts{artwork}, skipping artwork\n"; 87 delete($opts{artwork}); 88 } 89 } 90 91 pview(undef,1); 92 GNUpod::XMLhelper::doxml($con->{xml}) or usage("Failed to parse $con->{xml}, did you run gnupod_INIT.pl?\n"); 93 #XML::Parser finished, write new file if we deleted or renamed 94 if($dirty) { 95 GNUpod::XMLhelper::writexml($con,{automktunes=>$opts{automktunes}}); 96 } 97 98 $AWDB->WriteArtworkDb; 99} 100 101############################################# 102# Eventhandler for FILE items 103sub newfile { 104 my($el) = @_; 105 # 2 = mount + view (both are ALWAYS set) 106 my $ntm = keys(%opts)-2-$opts{'match-once'}-$opts{automktunes}-$opts{delete}-(defined $opts{rename})-(defined $opts{artwork})-(defined $opts{model}); 107 my $matched = undef; 108 my $dounlink = 0; 109 foreach my $opx (keys(%opts)) { 110 next if $opx =~ /mount|match-once|delete|view|rename/; #Skip this 111 112 113 if(substr($opts{$opx},0,1) eq ">") { 114 $matched++ if int($el->{file}->{$opx}) > int(substr($opts{$opx},1)); 115 } 116 elsif(substr($opts{$opx},0,1) eq "<") { 117 $matched++ if int($el->{file}->{$opx}) < int(substr($opts{$opx},1)); 118 } 119 elsif(substr($opts{$opx},0,1) eq "-") { 120 my($s_from, $s_to) = substr($opts{$opx},1) =~ /^(\d+)-(\d+)$/; 121 if( (int($el->{file}->{$opx}) >= $s_from) && (int($el->{file}->{$opx}) <= $s_to) ) { 122 $matched++; 123 } 124 } 125 elsif($el->{file}->{$opx} =~ /$opts{$opx}/i) { 126 $matched++; 127 } 128 } 129 130 131 if(($opts{'match-once'} && $matched) || $ntm == $matched) { 132 # => HIT 133 134 # -> Rename tags 135 foreach(keys(%rename_tags)) { 136 $el->{file}->{$_} = $rename_tags{$_}; 137 $dirty++; 138 } 139 # -> Print output 140 pview($el->{file},undef,$opts{delete}); 141 142 if($opts{delete}) { 143 $dounlink = 1; # Request deletion 144 } 145 elsif(defined($opts{artwork})) { 146 # -> Add/Set artwork 147 $el->{file}->{has_artwork} = 1; 148 $el->{file}->{artworkcnt} = 1; 149 $el->{file}->{dbid_1} = $AWDB->InjectImage; 150 $dirty++; 151 } 152 } 153 154 if($dounlink) { 155 # -> Remove file as requested 156 unlink(GNUpod::XMLhelper::realpath($opts{mount},$el->{file}->{path})) or warn "[!!] Remove failed: $!\n"; 157 $dirty++; 158 } 159 else { 160 # -> Keep file: add it to XML 161 GNUpod::XMLhelper::mkfile($el); 162 # -> and keep artwork 163 $AWDB->KeepImage($el->{file}->{dbid_1}); 164 # -> and playlists 165 $keeplist[$el->{file}->{id}] = 1; 166 } 167} 168 169############################################ 170# Eventhandler for PLAYLIST items 171sub newpl { 172 # Delete or rename needs to rebuild the XML file 173 my ($el, $name, $plt) = @_; 174 if(($plt eq "pl" or $plt eq "pcpl") && ref($el->{add}) eq "HASH") { #Add action 175 if(defined($el->{add}->{id}) && int(keys(%{$el->{add}})) == 1) { #Only id 176 return unless($keeplist[$el->{add}->{id}]); #ID not on keeplist. drop it 177 } 178 } 179 elsif($plt eq "spl" && ref($el->{splcont}) eq "HASH") { #spl content 180 if(defined($el->{splcont}->{id}) && int(keys(%{$el->{splcont}})) == 1) { #Only one item 181 return unless($keeplist[$el->{splcont}->{id}]); 182 } 183 } 184 GNUpod::XMLhelper::mkfile($el,{$plt."name"=>$name}); 185} 186 187 188############################################################## 189# Printout Search output 190sub pview { 191 my($orf,$xhead, $xdelete) = @_; 192 193 #Build refs 194 my %qh = (); 195 $qh{n}{k} = $orf->{songnum}; $qh{n}{w} = 4; $qh{n}{n} = "SNUM"; 196 $qh{t}{k} = $orf->{title}; $qh{t}{s} = "TITLE"; 197 $qh{a}{k} = $orf->{artist}; $qh{a}{s} = "ARTIST"; 198 $qh{r}{k} = $orf->{rating}; $qh{r}{w} = 4; $qh{r}{s} = "RTNG"; 199 $qh{p}{k} = $orf->{path}; $qh{p}{w} = 96; $qh{p}{s} = "PATH"; 200 $qh{l}{k} = $orf->{album}; $qh{l}{s} = "ALBUM"; 201 $qh{g}{k} = $orf->{genre}; $qh{g}{s} = "GENRE"; 202 $qh{R}{k} = $orf->{podcastrss}; $qh{R}{s} = "RSS"; 203 $qh{G}{k} = $orf->{podcastguid}; $qh{G}{s} = "GUID"; 204 $qh{c}{k} = $orf->{playcount}; $qh{c}{w} = 4; $qh{c}{s} = "CNT"; 205 $qh{i}{k} = $orf->{id}; $qh{i}{w} = 4; $qh{i}{s} = "ID"; 206 $qh{d}{k} = $orf->{dbid_1}; $qh{d}{w} = 16; $qh{d}{s} = "DBID"; 207 $qh{b}{k} = $orf->{bitrate}; $qh{b}{w} = 8; $qh{b}{s} = "BITRATE"; 208 $qh{u}{k} = GNUpod::XMLhelper::realpath($opts{mount},$orf->{path}); $qh{u}{w} = 96; $qh{u}{s} = "UNIXPATH"; 209 210 #Prepare view 211 212 my $ll = 0; #LineLength 213 foreach(split(//,$opts{view})) { 214 print "|" if $ll; 215 my $cs = $qh{$_}{k}; #CurrentString 216 $cs = $qh{$_}{s} if $xhead; #Replace it if HEAD is needed 217 218 my $cl = $qh{$_}{w}||DEFAULT_SPACE; #Current length 219 $ll += $cl+1; #Incrase LineLength 220 printf("%-*s",$cl,$cs); 221 } 222 223 if($xdelete && !$xhead) { 224 print " [RM]\n"; 225 } 226 elsif($xhead) { 227 print "\n"; 228 print "=" x $ll; 229 print "\n"; 230 } 231 else { 232 print "\n"; 233 } 234 235} 236 237 238############################################################### 239# Basic help 240sub usage { 241my($rtxt) = @_; 242die << "EOF"; 243$rtxt 244Usage: gnupod_search.pl [-h] [-m directory] File1 File2 ... 245 246 -h, --help display this help and exit 247 --version output version information and exit 248 -m, --mount=directory iPod mountpoint, default is \$IPOD_MOUNTPOINT 249 -t, --title=TITLE search songs by Title 250 -a, --artist=ARTIST search songs by Artist 251 -l, --album=ALBUM search songs by Album 252 -i, --id=ID search songs by ID 253 -g, --genre=GENRE search songs by Genre 254 -c, --playcount=COUNT search songs by Playcount 255 -s, --rating=COUNT search songs by Rating (20 is one star, 40 two, etc.) 256 -R, --podcastrss=RSS search songs by RSS 257 -U, --podcastguid=GUID search songs by GUID 258 -b, --bitrate=BITRATE search songs by Bitrate 259 -o, --match-once Search doesn't need to match multiple times (eg. -a & -l) 260 --delete REMOVE (!) matched songs 261 --view=ialt Modify output, default=ialt 262 t = title a = artist r = rating p = iPod Path 263 l = album g = genre c = playcount i = id 264 u = UnixPath n = Songnum G = podcastguid R = podcastrss 265 d = dbid 266 --rename=KEY=VAL Change tags on found songs. Example: --rename="ARTIST=Foo Bar" 267 --artwork=FILE Set FILE as Cover for found files, do not forget to run mktunes.pl 268 269Note: * Argument for title/artist/album/etc has to be UTF8 encoded, *not* latin1! 270 * Use '>3' to search all values above 3, use '<3' to search for values below 3 271 * Use '-10-30' to search all values between (and including) 10 to 30. 272 * Everything else is handled as regular expressions! If you want to search for 273 eg. ID '3' (excluding 13,63,32..), you would have to write: --id="^3\$" 274 275Report bugs to <bug-gnupod\@nongnu.org> 276EOF 277} 278 279 280sub version { 281die << "EOF"; 282gnupod_search.pl (gnupod) ###__VERSION__### 283Copyright (C) Adrian Ulrich 2002-2008 284 285This is free software; see the source for copying conditions. There is NO 286warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 287 288EOF 289} 290 291