1 #!/usr/local/bin/perl -w
  2 # author: seth
  3 # e-mail: for e-mail-address see http://www.wg-karlsruhe.de/seth/email_address.php 
  4 # description: modifies id3-tags, e.g. using filename
  5 
  6 use MP3::Tag;
  7 use Cwd;
  8 use strict;
  9 
 10 sub syntaxCheck{
 11   my @params=@_;
 12   my @path_splitted=split(/[\/\\]/, reverse($0));
 13   my $prg_name=reverse($path_splitted[0]);
 14   my $version='0.51.20060820';
 15   my $usage='writes id3-tags using filename (recursively)
 16 
 17 usage: '.$prg_name.' filesRE [options]
 18 
 19   filesRE                files to work on (use regular expressions!)
 20   -r,   --recursively    search subdirectories recursively
 21         --removeAllTags  removes all tags only (forced info will be ignored)
 22         --1to2           just copy id3v1 to id3v2
 23         --2to1           just copy id3v2 to id3v1
 24   -u    --update         don\'t overwrite existing id3-tags
 25   -l    --list_id3       list id3-tags only (similar to --test --v=3, but less output)
 26         --v[erbose]=x    verbose (x=0: no output, x=1: default output, x=2: much output, x=3: too much output)
 27   -t,   --test           don\'t change anything, just print possible changes
 28   -V    --version        display version and exit.
 29   forced info:           may be (multiple) chosen from the following case-sensitive list
 30         --TIT2=".*"      title      [string]
 31         --TRCK=\\d{2}     track no.  [2 digit number]
 32         --TPE1=".*"      artist     [string]
 33         --TYER=\\d{4}     year       [4 digit number]
 34         --TALB=".*"      album      [string]
 35 
 36 examples: '.$prg_name.' "^\\d\\d.*\\.mp3$"
 37           '.$prg_name.' "\\.mp3$" --removeAllTags
 38           '.$prg_name.' "\\.mp3$" -r --v=0 --TYER=2004 --TALB="dick und doof"
 39           '.$prg_name.' "\\.mp3$" -r -u --TYER=2004 --TALB="dick und doof"'."\n";
 40   my $syntax_correct=0;
 41   my %param_hash;
 42   $param_hash{'recursively'}  =0;
 43   $param_hash{'removeAllTags'}=0;
 44   $param_hash{'update'}      =0;
 45   $param_hash{'list_id3'}     =0;
 46   $param_hash{'1to2'}         =0;
 47   $param_hash{'2to1'}         =0;
 48   $param_hash{'verbose'}      =1;
 49   $param_hash{'version'}      =0;
 50   $param_hash{'test'}         =0;
 51   if(defined($params[0])){
 52     $param_hash{'filesRE'}=shift(@params);
 53     $syntax_correct=1;
 54     foreach(@params){
 55       if($_=~/^-[^-]./){
 56         while(length($_)>2){
 57           push(@params, '-'.substr($_, 2, 1));
 58           $_=substr($_, 0, 2).((length($_)>3)?substr($_, 3):'');
 59         }
 60       }
 61       if($_ eq '-r' || $_ eq '--recursively'){
 62         $param_hash{'recursively'}=1;
 63         next;
 64       }
 65       if($_ eq '-u' || $_ eq '--update'){
 66         $param_hash{'update'}=1;
 67         next;
 68       }
 69       if($_ eq '-l' || $_ eq '--list_id3'){
 70         $param_hash{'list_id3'}=1;
 71         next;
 72       }
 73       if($_ eq '--1to2'){
 74         $param_hash{'1to2'}=1;
 75         next;
 76       }
 77       if($_ eq '--2to1'){
 78         $param_hash{'2to1'}=1;
 79         next;
 80       }
 81       if($_ eq '--removeAllTags'){
 82         $param_hash{'removeAllTags'}=1;
 83         next;
 84       }
 85       if($_ eq '-t' || $_ eq '--test'){
 86         $param_hash{'test'}=1;
 87         next;
 88       }
 89       if($_=~/^--v(erbose)?=([0123])$/){
 90         $param_hash{'verbose'}=$2;
 91         next;
 92       }
 93       if($_ eq '-V' || $_ eq '--version'){
 94         $param_hash{'version'}=1;
 95         next;
 96       }
 97       if($_=~/^--(T[A-Z0-9]{3})=(.*)$/){
 98         $param_hash{$1}=$2;
 99       }else{
100         $syntax_correct=0;
101         last;
102       }
103     }
104   }
105   $syntax_correct=0 if $param_hash{'2to1'}==1 && $param_hash{'1to2'}==1;
106   $syntax_correct=0 if @params==0 && defined $param_hash{'filesRE'} && $param_hash{'filesRE'}=~/^--?h(elp)?$/;
107   if($param_hash{'version'}){
108     my $version_info='mod_id3.pl '.$version."\n".'
109 this program is distributed in the hope that it will be useful,
110 but without any warranty; without even the implied warranty of
111 merchantability or fitness for a particular purpose.
112 
113 originally written by seth (for e-mail-address see http://www.wg-karlsruhe.de/seth/email_address.php).'."\n";
114     die $version_info;
115   }else{
116     $syntax_correct || die $usage;
117   }
118   return %param_hash;
119 }
120 
121 sub print_id3_tags{
122   my $file=shift;
123   my $mp3=MP3::Tag->new($file);
124   $mp3->get_tags();
125   if(exists $mp3->{ID3v1}){
126     my @tagdata_v1=$mp3->{ID3v1}->all;
127     print '  id3v1: ('.$tagdata_v1[2].') '.$tagdata_v1[5].'. '.$tagdata_v1[1].' - '.$tagdata_v1[0].' ('.$tagdata_v1[3].")\n";
128     print '          '.$tagdata_v1[6].'; '.$tagdata_v1[4]."\n" if($tagdata_v1[4].$tagdata_v1[6] ne '');
129   }
130   if(exists $mp3->{ID3v2}){
131     my @tagdata_v2=('', '', '', '', '', '', '');
132     my $frameIDs_hash=$mp3->{ID3v2}->get_frame_ids;
133     foreach(keys %$frameIDs_hash){
134       my ($cont, $name)=$mp3->{ID3v2}->get_frame($_);
135       if(ref $cont){
136         while(my ($key,$val)=each %$cont){
137           push(@tagdata_v2, [($_, $key, $val)]) if $val ne '' && substr($key,0,1) ne '_';
138         }
139       }else{
140         if($_ eq 'TIT2'){ $tagdata_v2[0]=$cont;
141         }elsif($_ eq 'TPE1'){ $tagdata_v2[1]=$cont;
142         }elsif($_ eq 'TALB'){ $tagdata_v2[2]=$cont;
143         }elsif($_ eq 'TYER'){ $tagdata_v2[3]=$cont;
144         }elsif($_ eq 'TRCK'){ $tagdata_v2[5]=$cont;
145         }elsif($_ eq 'TCON'){ $tagdata_v2[6]=$cont;
146         }else{ push(@tagdata_v2, [($_, $name, $cont)]) if $cont ne '' && substr($name,0,1) ne '_';
147         }
148       }
149     }
150     print '  id3v2: ('.$tagdata_v2[2].') '.$tagdata_v2[5].'. '.$tagdata_v2[1].' - '.$tagdata_v2[0].' ('.$tagdata_v2[3].")\n";
151     print '          ';
152     print $tagdata_v2[6].'; ' if $tagdata_v2[6] ne '';
153     for(my $i=7;$i<@tagdata_v2;++$i){
154       print $tagdata_v2[$i][1].' ('.$tagdata_v2[$i][0].')='.$tagdata_v2[$i][2].'; ';
155     }
156     print "\n";
157   }
158   $mp3->close;
159 }
160 
161 sub set_info_to_file{
162   my $file=shift;
163   my @info;
164   for(1..5){
165     push(@info, shift);
166   }
167   my %params=@_;
168   die 'error 2078346: see code'."\n" if @_%2!=0;
169   my $removeAllTags=$params{'removeAllTags'};
170   my $verbose=$params{'verbose'};
171   my $update=$params{'update'};
172   my $one_to_two=$params{'1to2'};
173   my $two_to_one=$params{'2to1'};
174   my $test=$params{'test'};
175   my $mp3=MP3::Tag->new($file);
176   $mp3->get_tags() if $update==1;
177   if($test==0 && $one_to_two==0){
178     if($update==0 || not exists $mp3->{ID3v1}){
179       $mp3->new_tag("ID3v1") if not exists $mp3->{ID3v1};
180       if($removeAllTags==1){
181         $mp3->{ID3v1}->remove_tag();
182       }else{
183         $mp3->{ID3v1}->all($info[0], $info[2], $info[3], $info[4], '', $info[1], '');
184         $mp3->{ID3v1}->write_tag;
185       }
186     }else{ # $update==1 && exists $mp3->{ID3v1}
187       $mp3->{ID3v1}->title($info[0]) if($mp3->{ID3v1}->title eq '');
188       $mp3->{ID3v1}->track($info[1]) if($mp3->{ID3v1}->track eq '');
189       $mp3->{ID3v1}->artist($info[2]) if($mp3->{ID3v1}->artist eq '');
190       $mp3->{ID3v1}->album($info[3]) if($mp3->{ID3v1}->album eq '');
191       $mp3->{ID3v1}->year($info[4]) if($mp3->{ID3v1}->year eq '');
192       $mp3->{ID3v1}->write_tag;
193     }
194   }
195   if($two_to_one==0){
196     if($update==0 || not exists $mp3->{ID3v2}){
197       if($test==0){
198         $mp3->new_tag("ID3v2") if not exists $mp3->{ID3v2};
199         $mp3->{ID3v2}->remove_tag() if($update==0);
200         if($removeAllTags==0){
201           (defined $mp3->{ID3v2}->get_frame('TIT2'))? $mp3->{ID3v2}->change_frame('TIT2', $info[0]) : $mp3->{ID3v2}->add_frame('TIT2', $info[0]);
202           (defined $mp3->{ID3v2}->get_frame('TRCK'))? $mp3->{ID3v2}->change_frame('TRCK', $info[1]) : $mp3->{ID3v2}->add_frame('TRCK', $info[1]);
203           (defined $mp3->{ID3v2}->get_frame('TPE1'))? $mp3->{ID3v2}->change_frame('TPE1', $info[2]) : $mp3->{ID3v2}->add_frame('TPE1', $info[2]);
204           if(exists $params{'TYER'}){
205             (defined $mp3->{ID3v2}->get_frame('TYER'))? $mp3->{ID3v2}->change_frame('TYER', $info[4]) : $mp3->{ID3v2}->add_frame('TYER', $info[4]);
206           }
207           (defined $mp3->{ID3v2}->get_frame('TALB'))? $mp3->{ID3v2}->change_frame('TALB', $info[3]) : $mp3->{ID3v2}->add_frame('TALB', $info[3]);
208           $mp3->{ID3v2}->write_tag;
209         }
210       }
211     }else{ # $update==1 && exists $mp3->{ID3v2}
212       my $output=' forced : ';
213       my $changed=1;
214       if(defined $mp3->{ID3v2}->get_frame('TALB')){
215         if($mp3->{ID3v2}->get_frame('TALB') eq ''){
216           $mp3->{ID3v2}->change_frame('TALB', $info[3]) if($test==0);
217         }else{
218           $changed=0;
219         }
220       }else{
221         $mp3->{ID3v2}->add_frame('TALB', $info[3]) if($test==0);
222       }
223       $output.='('.(($changed==1)?$info[3]:'[leave]').') ';
224       $changed=1;
225       if(defined $mp3->{ID3v2}->get_frame('TRCK')){
226         if($mp3->{ID3v2}->get_frame('TRCK') eq ''){
227           $mp3->{ID3v2}->change_frame('TRCK', $info[1]) if($test==0);
228         }else{
229           $changed=0;
230         }
231       }else{
232         $mp3->{ID3v2}->add_frame('TRCK', $info[1]) if($test==0);
233       }
234       $output.=(($changed==1)?$info[1]:'[leave]').'. ';
235       $changed=1;
236       if(defined $mp3->{ID3v2}->get_frame('TPE1')){
237         if($mp3->{ID3v2}->get_frame('TPE1') eq ''){
238           $mp3->{ID3v2}->change_frame('TPE1', $info[2]) if($test==0);
239         }else{
240           $changed=0;
241         }
242       }else{
243         $mp3->{ID3v2}->add_frame('TPE1', $info[2]) if($test==0);
244       }
245       $output.=(($changed==1)?$info[2]:'[leave]').' - ';
246       $changed=1;
247       if(defined $mp3->{ID3v2}->get_frame('TIT2')){
248         if($mp3->{ID3v2}->get_frame('TIT2') eq ''){
249           $mp3->{ID3v2}->change_frame('TIT2', $info[0]) if($test==0);
250         }else{
251           $changed=0;
252         }
253       }else{
254         $mp3->{ID3v2}->add_frame('TIT2', $info[0]) if($test==0);
255       }
256       $output.=(($changed==1)?$info[0]:'[leave]').' ';
257       $changed=1;
258       if(exists $params{'TYER'}){
259         if(defined $mp3->{ID3v2}->get_frame('TYER')){
260           if($mp3->{ID3v2}->get_frame('TYER') eq ''){
261             $mp3->{ID3v2}->change_frame('TYER', $info[4]) if($test==0);
262           }else{
263             $changed=0;
264           }
265         }else{
266           $mp3->{ID3v2}->add_frame('TYER', $info[4]) if($test==0);
267         }
268       }
269       $output.='('.(($changed==1)?$info[4]:'[leave]').')';
270       print $output."\n" if($verbose>1);
271       $mp3->{ID3v2}->write_tag if($test==0);
272     }
273   }
274   $mp3->close;
275 }
276 
277 sub get_info_from_file{
278   my $file=shift;
279   my $verbose=shift;
280   my $list_id3=shift;
281   my $mp3=MP3::Tag->new($file);
282   $mp3->config('autoinfo', @_);
283   my @info=$mp3->autoinfo();
284   $info[4]=$info[5];
285   pop(@info);
286   pop(@info);
287   $info[1]='0'.$info[1] if(length($info[1])==1);
288   $mp3->close;
289   print_id3_tags($file) if($verbose>2 || ($verbose>0 && $list_id3));
290   return @info;
291 }
292 
293 sub search_dir{
294   my $working_dir=shift;
295   my %params=@_;
296   my $filesRE=$params{'filesRE'};
297   my $recursively=$params{'recursively'};
298   my $removeAllTags=$params{'removeAllTags'};
299   my $verbose=$params{'verbose'};
300   my $one_to_two=$params{'1to2'};
301   my $two_to_one=$params{'2to1'};
302   my $update=$params{'update'};
303   my $list_id3=$params{'list_id3'};
304   my $entry;
305   my @files;
306   my @dirs;
307   print "\n".'  '.$working_dir.'/'."\n" if $verbose>1 && $list_id3==0;
308   opendir(DIR, ".") || die $working_dir.": $!";
309   while(telldir(DIR)>=0){
310     $entry=readdir(DIR);
311     if(-d $entry){
312       push(@dirs, $entry)
313     }else{
314       if($entry=~/$filesRE/){
315         print "\n".'  '.$working_dir.'/'."\n" if(($verbose>1 && $list_id3==1) || ($verbose==1 && @files==0 && $list_id3==0));
316         print 'change: '.$entry."\n" if $verbose>0 && $list_id3==0;
317         push(@files, $entry);
318       }else{
319         print '  skip: '.$entry."\n" if $verbose>1 && $list_id3==0;
320       }
321     }
322   }
323   closedir(DIR);
324   @files=sort(@files);
325   @dirs=sort(@dirs);
326   my @info;
327   my @resources=('filename', 'ID3v2', 'ID3v1'); # 'ID3v2', 'ID3v1', 'filename'
328   @resources=('ID3v1') if $one_to_two==1;
329   @resources=('ID3v2') if $two_to_one==1;
330   foreach(@files){
331     @info=();
332     if($removeAllTags==0){
333       @info=get_info_from_file($working_dir.'\\'.$_, $verbose, $list_id3, @resources);
334       die('@info too small or to large.') if @info!=5;
335       print ' found : ('.$info[3].') '.$info[1].'. '.$info[2].' - '.$info[0].' ('.$info[4].')'."\n" if($verbose>1 && $list_id3==0 && $update==0);
336       $info[0]=$params{'TIT2'} if exists $params{'TIT2'};
337       $info[1]=$params{'TRCK'} if exists $params{'TRCK'};
338       $info[2]=$params{'TPE1'} if exists $params{'TPE1'};
339       $info[3]=$params{'TALB'} if exists $params{'TALB'};
340       $info[4]=$params{'TYER'} if exists $params{'TYER'};
341       print ' forced: ('.$info[3].') '.$info[1].'. '.$info[2].' - '.$info[0].' ('.$info[4].')'."\n" if($verbose>1 && $list_id3==0 && $update==0);
342     }elsif($removeAllTags==1){
343       @info=('','','','','');
344     }
345     set_info_to_file($working_dir.'\\'.$_, @info, %params) if $list_id3==0;
346   }
347   if($recursively==1){
348     foreach(@dirs){
349       if($_ ne '.' && $_ ne '..'){
350         chdir($_);
351         search_dir($working_dir.'/'.$_, %params);
352         chdir('..');
353       }
354     }
355   }
356 }
357 
358 sub write_id3_tags_using_filenames{
359   my %params=syntaxCheck(@_);
360   my $working_dir=cwd;
361   search_dir($working_dir, %params);
362   chdir($working_dir);
363 }
364 
365 write_id3_tags_using_filenames(@ARGV);