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