2b90b5dad1a2e84d94e89e98c46333aa25d1e83b
[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)=@_;
208     my $current=$self->tags();
209     my @current=split(/\s*,\s*/, $current);
210     my %hash=();
211     @hash{@current}=();
212     for my $tag (@tags)
213     {
214         delete($hash{$tag}) if(exists($hash{$tag}));
215     }
216     my @tagsout=sort keys(%hash);
217     my $genre=join(', ', @tagsout);
218     if(length($genre))
219     {
220         return($self->set("genre", $genre));
221     }
222     else
223     {
224         return($self->delete_genre());
225     }
226 }
227
228 sub delete_all
229 {
230     my($self)=@_;
231     if(exists($self->{mp3tag}->{ID3v1}))
232     {
233         $self->{mp3tag}->{ID3v1}->remove_tag;
234     }
235     if(exists($self->{mp3tag}->{ID3v2}))
236     {
237         $self->{mp3tag}->{ID3v2}->remove_tag;
238     }
239 }
240
241 sub delete
242 {
243     my($self, $thing)=@_;
244
245     if(exists($self->{mp3tag}->{ID3v1}) && $thing ne "genre")
246     {
247         my $action=$thing;
248         $action="genre" if($action eq "v1genre");
249         if($action eq "track")
250         {
251             $self->{mp3tag}->{ID3v1}->track("00");
252         }
253         else
254         {
255             $self->{mp3tag}->{ID3v1}->$action(" ");
256         }
257     }
258
259     if(exists($self->{mp3tag}->{ID3v2}))
260     {
261         if($thing eq "artist")
262         {
263             $self->{mp3tag}->{ID3v2}->remove_frame("TPE1");
264             $self->{mp3tag}->{ID3v2}->remove_frame("TPE2");
265         }
266         elsif($thing eq "album")
267         {
268             $self->{mp3tag}->{ID3v2}->remove_frame("TALB");
269         }
270         elsif($thing eq "song")
271         {
272             $self->{mp3tag}->{ID3v2}->remove_frame("TIT2");
273         }
274         elsif($thing eq "track")
275         {
276             $self->{mp3tag}->{ID3v2}->remove_frame("TRCK");
277         }
278         elsif($thing eq "year")
279         {
280             $self->{mp3tag}->{ID3v2}->remove_frame("TYER");
281             $self->{mp3tag}->{ID3v2}->remove_frame("TDRC");
282         }
283         elsif($thing eq "comment")
284         {
285             $self->{mp3tag}->{ID3v2}->remove_frame("COMM");
286         }
287         elsif($thing eq "genre")
288         {
289             $self->{mp3tag}->{ID3v2}->remove_frame("TCON");
290         }
291     }
292 }
293
294 sub uniq
295 {
296     my ($self, @things)=@_;
297     my %hash=();
298     @hash{@things}=();
299     return(sort keys(%hash));
300 }
301
302 sub channels
303 {
304     my($self)=@_;
305     return undef unless($self->{mp3info});
306     return( ($self->{mp3info}->stereo()) ? 2 : 1 );
307 }
308
309 sub bitrate
310 {
311     my($self)=@_;
312     return undef unless($self->{mp3info});
313     return( int($self->{mp3info}->bitrate()) );
314 }
315
316 sub samplerate
317 {
318     my($self)=@_;
319     return undef unless($self->{mp3info});
320     return(int($self->{mp3info}->frequency() * 1000));
321 }
322
323
324 1;