id3fs-tag -V: per-dir summaries
[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 ID3FS::AudioFile;
22 use MP3::Tag;
23 use MP3::Info;
24
25 sub new
26 {
27     my $proto=shift;
28     my $class=ref($proto) || $proto;
29     my $self={};
30     bless($self,$class);
31
32     $self->{path}=shift;
33     $self->{mp3tag}=MP3::Tag->new($self->{path});
34     $self->{mp3info}=MP3::Info->new($self->{path});
35     $self->get_tags();
36     $self->{tags}={};
37
38     return $self;
39 }
40
41 sub set
42 {
43     my ($self, $func, $value)=@_;
44     return $self->choose($func) unless($value);
45     unless(exists($self->{mp3tag}->{ID3v1}))
46     {
47         $self->{mp3tag}->new_tag("ID3v1");
48     }
49     unless(exists($self->{mp3tag}->{ID3v2}))
50     {
51         $self->{mp3tag}->new_tag("ID3v2");
52     }
53     my $method=$func . "_set";
54     $self->{mp3tag}->$method($value, 1);
55     return $value;
56 }
57
58 sub choose
59 {
60     my($self, $func)=@_;
61     my $thing=undef;
62     if(exists($self->{mp3tag}->{ID3v2}))
63     {
64         $thing=$self->{mp3tag}->{ID3v2}->$func();
65     }
66     if(exists($self->{mp3tag}->{ID3v1}) && (!defined($thing) || !length($thing)))
67     {
68         $thing=$self->{mp3tag}->{ID3v1}->$func();
69     }
70     return $thing;
71 }
72
73 sub year      { return(shift->set("year",    @_)); }
74 sub artist    { return(shift->set("artist",  @_)); }
75 sub album     { return(shift->set("album",   @_)); }
76 sub track     { return(shift->set("title",   @_)); }
77 sub tracknum  { return(shift->set("track",   @_)); }
78 sub comment   { return(shift->set("comment", @_)); }
79
80 sub audiotype { return "mp3";         }
81 sub haspic    { return undef;         } # NEXTVERSION
82
83 # we only set v2 genre
84 sub genre
85 {
86     my ($self, $value)=@_;
87     if($value)
88     {
89         if(exists($self->{mp3tag}->{ID3v2}))
90         {
91             $self->{mp3tag}->{ID3v2}->remove_frame("TCON");
92         }
93         else
94         {
95             $self->{mp3tag}->new_tag("ID3v2");
96         }
97         $self->{mp3tag}->{ID3v2}->add_frame("TCON", $value);
98
99     }
100     return($self->{mp3tag}->{ID3v2}->genre());
101 }
102
103 sub v1genre
104 {
105     my($self, $val)=@_;
106     if($val)
107     {
108         $self->{mp3tag}->new_tag("ID3v1") unless(defined($self->{mp3tag}->{ID3v1}));
109         $self->{mp3tag}->{ID3v1}->genre($val);
110         return $val;
111     }
112     my $genre=undef;
113     $genre=$self->{ID3v1}->genre() if(defined($self->{ID3v1}));
114     return $genre;
115 }
116
117 sub tags
118 {
119     my $self=shift;
120     return() unless(exists($self->{mp3tag}->{ID3v2}) && defined($self->{mp3tag}->{ID3v2}));
121     return($self->{mp3tag}->{ID3v2}->genre());
122 }
123
124 sub get_tags
125 {
126     my ($self)=@_;
127     # MP3::Tag->get_tags shows cryptic debug info via print when it finds
128     # an unhandled id3v2 version, in addition to the warning, so use
129     # select to send prints to /dev/null
130     my $oldout=undef;
131     if(open(NULL,">/dev/null"))
132     {
133         $oldout=select(NULL);
134     }
135     eval { $self->{mp3tag}->get_tags; };
136     warn("$self->{path}: $@\n") if($@);
137     if(defined($oldout))
138     {
139         select($oldout);
140         close(NULL);
141     }
142 }
143
144 sub add_tags
145 {
146     my($self, @tags)=@_;
147     my $existing=$self->tags();
148     my @existing=split(/\s*,\s*/, $existing) if($existing);
149     my @merged=ID3FS::AudioFile::uniq(@tags, @existing);
150     my $genre=join(', ', @merged);
151     return($self->set("genre", $genre));
152 }
153
154 sub write
155 {
156     my $self=shift;
157     if(exists($self->{mp3tag}->{ID3v1}))
158     {
159         my $del=1;
160         my $artist=$self->{mp3tag}->{ID3v1}->artist();
161         $del=0 if($artist && $artist =~ /\S+/);
162         my $album=$self->{mp3tag}->{ID3v1}->album();
163         $del=0 if($album && $album =~ /\S+/);
164         my $track=$self->{mp3tag}->{ID3v1}->title();
165         $del=0 if($track && $track =~ /\S+/);
166         my $tracknum=$self->{mp3tag}->{ID3v1}->track();
167         $del=0 if($tracknum && $tracknum !~ /^0+$/);
168         my $genre=$self->{mp3tag}->{ID3v1}->genre();
169         $del=0 if($genre && $genre =~ /\S+/);
170         my $comment=$self->{mp3tag}->{ID3v1}->comment();
171         $del=0 if($comment && $comment =~ /\S+/);
172         my $year=$self->{mp3tag}->{ID3v1}->year();
173         $del=0 if($year && $year =~ /\S+/ && $year !~ /^0+$/);
174         if($del)
175         {
176             $self->{mp3tag}->{ID3v1}->remove_tag;
177         }
178         else
179         {
180             $self->{mp3tag}->{ID3v1}->write_tag;
181         }
182     }
183     if(exists($self->{mp3tag}->{ID3v2}))
184     {
185         my $frames=$self->{mp3tag}->{ID3v2}->get_frame_ids();
186         if($frames && scalar(keys(%$frames)))
187         {
188             $self->{mp3tag}->{ID3v2}->write_tag;
189         }
190         else
191         {
192             $self->{mp3tag}->{ID3v2}->remove_tag;
193         }
194     }
195 }
196
197 sub delete_artist   { shift->delete("artist");  }
198 sub delete_album    { shift->delete("album");   }
199 sub delete_track    { shift->delete("song");    }
200 sub delete_tracknum { shift->delete("track");   }
201 sub delete_year     { shift->delete("year");    }
202 sub delete_v1genre  { shift->delete("v1genre"); }
203 sub delete_comment  { shift->delete("comment"); }
204 sub delete_genre    { shift->delete("genre");   }
205
206 sub delete_tags
207 {
208     my($self, $tags, $delvals)=@_;
209     my $current=$self->tags();
210     my @current=();
211     @current=split(/\s*,\s*/, $current) if($current);
212     my @tags=split(/\s*,\s*/, $tags);
213     my %hash=();
214     @hash{@current}=();
215     for my $tag (@tags)
216     {
217         delete($hash{$tag}) if(exists($hash{$tag}));
218         if($delvals)
219         {
220             my $base=($tag =~ /(.*?)\//)[0];
221             $base=$tag unless($base);
222             for my $curtag (keys %hash)
223             {
224                 delete($hash{$curtag}) if($curtag =~ /^$base\//);
225             }
226         }
227     }
228     my @tagsout=sort keys(%hash);
229     my $genre=join(', ', @tagsout);
230     if(length($genre))
231     {
232         return($self->set("genre", $genre));
233     }
234     else
235     {
236         return($self->delete_genre());
237     }
238 }
239
240 sub delete_all
241 {
242     my($self)=@_;
243     if(exists($self->{mp3tag}->{ID3v1}))
244     {
245         $self->{mp3tag}->{ID3v1}->remove_tag;
246     }
247     if(exists($self->{mp3tag}->{ID3v2}))
248     {
249         $self->{mp3tag}->{ID3v2}->remove_tag;
250     }
251 }
252
253 sub delete
254 {
255     my($self, $thing)=@_;
256
257     if(exists($self->{mp3tag}->{ID3v1}) && $thing ne "genre")
258     {
259         my $action=$thing;
260         $action="genre" if($action eq "v1genre");
261         if($action eq "track")
262         {
263             $self->{mp3tag}->{ID3v1}->track("00");
264         }
265         else
266         {
267             $self->{mp3tag}->{ID3v1}->$action(" ");
268         }
269     }
270
271     if(exists($self->{mp3tag}->{ID3v2}))
272     {
273         if($thing eq "artist")
274         {
275             $self->{mp3tag}->{ID3v2}->remove_frame("TPE1");
276             $self->{mp3tag}->{ID3v2}->remove_frame("TPE2");
277         }
278         elsif($thing eq "album")
279         {
280             $self->{mp3tag}->{ID3v2}->remove_frame("TALB");
281         }
282         elsif($thing eq "song")
283         {
284             $self->{mp3tag}->{ID3v2}->remove_frame("TIT2");
285         }
286         elsif($thing eq "track")
287         {
288             $self->{mp3tag}->{ID3v2}->remove_frame("TRCK");
289         }
290         elsif($thing eq "year")
291         {
292             $self->{mp3tag}->{ID3v2}->remove_frame("TYER");
293             $self->{mp3tag}->{ID3v2}->remove_frame("TDRC");
294         }
295         elsif($thing eq "comment")
296         {
297             $self->{mp3tag}->{ID3v2}->remove_frame("COMM");
298         }
299         elsif($thing eq "genre")
300         {
301             $self->{mp3tag}->{ID3v2}->remove_frame("TCON");
302         }
303     }
304 }
305
306
307 sub channels
308 {
309     my($self)=@_;
310     return undef unless($self->{mp3info});
311     return( ($self->{mp3info}->stereo()) ? 2 : 1 );
312 }
313
314 sub bitrate
315 {
316     my($self)=@_;
317     return undef unless($self->{mp3info});
318     return( int($self->{mp3info}->bitrate()) );
319 }
320
321 sub samplerate
322 {
323     my($self)=@_;
324     return undef unless($self->{mp3info});
325     return(int($self->{mp3info}->frequency() * 1000));
326 }
327
328
329 1;