0b1f20e5fa9d3cf2a1f3a2d4468b046c6d17cba5
[id3fs.git] / bin / id3fs-tag
1 #!/usr/bin/perl -w
2 #
3 # id3fs - a FUSE-based filesystem for browsing audio metadata
4 # Copyright (C) 2010  Ian Beckwith <ianb@erislabs.net>
5 #
6 # This program is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
10 #
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 # GNU General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
18
19 use lib '/home/ianb/projects/id3fs/id3fs/lib'; # FIXME: remove
20 use strict;
21 use Getopt::Long qw(Configure);
22 use File::Find;
23 use ID3FS::AudioFile;
24 use vars qw($me);
25 $me=($0=~/(?:.*\/)?(.*)/)[0];
26
27 my @extensions=qw(mp3); # FIXME:  flac ogg
28 my $verbose=0;
29 my $help=0;
30 my ($artist, $album, $track, $tracknum, $year, $v1genre, $comment,
31     $delete_artist, $delete_album, $delete_track, $delete_tracknum,
32     $delete_year, $delete_v1genre, $delete_comment, $delete_all,
33     $delete_genre, $genre, $add_tags, $delete_tags, $overwrite_tagvals,
34     $delete_tagvals);
35
36 Configure(qw(bundling no_ignore_case));
37 my $optret=GetOptions(
38     "verbose|v"                      => \$verbose,
39     "help|h"                         => \$help,
40     "artist|a=s"                     => \$artist,
41     "album|l=s"                      => \$album,
42     "song|s=s"                       => \$track,
43     "tracknum|n=s"                   => \$tracknum,
44     "year|y=i"                       => \$year,
45     "v1genre|1=s"                    => \$v1genre,
46     "comment|c=s"                    => \$comment,
47     "delete-artist|A"                => \$delete_artist,
48     "delete-album|L"                 => \$delete_album,
49     "delete-song|S"                  => \$delete_track,
50     "delete-tracknum|N"              => \$delete_tracknum,
51     "delete-year|Y"                  => \$delete_year,
52     "delete-v1genre|0"               => \$delete_v1genre,
53     "delete-comment|C"               => \$delete_comment,
54     "delete|delete-all|D"            => \$delete_all,
55     "delete-genre|delete-all-tags|G" => \$delete_genre,
56     "genre|g|replace-all-tags|R=s"   => \$genre,
57     "add-tags|tags|t=s"              => \$add_tags,
58     "overwrite-tagvals|tagvals|o=s"  => \$overwrite_tagvals,
59     "delete-tags|T=s"                => \$delete_tags,
60     "delete-tags-with-values|O=s"    => \$delete_tagvals,
61     );
62
63 usage() if(!@ARGV || !$optret || $help);
64
65 while(my $path=shift @ARGV)
66 {
67     unless(-e $path)
68     {
69         warn("$me: $path: not found\n");
70         next;
71     }
72     File::Find::find( {wanted => \&wanted, follow => 1, no_chdir => 1}, $path);
73 }
74
75 sub wanted
76 {
77     my $ext='';
78     if(/.*\.(.*)/) { $ext=lc($1); }
79     if(-f && scalar(grep({ $ext eq lc($_);} @extensions)))
80     {
81         my $file=ID3FS::AudioFile->new($_);
82         return unless($file);
83         my $changes=0;
84         $changes =  do_deletes($file);
85         $changes += do_adds($file);
86         if($changes)
87         {
88             do_write($file);
89         }
90         else
91         {
92             do_display($file);
93         }
94     }
95 }
96
97 sub do_deletes
98 {
99     my($file)=@_;
100     if($delete_all)
101     {
102         $file->delete_all();
103         # we don't want to save the tag if we've deleted it
104         return 0;
105     }
106     $file->delete_artist()   if($delete_artist);
107     $file->delete_album()    if($delete_album);
108     $file->delete_track()    if($delete_track);
109     $file->delete_tracknum() if($delete_tracknum);
110     $file->delete_year()     if($delete_year);
111     $file->delete_v1genre()  if($delete_v1genre);
112     $file->delete_comment()  if($delete_comment);
113     $file->delete_genre()    if($delete_genre || $genre);
114     $file->delete_tags($delete_tags, 0)       if($delete_tags);
115     $file->delete_tags($delete_tagvals, 1)    if($delete_tagvals);
116     $file->delete_tags($overwrite_tagvals, 1) if($overwrite_tagvals);
117
118     my $donesomething=($delete_artist   || $delete_album   || $delete_track   ||
119                        $delete_tracknum || $delete_year    || $delete_v1genre ||
120                        $delete_comment  || $delete_genre   || $delete_tags    ||
121                        $delete_tagvals  || defined($genre) || $overwrite_tagvals);
122     return($donesomething ? 1 : 0);
123 }
124
125 sub do_adds
126 {
127     my($file)=@_;
128     $file->artist($artist)     if($artist);
129     $file->album($album)       if($album);
130     $file->track($track)       if($track);
131     $file->tracknum($tracknum) if($tracknum);
132     $file->year($year)         if($year);
133     $file->v1genre($v1genre)   if($v1genre);
134     $file->comment($comment)   if($comment);
135     $file->add_tags($add_tags) if($add_tags);
136     $file->add_tags($genre)    if($genre);
137     $file->add_tags($overwrite_tagvals) if($overwrite_tagvals);
138
139     my $donesomething=(defined($artist)   || defined($album) ||
140                        defined($track)    || defined($tracknum) ||
141                        defined($year)     || defined($v1genre) ||
142                        defined($comment)  || defined($genre) ||
143                        defined($add_tags) || defined($overwrite_tagvals));
144     return( $donesomething ? 1 : 0 );
145 }
146
147 sub do_write
148 {
149     my($file)=@_;
150     $file->write();
151 }
152
153 sub do_display
154 {
155     my($file)=@_;
156     my $artist=$file->artist();
157     my $album=$file->album();
158     my $track=$file->track();
159     my $tracknum=$file->tracknum();
160     my $year=$file->year();
161     my $comment=$file->comment();
162     my $v1genre=$file->v1genre();
163     my @tags=$file->tags();
164     @tags = map { (ref($_) eq "ARRAY") ? join('/', grep {defined;} @{$_}) : $_; } @tags;
165     if($verbose)
166     {
167         print $file->path(), ":\n";
168         print "  tracknum: $tracknum\n" if($tracknum);
169         print "  artist: $artist\n"     if($artist);
170         print "  album: $album\n"       if($album);
171         print "  song: $track\n"        if($track);
172         print "  year: $year\n"         if($year);
173         print "  v1genre: $v1genre\n"   if($v1genre);
174         print "  comment: $comment\n"   if($comment);
175     }
176     else
177     {
178         my @fields=($file->path(), $tracknum, $artist, $album, $track,
179                     $year, $v1genre, $comment);
180         @fields=map { defined($_) ? $_ : ""; } @fields;
181         print join(':', @fields), "\n";
182     }
183     if(@tags)
184     {
185         if($verbose) { print "  tags: "; }
186 #       else         { print $file->path() . ":tags:"; }
187         else         { print "tags:"; }
188         print join(", ", @tags), "\n";
189     }
190 }
191
192 sub usage
193 {
194     die("Usage: $me [-vhALSNY0CDG] [-a ARTIST] [-l ALBUM] [-s SONG] [-n TRACKNUM] FILES...\n".
195         "       $me [-y YEAR] [-g GENRE] [-1 V1GENRE] [-c COMMENT] [--] FILES...\n".
196         "       $me [-t TAGS,TO,ADD] [-T TAGS,TO,DELETE]  FILES...\n".
197         "       $me [-r TAGS,TO,DELETE TAGS,TO,ADD] [-R TAGS,TO,OVERWRITE,WITH] FILES...\n".
198         "With no options, displays current info in tag\n".
199         "Options:\n".
200         " -a|--artist=ARTIST                    Set artist\n".
201         " -l|--album=ALBUM                      Set album\n".
202         " -s|--song=SONG                        Set song\n".
203         " -n|--tracknum=NUM                     Set tracknum\n".
204         " -y|--year=NUM                         Set year\n".
205         " -1|--v1genre=GENRE                    Set ID3v1 genre\n".
206         " -c|--comment=COMMENT                  Set comment\n".
207         " -A|--delete-artist                    Delete artist\n".
208         " -L|--delete-album                     Delete album\n".
209         " -S|--delete-song                      Delete song\n".
210         " -N|--delete-tracknum                  Delete tracknum\n".
211         " -Y|--delete-year                      Delete year\n".
212         " -0|--delete-v1genre                   Delete ID3v1 genre\n".
213         " -C|--delete-comment                   Delete comment\n".
214         " -D|--delete|delete-all                Delete entire ID3 tag\n".
215         " -G|--delete-genre|--delete-all-tags   Delete all tags stored in genre\n".
216         " -g|-R|replace-all-tags|--genre=GENRE  Replace all tags in genre tag\n".
217         " -t|--add-tags|tags=TAG1,TAG2          Add tags to genre tag, merging with existing ones\n".
218         " -T|--delete-tags=TAG1,TAG2            Delete tags from genre\n".
219         " -r|--replace-tags TAGS1 TAGS2         Replace TAGS1 in genre with TAGS2\n".
220         " -v|--verbose                          Verbose display\n".
221         " -h|--help                             This help\n".
222         " --                                    End of options\n");
223 }
224
225 __END__
226
227 =head1 NAME
228
229 id3fs-tag - Add files to id3fs index
230
231 =head1 SYNOPSIS
232
233 B<id3fs-tag> [B<-lvh>] S<[B<-d >I<basedir>]> S<[B<-f >I<dbpath>]> S<[B<-e >I<mp3,ogg,flac>]> [B<-->] [I<DIR>...]
234
235 =head1 DESCRIPTION
236
237 Extracts id3 tags from mp3 files (and comment tags from ogg and flac
238 files) and adds them to a sqlite database, ready for mounting
239 with L<id3fsd(8)>.
240
241 =head1 OPTIONS
242
243 =over 4
244
245 =item B<-l> | B<--list>
246
247 List tags in use in specified database.
248
249 =item S<B<-d >I<PATH>> | S<B<--dir=>I<PATH>>
250
251 Specify base directory of source files. All files will be indexed
252 relative to this point.
253
254 If not specified, defaults to the first non-option argument on the
255 command line. Note that to avoid ambiguities, if more than one
256 directory is specified on the command line, the base directory must
257 be specified explicitly.
258
259 All files indexed must be under the base directory.
260
261 =item S<B<-f >I<FILE>> | S<B<--database=>I<FILE>>
262
263 Database file to use. If not specified, defaults to
264 a hidden file called B<".id3fs"> under the base directory.
265
266 =item S<B<-e >I<EXT1,EXT2>> | S<B<--extensions=>I<EXT1,EXT2>>
267
268 File extensions to consider when indexing.
269 Defaults to B<.mp3>, B<.ogg> and B<.flac>.
270
271 =item B<-v>
272
273 Enable verbose operation.
274
275 =item B<-h>
276
277 Show a short help message.
278
279 =item B<-->
280
281 End of options.
282
283 =back
284
285 =head1 EXAMPLES
286
287 Index all files in the current directory:
288
289     id3fs-tag .
290
291 Index current directory, printing each subdirectory as it recurses
292 into it:
293
294     id3fs-tag -v .
295
296 Just index some sub-directories:
297
298     id3fs-tag -d . dir1 dir2
299
300 Store the database in a custom location:
301
302     id3fs-tag -f ~/.id3fs/index.sqlite .
303
304 Only index .mp3 and .flac files:
305
306     id3fs-tag -e mp3,flac .
307
308 =head1 BUGS
309
310 Please report any found to ianb@erislabs.net
311
312 =head1 SEE ALSO
313
314 L<id3fsd(8)>, L<MP3::Tag>, L<Audio::Flac::Header>, L<Ogg::Vorbis::Header>
315
316 =head1 AUTHOR
317
318 Ian Beckwith <ianb@erislabs.net>
319
320 Many thanks to Aubrey Stark-Toller for help wrangling SQL.
321
322 =head1 AVAILABILITY
323
324 The latest version can be found at:
325
326 L<http://erislabs.net/ianb/projects/id3fs/>
327
328 =head1 COPYRIGHT
329
330 Copyright (C) 2010  Ian Beckwith <ianb@erislabs.net>
331
332 This program is free software: you can redistribute it and/or modify
333 it under the terms of the GNU General Public License as published by
334 the Free Software Foundation, either version 3 of the License, or
335 (at your option) any later version.
336
337 This program is distributed in the hope that it will be useful,
338 but WITHOUT ANY WARRANTY; without even the implied warranty of
339 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
340 GNU General Public License for more details.
341
342 You should have received a copy of the GNU General Public License
343 along with this program.  If not, see <http://www.gnu.org/licenses/>.
344
345 =cut