7cd15b90998a9c5e5a00554d814875ae82284fd2
[id3fs.git] / lib / ID3FS / AudioFile / Mp3.pm
1 # id3fs - a FUSE-based filesystem for browsing audio metadata
2 # Copyright (C) 2010  Ian Beckwith <ianb@erislabs.net>
3 #
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation, either version 3 of the License, or
7 # (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
16
17 package ID3FS::AudioFile::Mp3;
18
19 use strict;
20 use warnings;
21 use MP3::Tag;
22 use MP3::Info;
23
24 sub new
25 {
26     my $proto=shift;
27     my $class=ref($proto) || $proto;
28     my $self={};
29     bless($self,$class);
30
31     $self->{path}=shift;
32     $self->{mp3tag}=MP3::Tag->new($self->{path});
33     $self->{mp3info}=MP3::Info->new($self->{path});
34     $self->get_tags();
35     $self->{tags}={};
36
37     return $self;
38 }
39
40 sub set
41 {
42     my ($self, $func, $value)=@_;
43     return $self->choose($func) unless($value);
44     unless(exists($self->{mp3tag}->{ID3v1}))
45     {
46         $self->{mp3tag}->new_tag("ID3v1");
47     }
48     unless(exists($self->{mp3tag}->{ID3v2}))
49     {
50         $self->{mp3tag}->new_tag("ID3v2");
51     }
52     my $method=$func . "_set";
53     $self->{mp3tag}->$method($value, 1);
54     return $value;
55 }
56
57 sub choose
58 {
59     my($self, $func)=@_;
60     my $thing=undef;
61     if(exists($self->{mp3tag}->{ID3v2}))
62     {
63         $thing=$self->{mp3tag}->{ID3v2}->$func();
64     }
65     if(exists($self->{mp3tag}->{ID3v1}) && (!defined($thing) || !length($thing)))
66     {
67         $thing=$self->{mp3tag}->{ID3v1}->$func();
68     }
69     return $thing;
70 }
71
72 sub year      { return(shift->set("year",    @_)); }
73 sub artist    { return(shift->set("artist",  @_)); }
74 sub album     { return(shift->set("album",   @_)); }
75 sub track     { return(shift->set("title",   @_)); }
76 sub tracknum  { return(shift->set("track",   @_)); }
77 sub comment   { return(shift->set("comment", @_)); }
78
79 sub audiotype { return "mp3";         }
80 sub haspic    { return undef;         } # NEXTVERSION
81
82 # we only set v2 genre
83 sub genre
84 {
85     my ($self, $value)=@_;
86     if($value)
87     {
88         if(exists($self->{mp3tag}->{ID3v2}))
89         {
90             $self->{mp3tag}->{ID3v2}->remove_frame("TCON");
91         }
92         else
93         {
94             $self->{mp3tag}->new_tag("ID3v2");
95         }
96         $self->{mp3tag}->{ID3v2}->add_frame("TCON", $value);
97
98     }
99     return($self->{mp3tag}->{ID3v2}->genre());
100 }
101
102 sub v1genre
103 {
104     my($self, $val)=@_;
105     if($val)
106     {
107         $self->{mp3tag}->new_tag("ID3v1") unless(defined($self->{mp3tag}->{ID3v1}));
108         $self->{mp3tag}->{ID3v1}->genre($val);
109         return $val;
110     }
111     my $genre=undef;
112     $genre=$self->{ID3v1}->genre() if(defined($self->{ID3v1}));
113     return $genre;
114 }
115
116 sub tags
117 {
118     my $self=shift;
119     return() unless(exists($self->{mp3tag}->{ID3v2}) && defined($self->{mp3tag}->{ID3v2}));
120     return($self->{mp3tag}->{ID3v2}->genre());
121 }
122
123 sub get_tags
124 {
125     my ($self)=@_;
126     # MP3::Tag->get_tags shows cryptic debug info via print when it finds
127     # an unhandled id3v2 version, in addition to the warning, so use
128     # select to send prints to /dev/null
129     my $oldout=undef;
130     if(open(NULL,">/dev/null"))
131     {
132         $oldout=select(NULL);
133     }
134     eval { $self->{mp3tag}->get_tags; };
135     warn("$self->{path}: $@\n") if($@);
136     if(defined($oldout))
137     {
138         select($oldout);
139         close(NULL);
140     }
141 }
142
143 sub add_tags
144 {
145     my($self, @tags)=@_;
146     my $existing=$self->tags();
147     my @existing=split(/\s*,\s*/, $existing) if($existing);
148     my @merged=$self->uniq(@tags, @existing);
149     my $genre=join(', ', @merged);
150     return($self->set("genre", $genre));
151 }
152
153 sub write
154 {
155     my $self=shift;
156     if(exists($self->{mp3tag}->{ID3v1}))
157     {
158         my $del=1;
159         my $artist=$self->{mp3tag}->{ID3v1}->artist();
160         $del=0 if($artist && $artist =~ /\S+/);
161         my $album=$self->{mp3tag}->{ID3v1}->album();
162         $del=0 if($album && $album =~ /\S+/);
163         my $track=$self->{mp3tag}->{ID3v1}->title();
164         $del=0 if($track && $track =~ /\S+/);
165         my $tracknum=$self->{mp3tag}->{ID3v1}->track();
166         $del=0 if($tracknum && $tracknum !~ /^0+$/);
167         my $genre=$self->{mp3tag}->{ID3v1}->genre();
168         $del=0 if($genre && $genre =~ /\S+/);
169         my $comment=$self->{mp3tag}->{ID3v1}->comment();
170         $del=0 if($comment && $comment =~ /\S+/);
171         my $year=$self->{mp3tag}->{ID3v1}->year();
172         $del=0 if($year && $year =~ /\S+/ && $year !~ /^0+$/);
173         if($del)
174         {
175             $self->{mp3tag}->{ID3v1}->remove_tag;
176         }
177         else
178         {
179             $self->{mp3tag}->{ID3v1}->write_tag;
180         }
181     }
182     if(exists($self->{mp3tag}->{ID3v2}))
183     {
184         my $frames=$self->{mp3tag}->{ID3v2}->get_frame_ids();
185         if($frames && scalar(keys(%$frames)))
186         {
187             $self->{mp3tag}->{ID3v2}->write_tag;
188         }
189         else
190         {
191             $self->{mp3tag}->{ID3v2}->remove_tag;
192         }
193     }
194 }
195
196 sub delete_artist   { shift->delete("artist");  }
197 sub delete_album    { shift->delete("album");   }
198 sub delete_track    { shift->delete("song");    }
199 sub delete_tracknum { shift->delete("track");   }
200 sub delete_year     { shift->delete("year");    }
201 sub delete_v1genre  { shift->delete("v1genre"); }
202 sub delete_comment  { shift->delete("comment"); }
203 sub delete_genre    { shift->delete("genre");   }
204
205 sub delete_tags
206 {
207     my($self, $tags, $delvals)=@_;
208     my $current=$self->tags();
209     my @current=split(/\s*,\s*/, $current);
210     my @tags=split(/\s*,\s*/, $tags);
211     my %hash=();
212     @hash{@current}=();
213     for my $tag (@tags)
214     {
215         delete($hash{$tag}) if(exists($hash{$tag}));
216         if($delvals)
217         {
218             my $base=($tag =~ /(.*?)\//)[0];
219             $base=$tag unless($base);
220             for my $curtag (keys %hash)
221             {
222                 delete($hash{$curtag}) if($curtag =~ /^$base\//);
223             }
224         }
225     }
226     my @tagsout=sort keys(%hash);
227     my $genre=join(', ', @tagsout);
228     if(length($genre))
229     {
230         return($self->set("genre", $genre));
231     }
232     else
233     {
234         return($self->delete_genre());
235     }
236 }
237
238 sub delete_all
239 {
240     my($self)=@_;
241     if(exists($self->{mp3tag}->{ID3v1}))
242     {
243         $self->{mp3tag}->{ID3v1}->remove_tag;
244     }
245     if(exists($self->{mp3tag}->{ID3v2}))
246     {
247         $self->{mp3tag}->{ID3v2}->remove_tag;
248     }
249 }
250
251 sub delete
252 {
253     my($self, $thing)=@_;
254
255     if(exists($self->{mp3tag}->{ID3v1}) && $thing ne "genre")
256     {
257         my $action=$thing;
258         $action="genre" if($action eq "v1genre");
259         if($action eq "track")
260         {
261             $self->{mp3tag}->{ID3v1}->track("00");
262         }
263         else
264         {
265             $self->{mp3tag}->{ID3v1}->$action(" ");
266         }
267     }
268
269     if(exists($self->{mp3tag}->{ID3v2}))
270     {
271         if($thing eq "artist")
272         {
273             $self->{mp3tag}->{ID3v2}->remove_frame("TPE1");
274             $self->{mp3tag}->{ID3v2}->remove_frame("TPE2");
275         }
276         elsif($thing eq "album")
277         {
278             $self->{mp3tag}->{ID3v2}->remove_frame("TALB");
279         }
280         elsif($thing eq "song")
281         {
282             $self->{mp3tag}->{ID3v2}->remove_frame("TIT2");
283         }
284         elsif($thing eq "track")
285         {
286             $self->{mp3tag}->{ID3v2}->remove_frame("TRCK");
287         }
288         elsif($thing eq "year")
289         {
290             $self->{mp3tag}->{ID3v2}->remove_frame("TYER");
291             $self->{mp3tag}->{ID3v2}->remove_frame("TDRC");
292         }
293         elsif($thing eq "comment")
294         {
295             $self->{mp3tag}->{ID3v2}->remove_frame("COMM");
296         }
297         elsif($thing eq "genre")
298         {
299             $self->{mp3tag}->{ID3v2}->remove_frame("TCON");
300         }
301     }
302 }
303
304 sub uniq
305 {
306     my ($self, @things)=@_;
307     my %hash=();
308     @hash{@things}=();
309     return(sort keys(%hash));
310 }
311
312 sub channels
313 {
314     my($self)=@_;
315     return undef unless($self->{mp3info});
316     return( ($self->{mp3info}->stereo()) ? 2 : 1 );
317 }
318
319 sub bitrate
320 {
321     my($self)=@_;
322     return undef unless($self->{mp3info});
323     return( int($self->{mp3info}->bitrate()) );
324 }
325
326 sub samplerate
327 {
328     my($self)=@_;
329     return undef unless($self->{mp3info});
330     return(int($self->{mp3info}->frequency() * 1000));
331 }
332
333
334 1;