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