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
31 my ($artist, $album, $track, $tracknum, $year, $v1genre, $comment,
32 $delete_artist, $delete_album, $delete_track, $delete_tracknum,
33 $delete_year, $delete_v1genre, $delete_comment, $delete_all,
34 $delete_genre, $genre, $add_tags, $delete_tags, $overwrite_tagvals,
35 $delete_tagvals, $tag_summary);
37 Configure(qw(bundling no_ignore_case));
38 my $optret=GetOptions(
39 "verbose|v" => \$verbose,
41 "artist|a=s" => \$artist,
42 "album|l=s" => \$album,
43 "song|s=s" => \$track,
44 "tracknum|n=s" => \$tracknum,
46 "v1genre|1=s" => \$v1genre,
47 "comment|c=s" => \$comment,
48 "delete-artist|A" => \$delete_artist,
49 "delete-album|L" => \$delete_album,
50 "delete-song|S" => \$delete_track,
51 "delete-tracknum|N" => \$delete_tracknum,
52 "delete-year|Y" => \$delete_year,
53 "delete-v1genre|0" => \$delete_v1genre,
54 "delete-comment|C" => \$delete_comment,
55 "delete|delete-all|D" => \$delete_all,
56 "delete-genre|delete-all-tags|G" => \$delete_genre,
57 "genre|g|replace-all-tags|R=s" => \$genre,
58 "add-tags|tags|t=s" => \$add_tags,
59 "overwrite-tagvals|tagvals|o=s" => \$overwrite_tagvals,
60 "delete-tags|T=s" => \$delete_tags,
61 "delete-tags-with-values|O=s" => \$delete_tagvals,
62 "summary|V" => \$tag_summary,
65 usage() if(!@ARGV || !$optret || $help);
67 while(my $path=shift @ARGV)
71 warn("$me: $path: not found\n");
74 File::Find::find( {wanted => \&wanted, follow => 1, no_chdir => 1}, $path);
77 summarize_tags() if($tag_summary);
83 if(/.*\.(.*)/) { $ext=lc($1); }
84 if(-f && scalar(grep({ $ext eq lc($_);} @extensions)))
86 my $file=ID3FS::AudioFile->new($_);
90 gather_tags($_, $file);
95 $changes = do_deletes($file);
96 $changes += do_adds($file);
115 # we don't want to save the tag if we've deleted it
118 $file->delete_artist() if($delete_artist);
119 $file->delete_album() if($delete_album);
120 $file->delete_track() if($delete_track);
121 $file->delete_tracknum() if($delete_tracknum);
122 $file->delete_year() if($delete_year);
123 $file->delete_v1genre() if($delete_v1genre);
124 $file->delete_comment() if($delete_comment);
125 $file->delete_genre() if($delete_genre || $genre);
126 $file->delete_tags($delete_tags, 0) if($delete_tags);
127 $file->delete_tags($delete_tagvals, 1) if($delete_tagvals);
128 $file->delete_tags($overwrite_tagvals, 1) if($overwrite_tagvals);
130 my $donesomething=($delete_artist || $delete_album || $delete_track ||
131 $delete_tracknum || $delete_year || $delete_v1genre ||
132 $delete_comment || $delete_genre || $delete_tags ||
133 $delete_tagvals || defined($genre) || $overwrite_tagvals);
134 return($donesomething ? 1 : 0);
140 $file->artist($artist) if($artist);
141 $file->album($album) if($album);
142 $file->track($track) if($track);
143 $file->tracknum($tracknum) if($tracknum);
144 $file->year($year) if($year);
145 $file->v1genre($v1genre) if($v1genre);
146 $file->comment($comment) if($comment);
147 $file->add_tags($add_tags) if($add_tags);
148 $file->add_tags($genre) if($genre);
149 $file->add_tags($overwrite_tagvals) if($overwrite_tagvals);
151 my $donesomething=(defined($artist) || defined($album) ||
152 defined($track) || defined($tracknum) ||
153 defined($year) || defined($v1genre) ||
154 defined($comment) || defined($genre) ||
155 defined($add_tags) || defined($overwrite_tagvals));
156 return( $donesomething ? 1 : 0 );
168 my $artist=$file->artist();
169 my $album=$file->album();
170 my $track=$file->track();
171 my $tracknum=$file->tracknum();
172 my $year=$file->year();
173 my $comment=$file->comment();
174 my $v1genre=$file->v1genre();
175 my @tags=$file->tags();
176 @tags = map { (ref($_) eq "ARRAY") ? join('/', grep {defined;} @{$_}) : $_; } @tags;
179 print $file->path(), ":\n";
180 print " tracknum: $tracknum\n" if($tracknum);
181 print " artist: $artist\n" if($artist);
182 print " album: $album\n" if($album);
183 print " song: $track\n" if($track);
184 print " year: $year\n" if($year);
185 print " v1genre: $v1genre\n" if($v1genre);
186 print " comment: $comment\n" if($comment);
190 my @fields=($file->path(), $tracknum, $artist, $album, $track,
191 $year, $v1genre, $comment);
192 @fields=map { defined($_) ? $_ : ""; } @fields;
193 print join(':', @fields), "\n";
197 if($verbose) { print " tags: "; }
198 # else { print $file->path() . ":tags:"; }
199 else { print "tags:"; }
200 print join(", ", @tags), "\n";
207 my @tags=$file->tags();
208 @tags=map { join('/', grep { defined; } @$_); } @tags;
209 @tags=ID3FS::AudioFile::uniq(@tags);
210 $file_tags{$path}=\@tags;
215 my @all_tags=ID3FS::AudioFile::uniq(map { @$_; } values(%file_tags));
217 # group tags by directories
219 my @dirs=map { s/(.*)\/.*/$1/; $_; } keys %file_tags;
220 @dirs=ID3FS::AudioFile::uniq(@dirs);
221 for my $tag (@all_tags)
223 DIR: for my $dir (@dirs)
225 for my $file (keys %file_tags)
227 next unless(@{$file_tags{$file}});
228 next unless($file =~ /^$dir/);
229 next DIR unless(grep { $_ eq $tag; } @{$file_tags{$file}});
231 push(@{$dir_tags{$dir}}, $tag);
235 # remove dir tags from files
236 for my $path (keys %dir_tags)
238 for my $file (keys %file_tags)
240 next unless($file =~ /^$path/);
241 $file_tags{$file} = [ ID3FS::AudioFile::list_remove(
243 $file_tags{$file}) ];
249 OUTER: for my $tag (@all_tags)
251 for my $taglist (values(%dir_tags))
253 next OUTER unless(grep { $_ eq $tag; } @$taglist);
255 push(@common_tags, $tag);
258 # remove common tags from %file_tags
259 for my $filename (keys(%file_tags))
261 next unless(@{$file_tags{$filename}});
262 $file_tags{$filename}= [ ID3FS::AudioFile::list_remove(\@common_tags,
263 $file_tags{$filename}) ];
266 # remove common tags from %dir_tags
267 for my $path (keys(%dir_tags))
269 next unless(@{$dir_tags{$path}});
270 $dir_tags{$path}= [ ID3FS::AudioFile::list_remove(\@common_tags,
274 # print "ALL: ", join(', ', @all_tags), "\n";
275 print "Common tags: ", join(', ', @common_tags), "\n";
276 # print "PER-DIR:\n";
277 for my $path (sort keys %dir_tags)
279 next unless(@{$dir_tags{$path}});
280 print "$path: ", join(', ', @{$dir_tags{$path}}), "\n";
283 # print "PER-FILE: \n";
284 for my $filename (keys(%file_tags))
286 next unless(@{$file_tags{$filename}});
287 print "$filename: ", join(', ', @{$file_tags{$filename}}), "\n";
293 die("Usage: $me [-vhALSNY0CDG] [-a ARTIST] [-l ALBUM] [-s SONG] [-n TRACKNUM] FILES...\n".
294 " $me [-y YEAR] [-g GENRE] [-1 V1GENRE] [-c COMMENT] [--] FILES...\n".
295 " $me [-t TAGS,TO,ADD] [-T TAGS,TO,DELETE] FILES...\n".
296 " $me [-r TAGS,TO,DELETE TAGS,TO,ADD] [-R TAGS,TO,OVERWRITE,WITH] FILES...\n".
297 "With no options, displays current info in tag\n".
299 " -a|--artist=ARTIST Set artist\n".
300 " -l|--album=ALBUM Set album\n".
301 " -s|--song=SONG Set song\n".
302 " -n|--tracknum=NUM Set tracknum\n".
303 " -y|--year=NUM Set year\n".
304 " -1|--v1genre=GENRE Set ID3v1 genre\n".
305 " -c|--comment=COMMENT Set comment\n".
306 " -A|--delete-artist Delete artist\n".
307 " -L|--delete-album Delete album\n".
308 " -S|--delete-song Delete song\n".
309 " -N|--delete-tracknum Delete tracknum\n".
310 " -Y|--delete-year Delete year\n".
311 " -0|--delete-v1genre Delete ID3v1 genre\n".
312 " -C|--delete-comment Delete comment\n".
313 " -D|--delete|delete-all Delete entire ID3 tag\n".
314 " -G|--delete-genre|--delete-all-tags Delete all tags stored in genre\n".
315 " -g|-R|replace-all-tags|--genre=GENRE Replace all tags in genre tag\n".
316 " -t|--add-tags|tags=TAG1,TAG2 Add tags to genre tag, merging with existing ones\n".
317 " -T|--delete-tags=TAG1,TAG2 Delete tags from genre\n".
318 " -r|--replace-tags TAGS1 TAGS2 Replace TAGS1 in genre with TAGS2\n".
319 " -V|--summary Summarize tags by directory\n" .
320 " -v|--verbose Verbose display\n".
321 " -h|--help This help\n".
322 " -- End of options\n");
329 id3fs-tag - Add files to id3fs index
333 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>...]
337 Extracts id3 tags from mp3 files (and comment tags from ogg and flac
338 files) and adds them to a sqlite database, ready for mounting
345 =item B<-l> | B<--list>
347 List tags in use in specified database.
349 =item S<B<-d >I<PATH>> | S<B<--dir=>I<PATH>>
351 Specify base directory of source files. All files will be indexed
352 relative to this point.
354 If not specified, defaults to the first non-option argument on the
355 command line. Note that to avoid ambiguities, if more than one
356 directory is specified on the command line, the base directory must
357 be specified explicitly.
359 All files indexed must be under the base directory.
361 =item S<B<-f >I<FILE>> | S<B<--database=>I<FILE>>
363 Database file to use. If not specified, defaults to
364 a hidden file called B<".id3fs"> under the base directory.
366 =item S<B<-e >I<EXT1,EXT2>> | S<B<--extensions=>I<EXT1,EXT2>>
368 File extensions to consider when indexing.
369 Defaults to B<.mp3>, B<.ogg> and B<.flac>.
373 Enable verbose operation.
377 Show a short help message.
387 Index all files in the current directory:
391 Index current directory, printing each subdirectory as it recurses
396 Just index some sub-directories:
398 id3fs-tag -d . dir1 dir2
400 Store the database in a custom location:
402 id3fs-tag -f ~/.id3fs/index.sqlite .
404 Only index .mp3 and .flac files:
406 id3fs-tag -e mp3,flac .
410 Please report any found to ianb@erislabs.net
414 L<id3fsd(8)>, L<MP3::Tag>, L<Audio::Flac::Header>, L<Ogg::Vorbis::Header>
418 Ian Beckwith <ianb@erislabs.net>
420 Many thanks to Aubrey Stark-Toller for help wrangling SQL.
424 The latest version can be found at:
426 L<http://erislabs.net/ianb/projects/id3fs/>
430 Copyright (C) 2010 Ian Beckwith <ianb@erislabs.net>
432 This program is free software: you can redistribute it and/or modify
433 it under the terms of the GNU General Public License as published by
434 the Free Software Foundation, either version 3 of the License, or
435 (at your option) any later version.
437 This program is distributed in the hope that it will be useful,
438 but WITHOUT ANY WARRANTY; without even the implied warranty of
439 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
440 GNU General Public License for more details.
442 You should have received a copy of the GNU General Public License
443 along with this program. If not, see <http://www.gnu.org/licenses/>.