1package MP3::Tag::ID3v1; 2 3# Copyright (c) 2000-2004 Thomas Geffert. All rights reserved. 4# 5# This program is free software; you can redistribute it and/or 6# modify it under the terms of the Artistic License, distributed 7# with Perl. 8 9use strict; 10use vars qw /@mp3_genres @winamp_genres $AUTOLOAD %ok_length $VERSION @ISA/; 11 12$VERSION="1.00"; 13@ISA = 'MP3::Tag::__hasparent'; 14 15# allowed fields in ID3v1.1 and max length of this fields (except for track and genre which are coded later) 16%ok_length = (title => 30, artist => 30, album => 30, comment => 28, track => 3, genre => 3000, year=>4, genreID=>1); 17 18=pod 19 20=head1 NAME 21 22MP3::Tag::ID3v1 - Module for reading / writing ID3v1 tags of MP3 audio files 23 24=head1 SYNOPSIS 25 26MP3::Tag::ID3v1 is designed to be called from the MP3::Tag module. 27 28 use MP3::Tag; 29 $mp3 = MP3::Tag->new($filename); 30 31 # read an existing tag 32 $mp3->get_tags(); 33 $id3v1 = $mp3->{ID3v1} if exists $mp3->{ID3v1}; 34 35 # or create a new tag 36 $id3v1 = $mp3->new_tag("ID3v1"); 37 38See L<MP3::Tag|according documentation> for information on the above used functions. 39 40* Reading the tag 41 42 print " Title: " .$id3v1->title . "\n"; 43 print " Artist: " .$id3v1->artist . "\n"; 44 print " Album: " .$id3v1->album . "\n"; 45 print "Comment: " .$id3v1->comment . "\n"; 46 print " Year: " .$id3v1->year . "\n"; 47 print " Genre: " .$id3v1->genre . "\n"; 48 print " Track: " .$id3v1->track . "\n"; 49 50 # or at once 51 @tagdata = $mp3->all(); 52 foreach $tag (@tagdata) { 53 print $tag; 54 } 55 56* Changing / Writing the tag 57 58 $id3v1->comment("This is only a Test Tag"); 59 $id3v1->title("testing"); 60 $id3v1->artist("Artest"); 61 $id3v1->album("Test it"); 62 $id3v1->year("1965"); 63 $id3v1->track("5"); 64 $id3v1->genre("Blues"); 65 # or at once 66 $id3v1->all("song title","artist","album","1900","comment",10,"Ska"); 67 $id3v1->write_tag(); 68 69* Removing the tag from the file 70 71 $id3v1->remove_tag(); 72 73=head1 AUTHOR 74 75Thomas Geffert, thg@users.sourceforge.net 76 77=head1 DESCRIPTION 78 79=pod 80 81=over 82 83=item title(), artist(), album(), year(), comment(), track(), genre() 84 85 $artist = $id3v1->artist; 86 $artist = $id3v1->artist($artist); 87 $album = $id3v1->album; 88 $album = $id3v1->album($album); 89 $year = $id3v1->year; 90 $year = $id3v1->year($year); 91 $comment = $id3v1->comment; 92 $comment = $id3v1->comment($comment); 93 $track = $id3v1->track; 94 $track = $id3v1->track($track); 95 $genre = $id3v1->genre; 96 $genre = $id3v1->genre($genre); 97 98Use these functions to retrieve the date of these fields, 99or to set the data. 100 101$genre can be a string with the name of the genre, or a number 102describing the genre. 103 104=cut 105 106sub AUTOLOAD { 107 my $self = shift; 108 my $attr = $AUTOLOAD; 109 110 # is it an allowed field 111 $attr =~ s/.*:://; 112 return unless $attr =~ /[^A-Z]/; 113 $attr = 'title' if $attr eq 'song'; 114 warn "invalid field: ->$attr()" unless $ok_length{$attr}; 115 116 if (@_) { 117 my $new = shift; 118 $new =~ s/ *$//; 119 if ($attr eq "genre") { 120 if ($new =~ /^\d+$/) { 121 $self->{genreID} = $new; 122 } else { 123 $self->{genreID} = genre2id($new); 124 } 125 $new = id2genre($self->{genreID}) 126 if defined $self->{genreID} and $self->{genreID} < @winamp_genres; 127 } 128 $new = substr $new, 0, $ok_length{$attr}; 129 $self->{$attr}=$new; 130 $self->{changed} = 1; 131 } 132 $self->{$attr} =~ s/ +$//; 133 return $self->{$attr}; 134} 135 136=pod 137 138=item all() 139 140 @tagdata = $id3v1->all; 141 @tagdata = $id3v1->all($title, $artist, $album, $year, $comment, $track, $genre); 142 143Returns all information of the tag in a list. 144You can use this sub also to set the data of the complete tag. 145 146The order of the data is always title, artist, album, year, comment, track, and genre. 147genre has to be a string with the name of the genre, or a number identifying the genre. 148 149=cut 150 151sub all { 152 my $self=shift; 153 if ($#_ == 6) { 154 my $new; 155 for (qw/title artist album year comment track genre/) { 156 $new = shift; 157 $new =~ s/ +$//; 158 $new = substr $new, 0, $ok_length{$_}; 159 $self->{$_}=$new; 160 } 161 if ($self->{genre} =~ /^\d+$/) { 162 $self->{genreID} = $self->{genre}; 163 } else { 164 $self->{genreID} = genre2id($self->{genre}); 165 } 166 $self->{genre} = id2genre($self->{genreID}) 167 if defined $self->{genreID} and $self->{genreID} < @winamp_genres; 168 $self->{changed} = 1; 169 } 170 for (qw/title artist album year comment track genre/) { 171 $self->{$_} =~ s/ +$//; 172 } 173 if (wantarray) { 174 return ($self->{title},$self->{artist},$self->{album}, 175 $self->{year},$self->{comment}, $self->{track}, $self->{genre}); 176 } 177 return $self->{title}; 178} 179 180=pod 181 182=item fits_tag() 183 184 warn "data truncated" unless $id3v1->fits_tag($hash); 185 186Check whether the info in ID3v1 tag fits into the format of the file. 187 188=cut 189 190sub fits_tag { 191 my ($self, $hash) = (shift, shift); 192 my $elt; 193 if (defined (my $track = $hash->{track})) { 194 $track = $track->[0] if ref $track; 195 return unless $track =~ /^\d{0,3}$/ and ($track eq '' or $track < 256); 196 } 197 my $s = ''; 198 for $elt (qw(title artist album comment year)) { 199 next unless defined (my $data = $hash->{$elt}); 200 $data = $data->[0] if ref $data; 201 return if $data =~ /[^\x00-\xFF]/; 202 $s .= $data; 203 next if $ok_length{$elt} >= length $data; 204 next 205 if $elt eq 'comment' and not $hash->{track} and length $data <= 30; 206 return; 207 } 208 if (defined (my $genre = $hash->{genre})) { 209 $genre = $genre->[0] if ref $genre; 210 my @g = MP3::Tag::Implemenation::_massage_genres($genre); 211 return if @g > 1; 212 my $id = MP3::Tag::Implemenation::_massage_genres($genre, 'num'); 213 return if not defined $id or $id eq '' or $id == 255; 214 } 215 if ($s =~ /[^\x00-\x7E]/) { 216 my $w = ($self->get_config('encode_encoding_v1') || [0])->[0]; 217 my $r = ($self->get_config('decode_encoding_v1') || [0])->[0]; 218 $_ = (lc or 'iso-8859-1') for $r, $w; 219 # Safe: per-standard and read+write is idempotent: 220 return 1 if $r eq $w and $w eq 'iso-8859-1'; 221 return !(($self->get_config('encoded_v1_fits')||[0])->[0]) 222 if $w eq 'iso-8859-1'; # read+write not idempotent 223 return if $w ne $r 224 and not (($self->get_config('encoded_v1_fits')||[0])->[0]); 225 } 226 return 1; 227} 228 229=item as_bin() 230 231 $str = $id3v1->as_bin(); 232 233Returns the ID3v1 tag as a string. 234 235=item write_tag() 236 237 $id3v1->write_tag(); 238 239 [old name: writeTag() . The old name is still available, but you should use the new name] 240 241Writes the ID3v1 tag to the file. 242 243=cut 244 245sub as_bin { 246 my $self = shift; 247 my($t) = ( $self->{track} =~ m[^(\d+)(?:/|$)], 0 ); 248 my (%f, $f, $e); 249 for $f (qw(title artist album comment) ) { 250 $f{$f} = $self->{$f}; 251 } 252 253 if ($e = $self->get_config('encode_encoding_v1') and $e->[0]) { 254 my $field; 255 require Encode; 256 257 for $field (qw(title artist album comment)) { 258 $f{$field} = Encode::encode($e->[0], $f{$field}); 259 } 260 } 261 262 $f{comment} = pack "a28 x C", $f{comment}, $t if $t; 263 $self->{genreID}=255 unless $self->{genreID} =~ /^\d+$/; 264 265 return pack("a3a30a30a30a4a30C","TAG",$f{title}, $f{artist}, 266 $f{album}, $self->{year}, $f{comment}, $self->{genreID}); 267} 268 269sub write_tag { 270 my $self = shift; 271 return undef unless exists $self->{title} && exists $self->{changed}; 272 my $data = $self->as_bin(); 273 my $mp3obj = $self->{mp3}; 274 my $mp3tag; 275 $mp3obj->close; 276 if ($mp3obj->open("write")) { 277 $mp3obj->seek(-128,2); 278 $mp3obj->read(\$mp3tag, 3); 279 if ($mp3tag eq "TAG") { 280 $mp3obj->seek(-125,2); # neccessary for windows 281 $mp3obj->write(substr $data, 3); 282 } else { 283 $mp3obj->seek(0,2); 284 $mp3obj->write($data); 285 } 286 } else { 287 warn "Couldn't open file `" . $mp3obj->filename() . "' to write tag"; 288 return 0; 289 } 290 return 1; 291} 292 293*writeTag = \&write_tag; 294 295=pod 296 297=item remove_tag() 298 299 $id3v1->remove_tag(); 300 301Removes the ID3v1 tag from the file. Returns negative on failure, 302FALSE if no tag was found. 303 304(Caveat: only I<one tag> is removed; some - broken - files may have 305many chain-loaded one after another; you may need to call remove_tag() 306in a loop to handle such beasts.) 307 308[old name: removeTag() . The old name is still available, but you 309should use the new name] 310 311=cut 312 313sub remove_tag { 314 my $self = shift; 315 my $mp3obj = $self->{mp3}; 316 my $mp3tag; 317 $mp3obj->seek(-128,2); 318 $mp3obj->read(\$mp3tag, 3); 319 if ($mp3tag eq "TAG") { 320 $mp3obj->close; 321 if ($mp3obj->open("write")) { 322 $mp3obj->truncate(-128); 323 $self->all("","","","","",0,255); 324 $mp3obj->close; 325 $self->{changed} = 1; 326 return 1; 327 } 328 return -1; 329 } 330 return 0; 331} 332 333*removeTag = \&remove_tag; 334 335=pod 336 337=item genres() 338 339 @allgenres = $id3v1->genres; 340 $genreName = $id3v1->genres($genreID); 341 $genreID = $id3v1->genres($genreName); 342 343Returns a list of all genres, or the according name or id to 344a given id or name. 345 346=cut 347 348sub genres { 349 # return an array with all genres, of if a parameter is given, the according genre 350 my ($self, $genre) = @_; 351 if ( (defined $self) and (not defined $genre) and ($self !~ /MP3::Tag/)) { 352 ## genres may be called directly via MP3::Tag::ID3v1::genres() 353 ## and $self is then not used for an id3v1 object 354 $genre = $self; 355 } 356 357 return \@winamp_genres unless defined $genre; 358 359 if ($genre =~ /^\d+$/) { 360 return $winamp_genres[$genre] if $genre<scalar @winamp_genres; 361 return undef; 362 } 363 364 my ($id, $found)=0; 365 foreach (@winamp_genres) { 366 if (uc $_ eq uc $genre) { 367 $found = 1; 368 last; 369 } 370 $id++; 371 } 372 $id=255 unless $found; 373 return $id; 374} 375 376=item new() 377 378 $id3v1 = MP3::Tag::ID3v1->new($mp3fileobj[, $create]); 379 380Generally called from MP3::Tag, because a $mp3fileobj is needed. 381If $create is true, a new tag is created. Otherwise undef is 382returned, if now ID3v1 tag is found in the $mp3obj. 383 384Please use 385 386 $mp3 = MP3::Tag->new($filename); 387 $id3v1 = $mp3->new_tag("ID3v1"); # Empty new tag 388 389or 390 391 $mp3 = MP3::Tag->new($filename); 392 $mp3->get_tags(); 393 $id3v1 = $mp3->{ID3v1}; # Existing tag (if present) 394 395instead of using this function directly 396 397=back 398 399=cut 400 401# create a ID3v1 object 402sub new { 403 my ($class, $fileobj, $create) = @_; 404 my $self={mp3=>$fileobj}; 405 my $buffer; 406 407 if ($create) { 408 $self->{new} = 1; 409 } else { 410 $fileobj->open or return unless $fileobj->is_open; 411 $fileobj->seek(-128,2); 412 $fileobj->read(\$buffer, 128); 413 return undef unless substr ($buffer,0,3) eq "TAG"; 414 } 415 416 bless $self, $class; 417 $self->read_tag($buffer); # $buffer unused if ->{new} 418 return $self; 419} 420 421sub new_with_parent { 422 my ($class, $filename, $parent) = @_; 423 return unless my $new = $class->new($filename, undef); 424 $new->{parent} = $parent; 425 $new; 426} 427 428################# 429## 430## internal subs 431 432# actually read the tag data 433sub read_tag { 434 my ($self, $buffer) = @_; 435 my ($id3v1, $e); 436 437 if ($self->{new}) { 438 ($self->{title}, $self->{artist}, $self->{album}, $self->{year}, 439 $self->{comment}, $self->{track}, $self->{genre}, $self->{genreID}) = ("","","","","",'',"",255); 440 $self->{changed} = 1; 441 } else { 442 (undef, $self->{title}, $self->{artist}, $self->{album}, $self->{year}, 443 $self->{comment}, $id3v1, $self->{track}, $self->{genreID}) = 444 unpack (($] < 5.6 445 ? "a3 A30 A30 A30 A4 A28 C C C" # Trailing spaces stripped too 446 : "a3 Z30 Z30 Z30 Z4 Z28 C C C"), 447 $buffer); 448 449 if ($id3v1!=0) { # ID3v1 tag found: track is not valid, comment two chars longer 450 $self->{comment} .= chr($id3v1); 451 $self->{comment} .= chr($self->{track}) 452 if $self->{track} and $self->{track}!=32; 453 $self->{track} = ''; 454 }; 455 $self->{track} = '' unless $self->{track}; 456 $self->{genre} = id2genre($self->{genreID}); 457 if ($e = $self->get_config('decode_encoding_v1') and $e->[0]) { 458 my $field; 459 require Encode; 460 461 for $field (qw(title artist album comment)) { 462 $self->{$field} = Encode::decode($e->[0], $self->{$field}); 463 } 464 } 465 } 466} 467 468# convert small integer id to genre name 469sub id2genre { 470 my $id=shift; 471 return "" unless defined $id and $id < @winamp_genres; 472 return $winamp_genres[$id]; 473} 474 475# convert genre name to small integer id 476sub genre2id { 477 my $genre = MP3::Tag::Implemenation::_massage_genres(shift, 'num'); 478 return $genre if defined $genre; 479 return 255; 480} 481 482# nothing to do for destroy 483sub DESTROY { 484} 485 4861; 487 488######## define all the genres 489 490BEGIN { @mp3_genres = ( 'Blues', 'Classic Rock', 'Country', 'Dance', 491 'Disco', 'Funk', 'Grunge', 'Hip-Hop', 'Jazz', 'Metal', 'New Age', 492 'Oldies', 'Other', 'Pop', 'R&B', 'Rap', 'Reggae', 'Rock', 'Techno', 493 'Industrial', 'Alternative', 'Ska', 'Death Metal', 'Pranks', 494 'Soundtrack', 'Euro-Techno', 'Ambient', 'Trip-Hop', 'Vocal', 495 'Jazz+Funk', 'Fusion', 'Trance', 'Classical', 'Instrumental', 'Acid', 496 'House', 'Game', 'Sound Clip', 'Gospel', 'Noise', 'AlternRock', 497 'Bass', 'Soul', 'Punk', 'Space', 'Meditative', 'Instrumental Pop', 498 'Instrumental Rock', 'Ethnic', 'Gothic', 'Darkwave', 499 'Techno-Industrial', 'Electronic', 'Pop-Folk', 'Eurodance', 'Dream', 500 'Southern Rock', 'Comedy', 'Cult', 'Gangsta', 'Top 40', 501 'Christian Rap', 'Pop/Funk', 'Jungle', 'Native American', 'Cabaret', 'New Wave', 502 'Psychadelic', 'Rave', 'Showtunes', 'Trailer', 'Lo-Fi', 'Tribal', 503 'Acid Punk', 'Acid Jazz', 'Polka', 'Retro', 'Musical', 'Rock & Roll', 504 'Hard Rock', ); 505 506 @winamp_genres = ( @mp3_genres, 'Folk', 'Folk-Rock', 507 'National Folk', 'Swing', 'Fast Fusion', 'Bebob', 'Latin', 'Revival', 508 'Celtic', 'Bluegrass', 'Avantgarde', 'Gothic Rock', 509 'Progressive Rock', 'Psychedelic Rock', 'Symphonic Rock', 510 'Slow Rock', 'Big Band', 'Chorus', 'Easy Listening', 511 'Acoustic', 'Humour', 'Speech', 'Chanson', 'Opera', 512 'Chamber Music', 'Sonata', 'Symphony', 'Booty Bass', 'Primus', 513 'Porn Groove', 'Satire', 'Slow Jam', 'Club', 'Tango', 'Samba', 514 'Folklore', 'Ballad', 'Power Ballad', 'Rhythmic Soul', 515 'Freestyle', 'Duet', 'Punk Rock', 'Drum Solo', 'Acapella', 516 'Euro-House', 'Dance Hall', 517 # More from MP3::Info 518 'Goa', 'Drum & Bass', 'Club-House', 'Hardcore', 519 'Terror', 'Indie', 'BritPop', 'Negerpunk', 520 'Polsk Punk', 'Beat', 'Christian Gangsta Rap', 521 'Heavy Metal', 'Black Metal', 'Crossover', 522 'Contemporary Christian Music', 'Christian Rock', 523 'Merengue', 'Salsa', 'Thrash Metal', 'Anime', 524 'JPop', 'SynthPop', # 149 525 ); 526} 527 528=pod 529 530=head1 SEE ALSO 531 532L<MP3::Tag>, L<MP3::Tag::ID3v2> 533 534ID3v1 standard - http://www.id3.org 535 536=head1 COPYRIGHT 537 538Copyright (c) 2000-2004 Thomas Geffert. All rights reserved. 539 540This program is free software; you can redistribute it and/or 541modify it under the terms of the Artistic License, distributed 542with Perl. 543 544=cut 545