1package MP3::Tag::ID3v1;
2
3use strict;
4use vars qw /@mp3_genres @winamp_genres $AUTOLOAD %ok_length $VERSION/;
5
6$VERSION="0.60";
7
8# allowed fields in ID3v1.1 and max length of this fields (expect for track and genre which are coded later)
9%ok_length = (song => 30, artist => 30, album => 30, comment => 28, track => 3, genre => 30, year=>4, genreID=>1);
10
11=pod
12
13=head1 NAME
14
15MP3::Tag::ID3v1 - Module for reading / writing ID3v1 tags of MP3 audio files
16
17=head1 SYNOPSIS
18
19MP3::Tag::ID3v1 is designed to be called from the MP3::Tag module.
20
21  use MP3::Tag;
22  $mp3 = MP3::Tag->new($filename);
23
24  # read an existing tag
25  $mp3->get_tags();
26  $id3v1 = $mp3->{ID3v1} if exists $mp3->{ID3v1};
27
28  # or create a new tag
29  $id3v1 = $mp3->new_tag("ID3v1");
30
31See L<MP3::Tag|according documentation> for information on the above used functions.
32
33* Reading the tag
34
35    print "   Song: " .$id3v1->song . "\n";
36    print " Artist: " .$id3v1->artist . "\n";
37    print "  Album: " .$id3v1->album . "\n";
38    print "Comment: " .$id3v1->comment . "\n";
39    print "   Year: " .$id3v1->year . "\n";
40    print "  Genre: " .$id3v1->genre . "\n";
41    print "  Track: " .$id3v1->track . "\n";
42
43    # or at once
44    @tagdata = $mp3->all();
45    foreach $tag (@tagdata) {
46	print $tag;
47    }
48
49* Changing / Writing the tag
50
51      $id3v1->comment("This is only a Test Tag");
52      $id3v1->song("testing");
53      $id3v1->artist("Artest");
54      $id3v1->album("Test it");
55      $id3v1->year("1965");
56      $id3v1->track("5");
57      $id3v1->genre("Blues");
58      # or at once
59      $id3v1->all("song title","artist","album","1900","comment",10,"Ska");
60      $id3v1->write_tag();
61
62* Removing the tag from the file
63
64      $id3v1->remove_tag();
65
66=head1 AUTHOR
67
68Thomas Geffert, thg@users.sourceforge.net
69
70=head1 DESCRIPTION
71
72=pod
73
74=item song(), artist(), album(), year(), comment(), track(), genre()
75
76  $artist  = $id3v1->artist;
77  $artist  = $id3v1->artist($artist);
78  $album   = $id3v1->album;
79  $album   = $id3v1->album($album);
80  $year    = $id3v1->year;
81  $year    = $id3v1->year($year);
82  $comment = $id3v1->comment;
83  $comment = $id3v1->comment($comment);
84  $track   = $id3v1->track;
85  $track   = $id3v1->track($track);
86  $genre   = $id3v1->genre;
87  $genre   = $id3v1->genre($genre);
88
89Use these functions to retrieve the date of these fields,
90or to set the data.
91
92$genre can be a string with the name of the genre, or a number
93describing the genre.
94
95=cut
96
97sub AUTOLOAD {
98  my $self = shift;
99  my $attr = $AUTOLOAD;
100
101  # is it an allowed field
102  $attr =~ s/.*:://;
103  return unless $attr =~ /[^A-Z]/;
104  warn "invalid field: ->$attr()" unless $ok_length{$attr};
105
106  if (my $new = shift) {
107    $new =~ s/ *$//;
108    $new = substr  $new, 0, $ok_length{$attr};
109    if ($attr eq "genre") {
110      if ($new =~ /^\d+$/) {
111	$self->{genreID} = $new;
112      } else {
113	$self->{genreID} = genre2id($new);
114      }
115      $new = id2genre($self->{genreID});
116    }
117    $self->{$attr}=$new;
118    $self->{changed} = 1;
119  }
120  $self->{$attr} =~ s/ +$//;
121  return $self->{$attr};
122}
123
124=pod
125
126=item all()
127
128  @tagdata = $id3v1->all;
129  @tagdata = $id3v1->all($song, $artist, $album, $year, $comment, $track, $genre);
130
131Returns all information of the tag in a list.
132You can use this sub also to set the data of the complete tag.
133
134The order of the data is always song, artist, album, year, comment, track, and  genre.
135genre has to be a string with the name of the genre, or a number identifying the genre.
136
137=cut
138
139sub all {
140  my $self=shift;
141  if ($#_ == 6) {
142      my $new;
143      for (qw/song artist album year comment track genre/) {
144	  $new = shift;
145	  $new =~ s/ +$//;
146	  $new = substr  $new, 0, $ok_length{$_};
147	  $self->{$_}=$new;
148      }
149      if ($self->{genre} =~ /^\d+$/) {
150	  $self->{genreID} = $self->{genre};
151      } else {
152	  $self->{genreID} = genre2id($self->{genre});
153      }
154      $self->{genre} = id2genre($self->{genreID});
155      $self->{changed} = 1;
156  }
157  for (qw/song artist album year comment track genre/) {
158      $self->{$_} =~ s/ +$//;
159  }
160  if (wantarray) {
161      return ($self->{song},$self->{artist},$self->{album},
162	      $self->{year},$self->{comment}, $self->{track}, $self->{genre});
163  }
164  return $self->{song};
165}
166=pod
167
168=item write_tag()
169
170  $id3v1->write_tag();
171
172  [old name: writeTag() . The old name is still available, but you should use the new name]
173
174Writes the ID3v1 tag to the file.
175
176=cut
177
178sub write_tag {
179    my $self = shift;
180    return undef unless exists $self->{song} && exists $self->{changed};
181    $self->{track}=0 unless $self->{track} =~ /^\d+$/;
182    $self->{genreID}=255 unless $self->{genreID} =~ /^\d+$/;
183    my $data = pack("a30a30a30a4a28xCC",$self->{song},$self->{artist},$self->{album},
184		    $self->{year}, $self->{comment}, $self->{track}, $self->{genreID});
185    my $mp3obj = $self->{mp3};
186    my $mp3tag;
187    $mp3obj->close;
188    if ($mp3obj->open("write")) {
189	$mp3obj->seek(-128,2);
190	$mp3obj->read(\$mp3tag, 3);
191	if ($mp3tag eq "TAG") {
192	    $mp3obj->seek(-125,2); # neccessary for windows
193	    $mp3obj->write($data);
194	} else {
195	    $mp3obj->seek(0,2);
196	    $mp3obj->write("TAG$data");
197	}
198    } else {
199	warn "Couldn't open file to write tag";
200	return 0;
201    }
202    return 1;
203}
204
205*writeTag = \&write_tag;
206
207=pod
208
209=item remove_tag()
210
211  $id3v1->remove_tag();
212
213  [old name: removeTag() . The old name is still available, but you should use the new name]
214
215Removes the ID3v1 tag from the file.
216
217=cut
218
219sub remove_tag {
220  my $self = shift;
221  my $mp3obj = $self->{mp3};
222  my $mp3tag;
223  $mp3obj->seek(-128,2);
224  $mp3obj->read(\$mp3tag, 3);
225  if ($mp3tag eq "TAG") {
226    $mp3obj->close;
227    if ($mp3obj->open("write")) {
228      $mp3obj->truncate(-128);
229      $self->all("","","","","",0,255);
230      $mp3obj->close;
231      $self->{changed} = 1;
232      return 1;
233    }
234    return -1;
235  }
236  return 0;
237}
238
239*removeTag = \&remove_tag;
240
241=pod
242
243=item genres()
244
245  @allgenres = $id3v1->genres;
246  $genreName = $id3v1->genres($genreID);
247  $genreID   = $id3v1->genres($genreName);
248
249Returns a list of all genres, or the according name or id to
250a given id or name.
251
252=cut
253
254sub genres {
255    # return an array with all genres, of if a parameter is given, the according genre
256    my ($self, $genre) = @_;
257    if ( (defined $self) and (not defined $genre) and ($self !~ /MP3::Tag/)) {
258	## genres may be called directly via MP3::Tag::ID3v1::genres()
259	## and $self is then not used for an id3v1 object
260	$genre = $self;
261    }
262    return \@winamp_genres unless defined $genre;
263    return $winamp_genres[$genre] if $genre =~ /^\d+$/;
264    my $r;
265    foreach (@winamp_genres) {
266	if ($_ eq $genre) {
267	    $r=$_;
268	    last;
269	}
270    }
271    return $r;
272}
273
274=item new()
275
276  $id3v1 = MP3::Tag::ID3v1->new($mp3fileobj[, $create]);
277
278Generally called from MP3::Tag, because a $mp3fileobj is needed.
279If $create is true, a new tag is created. Otherwise undef is
280returned, if now ID3v1 tag is found in the $mp3obj.
281
282Please use
283
284   $mp3 = MP3::Tag->new($filename);
285   $id3v1 = $mp3->new_tag($filename);
286
287instead of using this function directly
288
289=cut
290
291# create a ID3v1 object
292sub new {
293    my ($class, $mp3obj, $create) = @_;
294    my $self={mp3=>$mp3obj};
295    my $buffer;
296
297    if (defined $create && $create) {
298	$self->{new} = 1;
299    } else {
300	$mp3obj->seek(-128,2);
301	$mp3obj->read(\$buffer, 128);
302	$mp3obj->close;
303    }
304
305    if (exists $self->{new} || substr ($buffer,0,3) eq "TAG") {
306	bless $self, $class;
307	$self->read_tag($buffer);
308
309	return $self;
310    } else {
311	return undef;
312    }
313}
314
315#################
316##
317## internal subs
318
319# actually read the tag data
320sub read_tag {
321    my ($self, $buffer) = @_;
322    my $mp3obj = $self->{mp3};
323    my $id3v1;
324
325    if ($self->{new}) {
326	($self->{song}, $self->{artist}, $self->{album}, $self->{year},
327	 $self->{comment}, $self->{track}, $self->{genre}, $self->{genreID}) = ("","","","","",0,"",255);
328	$self->{changed} = 1;
329    } else {
330	(undef, $self->{song}, $self->{artist}, $self->{album}, $self->{year},
331	 $self->{comment}, $id3v1, $self->{track}, $self->{genreID}) =
332	   unpack ("a3Z30Z30Z30Z4Z28CCC", $buffer);
333
334	if ($id3v1!=0) { # ID3v1 tag found: track is not valid, comment two chars longer
335	    $self->{comment} .= chr($id3v1);
336	    $self->{comment} .= chr($self->{track}) if $self->{track}!=32;
337	    $self->{track} = 0;
338	};
339	$self->{genre} = id2genre($self->{genreID});
340    }
341}
342
343# convert one byte id to genre name
344sub id2genre {
345    my $id=shift;
346    return "" unless defined $id && $id<$#winamp_genres;
347    return $winamp_genres[$id];
348}
349
350# convert genre name to one byte id
351sub genre2id {
352    my $genre = shift;
353    my $i=0;
354    foreach (@winamp_genres) {
355	if (uc $genre eq uc $_) {
356	    return $i;
357	}
358	$i++,
359    }
360    return 255;
361}
362
363# nothing to do for destroy
364sub DESTROY {
365}
366
3671;
368
369######## define all the genres
370
371BEGIN { @mp3_genres = ( 'Blues', 'Classic Rock', 'Country', 'Dance',
372			'Disco', 'Funk', 'Grunge', 'Hip-Hop', 'Jazz', 'Metal', 'New Age',
373			'Oldies', 'Other', 'Pop', 'R&B', 'Rap', 'Reggae', 'Rock', 'Techno',
374			'Industrial', 'Alternative', 'Ska', 'Death Metal', 'Pranks',
375			'Soundtrack', 'Euro-Techno', 'Ambient', 'Trip-Hop', 'Vocal',
376			'Jazz+Funk', 'Fusion', 'Trance', 'Classical', 'Instrumental', 'Acid',
377			'House', 'Game', 'Sound Clip', 'Gospel', 'Noise', 'AlternRock',
378			'Bass', 'Soul', 'Punk', 'Space', 'Meditative', 'Instrumental Pop',
379			'Instrumental Rock', 'Ethnic', 'Gothic', 'Darkwave',
380			'Techno-Industrial', 'Electronic', 'Pop-Folk', 'Eurodance', 'Dream',
381			'Southern Rock', 'Comedy', 'Cult', 'Gangsta', 'Top 40',
382			'Christian Rap', 'Pop/Funk', 'Jungle', 'Native American', 'Cabaret', 'New Wave',
383			'Psychadelic', 'Rave', 'Showtunes', 'Trailer', 'Lo-Fi', 'Tribal',
384			'Acid Punk', 'Acid Jazz', 'Polka', 'Retro', 'Musical', 'Rock & Roll',
385			'Hard Rock', );
386
387  	@winamp_genres = ( @mp3_genres, 'Folk', 'Folk-Rock',
388			   'National Folk', 'Swing', 'Fast Fusion', 'Bebob', 'Latin', 'Revival',
389			   'Celtic', 'Bluegrass', 'Avantgarde', 'Gothic Rock',
390			   'Progressive Rock', 'Psychedelic Rock', 'Symphonic Rock',
391			   'Slow Rock', 'Big Band', 'Chorus', 'Easy Listening',
392			   'Acoustic', 'Humour', 'Speech', 'Chanson', 'Opera',
393			   'Chamber Music', 'Sonata', 'Symphony', 'Booty Bass', 'Primus',
394			   'Porn Groove', 'Satire', 'Slow Jam', 'Club', 'Tango', 'Samba',
395			   'Folklore', 'Ballad', 'Power Ballad', 'Rhythmic Soul',
396			   'Freestyle', 'Duet', 'Punk Rock', 'Drum Solo', 'Acapella',
397			   'Euro-House', 'Dance Hall', );
398}
399
400=pod
401
402=head1 SEE ALSO
403
404L<MP3::Tag>, L<MP3::Tag::ID3v2>
405
406ID3v1 standard - http://www.id3.org
407
408=cut
409