File Coverage

blib/lib/MP3/Tag/ID3v1.pm
Criterion Covered Total %
statement 136 172 79.0
branch 71 106 66.9
condition 22 63 34.9
subroutine 14 16 87.5
pod 7 11 63.6
total 250 368 67.9


line stmt bran cond sub pod time code
1             package 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              
9 6     6   40 use strict;
  6         11  
  6         286  
10 6     6   35 use vars qw /@mp3_genres @winamp_genres $AUTOLOAD %ok_length $VERSION @ISA/;
  6         13  
  6         15942  
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              
22             MP3::Tag::ID3v1 - Module for reading / writing ID3v1 tags of MP3 audio files
23              
24             =head1 SYNOPSIS
25              
26             MP3::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              
38             See L 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              
75             Thomas 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              
98             Use these functions to retrieve the date of these fields,
99             or to set the data.
100              
101             $genre can be a string with the name of the genre, or a number
102             describing the genre.
103              
104             =cut
105              
106             sub AUTOLOAD {
107 153     153   434 my $self = shift;
108 153         206 my $attr = $AUTOLOAD;
109              
110             # is it an allowed field
111 153         635 $attr =~ s/.*:://;
112 153 50       521 return unless $attr =~ /[^A-Z]/;
113 153 100       372 $attr = 'title' if $attr eq 'song';
114 153 50       334 warn "invalid field: ->$attr()" unless $ok_length{$attr};
115              
116 153 100       297 if (@_) {
117 79         131 my $new = shift;
118 79         271 $new =~ s/ *$//;
119 79 100       173 if ($attr eq "genre") {
120 17 100       63 if ($new =~ /^\d+$/) {
121 9         21 $self->{genreID} = $new;
122             } else {
123 8         19 $self->{genreID} = genre2id($new);
124             }
125             $new = id2genre($self->{genreID})
126 17 100 66     101 if defined $self->{genreID} and $self->{genreID} < @winamp_genres;
127             }
128 79         160 $new = substr $new, 0, $ok_length{$attr};
129 79         182 $self->{$attr}=$new;
130 79         152 $self->{changed} = 1;
131             }
132 153         298 $self->{$attr} =~ s/ +$//;
133 153         463 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              
143             Returns all information of the tag in a list.
144             You can use this sub also to set the data of the complete tag.
145              
146             The order of the data is always title, artist, album, year, comment, track, and genre.
147             genre has to be a string with the name of the genre, or a number identifying the genre.
148              
149             =cut
150              
151             sub all {
152 1     1 1 8 my $self=shift;
153 1 50       4 if ($#_ == 6) {
154 1         2 my $new;
155 1         4 for (qw/title artist album year comment track genre/) {
156 7         11 $new = shift;
157 7         9 $new =~ s/ +$//;
158 7         15 $new = substr $new, 0, $ok_length{$_};
159 7         10 $self->{$_}=$new;
160             }
161 1 50       4 if ($self->{genre} =~ /^\d+$/) {
162 0         0 $self->{genreID} = $self->{genre};
163             } else {
164 1         3 $self->{genreID} = genre2id($self->{genre});
165             }
166             $self->{genre} = id2genre($self->{genreID})
167 1 50 33     17 if defined $self->{genreID} and $self->{genreID} < @winamp_genres;
168 1         4 $self->{changed} = 1;
169             }
170 1         3 for (qw/title artist album year comment track genre/) {
171 7         12 $self->{$_} =~ s/ +$//;
172             }
173 1 50       6 if (wantarray) {
174             return ($self->{title},$self->{artist},$self->{album},
175 1         8 $self->{year},$self->{comment}, $self->{track}, $self->{genre});
176             }
177 0         0 return $self->{title};
178             }
179              
180             =pod
181              
182             =item fits_tag()
183              
184             warn "data truncated" unless $id3v1->fits_tag($hash);
185              
186             Check whether the info in ID3v1 tag fits into the format of the file.
187              
188             =cut
189              
190             sub fits_tag {
191 35     35 1 91 my ($self, $hash) = (shift, shift);
192 35         63 my $elt;
193 35 100       99 if (defined (my $track = $hash->{track})) {
194 15 100       68 $track = $track->[0] if ref $track;
195 15 100 33     124 return unless $track =~ /^\d{0,3}$/ and ($track eq '' or $track < 256);
      66        
196             }
197 33         60 my $s = '';
198 33         75 for $elt (qw(title artist album comment year)) {
199 153 100       366 next unless defined (my $data = $hash->{$elt});
200 43 100       98 $data = $data->[0] if ref $data;
201 43 50       101 return if $data =~ /[^\x00-\xFF]/;
202 43         70 $s .= $data;
203 43 100       100 next if $ok_length{$elt} >= length $data;
204             next
205 3 0 33     12 if $elt eq 'comment' and not $hash->{track} and length $data <= 30;
      33        
206 3         16 return;
207             }
208 30 100       104 if (defined (my $genre = $hash->{genre})) {
209 16 50       37 $genre = $genre->[0] if ref $genre;
210 16         51 my @g = MP3::Tag::Implemenation::_massage_genres($genre);
211 16 100       51 return if @g > 1;
212 14         34 my $id = MP3::Tag::Implemenation::_massage_genres($genre, 'num');
213 14 50 66     107 return if not defined $id or $id eq '' or $id == 255;
      66        
214             }
215 24 50       64 if ($s =~ /[^\x00-\x7E]/) {
216 0   0     0 my $w = ($self->get_config('encode_encoding_v1') || [0])->[0];
217 0   0     0 my $r = ($self->get_config('decode_encoding_v1') || [0])->[0];
218 0   0     0 $_ = (lc or 'iso-8859-1') for $r, $w;
219             # Safe: per-standard and read+write is idempotent:
220 0 0 0     0 return 1 if $r eq $w and $w eq 'iso-8859-1';
221 0 0 0     0 return !(($self->get_config('encoded_v1_fits')||[0])->[0])
222             if $w eq 'iso-8859-1'; # read+write not idempotent
223 0 0 0     0 return if $w ne $r
      0        
224             and not (($self->get_config('encoded_v1_fits')||[0])->[0]);
225             }
226 24         204 return 1;
227             }
228              
229             =item as_bin()
230              
231             $str = $id3v1->as_bin();
232              
233             Returns 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              
241             Writes the ID3v1 tag to the file.
242              
243             =cut
244              
245             sub as_bin {
246 33     33 1 53 my $self = shift;
247 33         90 my($t) = ( $self->{track} =~ m[^(\d+)(?:/|$)], 0 );
248 33         55 my (%f, $f, $e);
249 33         69 for $f (qw(title artist album comment) ) {
250 132         277 $f{$f} = $self->{$f};
251             }
252              
253 33 0 33     107 if ($e = $self->get_config('encode_encoding_v1') and $e->[0]) {
254 0         0 my $field;
255 0         0 require Encode;
256              
257 0         0 for $field (qw(title artist album comment)) {
258 0         0 $f{$field} = Encode::encode($e->[0], $f{$field});
259             }
260             }
261              
262 33 100       105 $f{comment} = pack "a28 x C", $f{comment}, $t if $t;
263 33 50       180 $self->{genreID}=255 unless $self->{genreID} =~ /^\d+$/;
264              
265             return pack("a3a30a30a30a4a30C","TAG",$f{title}, $f{artist},
266 33         282 $f{album}, $self->{year}, $f{comment}, $self->{genreID});
267             }
268              
269             sub write_tag {
270 33     33 1 60 my $self = shift;
271 33 50 33     146 return undef unless exists $self->{title} && exists $self->{changed};
272 33         94 my $data = $self->as_bin();
273 33         70 my $mp3obj = $self->{mp3};
274 33         53 my $mp3tag;
275 33         129 $mp3obj->close;
276 33 50       119 if ($mp3obj->open("write")) {
277 33         137 $mp3obj->seek(-128,2);
278 33         193 $mp3obj->read(\$mp3tag, 3);
279 33 100       175 if ($mp3tag eq "TAG") {
280 19         196 $mp3obj->seek(-125,2); # neccessary for windows
281 19         116 $mp3obj->write(substr $data, 3);
282             } else {
283 14         57 $mp3obj->seek(0,2);
284 14         72 $mp3obj->write($data);
285             }
286             } else {
287 0         0 warn "Couldn't open file `" . $mp3obj->filename() . "' to write tag";
288 0         0 return 0;
289             }
290 33         115 return 1;
291             }
292              
293             *writeTag = \&write_tag;
294              
295             =pod
296              
297             =item remove_tag()
298              
299             $id3v1->remove_tag();
300              
301             Removes the ID3v1 tag from the file. Returns negative on failure,
302             FALSE if no tag was found.
303              
304             (Caveat: only I is removed; some - broken - files may have
305             many chain-loaded one after another; you may need to call remove_tag()
306             in a loop to handle such beasts.)
307              
308             [old name: removeTag() . The old name is still available, but you
309             should use the new name]
310              
311             =cut
312              
313             sub remove_tag {
314 0     0 1 0 my $self = shift;
315 0         0 my $mp3obj = $self->{mp3};
316 0         0 my $mp3tag;
317 0         0 $mp3obj->seek(-128,2);
318 0         0 $mp3obj->read(\$mp3tag, 3);
319 0 0       0 if ($mp3tag eq "TAG") {
320 0         0 $mp3obj->close;
321 0 0       0 if ($mp3obj->open("write")) {
322 0         0 $mp3obj->truncate(-128);
323 0         0 $self->all("","","","","",0,255);
324 0         0 $mp3obj->close;
325 0         0 $self->{changed} = 1;
326 0         0 return 1;
327             }
328 0         0 return -1;
329             }
330 0         0 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              
343             Returns a list of all genres, or the according name or id to
344             a given id or name.
345              
346             =cut
347              
348             sub genres {
349             # return an array with all genres, of if a parameter is given, the according genre
350 81     81 1 168 my ($self, $genre) = @_;
351 81 50 66     368 if ( (defined $self) and (not defined $genre) and ($self !~ /MP3::Tag/)) {
      66        
352             ## genres may be called directly via MP3::Tag::ID3v1::genres()
353             ## and $self is then not used for an id3v1 object
354 75         113 $genre = $self;
355             }
356              
357 81 100       172 return \@winamp_genres unless defined $genre;
358              
359 75 100       284 if ($genre =~ /^\d+$/) {
360 61 100       277 return $winamp_genres[$genre] if $genre
361 22         78 return undef;
362             }
363              
364 14         28 my ($id, $found)=0;
365 14         27 foreach (@winamp_genres) {
366 1901 100       2945 if (uc $_ eq uc $genre) {
367 2         4 $found = 1;
368 2         4 last;
369             }
370 1899         2251 $id++;
371             }
372 14 100       33 $id=255 unless $found;
373 14         31 return $id;
374             }
375              
376             =item new()
377              
378             $id3v1 = MP3::Tag::ID3v1->new($mp3fileobj[, $create]);
379              
380             Generally called from MP3::Tag, because a $mp3fileobj is needed.
381             If $create is true, a new tag is created. Otherwise undef is
382             returned, if now ID3v1 tag is found in the $mp3obj.
383              
384             Please use
385              
386             $mp3 = MP3::Tag->new($filename);
387             $id3v1 = $mp3->new_tag("ID3v1"); # Empty new tag
388              
389             or
390              
391             $mp3 = MP3::Tag->new($filename);
392             $mp3->get_tags();
393             $id3v1 = $mp3->{ID3v1}; # Existing tag (if present)
394              
395             instead of using this function directly
396              
397             =back
398              
399             =cut
400              
401             # create a ID3v1 object
402             sub new {
403 101     101 1 218 my ($class, $fileobj, $create) = @_;
404 101         267 my $self={mp3=>$fileobj};
405 101         148 my $buffer;
406              
407 101 100       218 if ($create) {
408 16         46 $self->{new} = 1;
409             } else {
410 85 50 50     223 $fileobj->open or return unless $fileobj->is_open;
411 85         397 $fileobj->seek(-128,2);
412 85         396 $fileobj->read(\$buffer, 128);
413 85 100       565 return undef unless substr ($buffer,0,3) eq "TAG";
414             }
415              
416 65         200 bless $self, $class;
417 65         208 $self->read_tag($buffer); # $buffer unused if ->{new}
418 65         196 return $self;
419             }
420              
421             sub new_with_parent {
422 85     85 0 211 my ($class, $filename, $parent) = @_;
423 85 100       206 return unless my $new = $class->new($filename, undef);
424 49         90 $new->{parent} = $parent;
425 49         99 $new;
426             }
427              
428             #################
429             ##
430             ## internal subs
431              
432             # actually read the tag data
433             sub read_tag {
434 65     65 0 135 my ($self, $buffer) = @_;
435 65         97 my ($id3v1, $e);
436              
437 65 100       172 if ($self->{new}) {
438             ($self->{title}, $self->{artist}, $self->{album}, $self->{year},
439 16         110 $self->{comment}, $self->{track}, $self->{genre}, $self->{genreID}) = ("","","","","",'',"",255);
440 16         38 $self->{changed} = 1;
441             } else {
442             (undef, $self->{title}, $self->{artist}, $self->{album}, $self->{year},
443 49 50       583 $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 49 50       155 if ($id3v1!=0) { # ID3v1 tag found: track is not valid, comment two chars longer
450 0         0 $self->{comment} .= chr($id3v1);
451             $self->{comment} .= chr($self->{track})
452 0 0 0     0 if $self->{track} and $self->{track}!=32;
453 0         0 $self->{track} = '';
454             };
455 49 100       155 $self->{track} = '' unless $self->{track};
456 49         116 $self->{genre} = id2genre($self->{genreID});
457 49 0 33     161 if ($e = $self->get_config('decode_encoding_v1') and $e->[0]) {
458 0         0 my $field;
459 0         0 require Encode;
460              
461 0         0 for $field (qw(title artist album comment)) {
462 0         0 $self->{$field} = Encode::decode($e->[0], $self->{$field});
463             }
464             }
465             }
466             }
467              
468             # convert small integer id to genre name
469             sub id2genre {
470 61     61 0 135 my $id=shift;
471 61 100 66     269 return "" unless defined $id and $id < @winamp_genres;
472 33         76 return $winamp_genres[$id];
473             }
474              
475             # convert genre name to small integer id
476             sub genre2id {
477 9     9 0 27 my $genre = MP3::Tag::Implemenation::_massage_genres(shift, 'num');
478 9 100       31 return $genre if defined $genre;
479 3         5 return 255;
480             }
481              
482             # nothing to do for destroy
483       0     sub DESTROY {
484             }
485              
486             1;
487              
488             ######## define all the genres
489              
490 6     6   109 BEGIN { @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 6         376 @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              
532             L, L
533              
534             ID3v1 standard - http://www.id3.org
535              
536             =head1 COPYRIGHT
537              
538             Copyright (c) 2000-2004 Thomas Geffert. All rights reserved.
539              
540             This program is free software; you can redistribute it and/or
541             modify it under the terms of the Artistic License, distributed
542             with Perl.
543              
544             =cut