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