3 # id3fs - a FUSE-based filesystem for browsing audio metadata
4 # Copyright (C) 2010 Ian Beckwith <ianb@erislabs.net>
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.
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.
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/>.
19 use lib '/home/ianb/projects/id3fs/id3fs/lib'; # FIXME: remove
21 use Getopt::Long qw(Configure);
25 $me=($0=~/(?:.*\/)?(.*)/)[0];
27 my @extensions=qw(mp3); # FIXME: flac ogg
28 my (%argv_tags, %dir_tags, %file_tags);
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);
38 Configure(qw(bundling no_ignore_case));
39 my $optret=GetOptions(
40 "verbose|v" => \$verbose,
42 "artist|a=s" => \$artist,
43 "album|l=s" => \$album,
44 "song|s=s" => \$track,
45 "tracknum|n=s" => \$tracknum,
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,
66 usage() if(!@ARGV || !$optret || $help);
68 while(my $path=shift @ARGV)
72 warn("$me: $path: not found\n");
75 $current_argv=$path; # ick, global nastiness
76 File::Find::find( {wanted => \&wanted, follow => 1, no_chdir => 1}, $path);
79 summarize_tags() if($tag_summary);
85 if(/.*\.(.*)/) { $ext=lc($1); }
86 if(-f && scalar(grep({ $ext eq lc($_);} @extensions)))
88 my $file=ID3FS::AudioFile->new($_);
92 gather_tags($_, $file);
97 $changes = do_deletes($file);
98 $changes += do_adds($file);
117 # we don't want to save the tag if we've deleted it
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);
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);
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);
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 );
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;
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);
192 my @fields=($file->path(), $tracknum, $artist, $album, $track,
193 $year, $v1genre, $comment);
194 @fields=map { defined($_) ? $_ : ""; } @fields;
195 print join(':', @fields), "\n";
199 if($verbose) { print " tags: "; }
200 # else { print $file->path() . ":tags:"; }
201 else { print "tags:"; }
202 print join(", ", @tags), "\n";
209 my @tags=$file->tags();
210 @tags=map { join('/', grep { defined; } @$_); } @tags;
211 @tags=ID3FS::AudioFile::uniq(@tags);
212 $file_tags{$path}=\@tags;
214 @argv_tags=@{$argv_tags{$current_argv}} if($argv_tags{$current_argv});
215 $argv_tags{$current_argv}=[ ID3FS::AudioFile::uniq(@tags, @argv_tags) ];
220 my @all_tags=ID3FS::AudioFile::uniq(map { @$_; } values(%argv_tags));
223 OUTER: for my $tag (@all_tags)
225 for my $taglist (values(%argv_tags))
227 next OUTER unless(grep { $_ eq $tag; } @$taglist);
229 push(@common_tags, $tag);
231 print "ALL: ", join(', ', @all_tags), "\n";
232 print "COMMON: ", join(', ', @common_tags), "\n";
235 # remove common tags from %argv_tags
236 for my $argv (keys(%argv_tags))
238 next unless(@{$argv_tags{$argv}});
239 $argv_tags{$argv}= [ ID3FS::AudioFile::list_remove(\@common_tags, $argv_tags{$argv}) ];
243 for my $argv (keys(%argv_tags))
245 print "$argv: ", join(', ', @{$argv_tags{$argv}}), "\n";
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".
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");
287 id3fs-tag - Add files to id3fs index
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>...]
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
303 =item B<-l> | B<--list>
305 List tags in use in specified database.
307 =item S<B<-d >I<PATH>> | S<B<--dir=>I<PATH>>
309 Specify base directory of source files. All files will be indexed
310 relative to this point.
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.
317 All files indexed must be under the base directory.
319 =item S<B<-f >I<FILE>> | S<B<--database=>I<FILE>>
321 Database file to use. If not specified, defaults to
322 a hidden file called B<".id3fs"> under the base directory.
324 =item S<B<-e >I<EXT1,EXT2>> | S<B<--extensions=>I<EXT1,EXT2>>
326 File extensions to consider when indexing.
327 Defaults to B<.mp3>, B<.ogg> and B<.flac>.
331 Enable verbose operation.
335 Show a short help message.
345 Index all files in the current directory:
349 Index current directory, printing each subdirectory as it recurses
354 Just index some sub-directories:
356 id3fs-tag -d . dir1 dir2
358 Store the database in a custom location:
360 id3fs-tag -f ~/.id3fs/index.sqlite .
362 Only index .mp3 and .flac files:
364 id3fs-tag -e mp3,flac .
368 Please report any found to ianb@erislabs.net
372 L<id3fsd(8)>, L<MP3::Tag>, L<Audio::Flac::Header>, L<Ogg::Vorbis::Header>
376 Ian Beckwith <ianb@erislabs.net>
378 Many thanks to Aubrey Stark-Toller for help wrangling SQL.
382 The latest version can be found at:
384 L<http://erislabs.net/ianb/projects/id3fs/>
388 Copyright (C) 2010 Ian Beckwith <ianb@erislabs.net>
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.
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.
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/>.