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