id3fs-tag: -V: summarize tags in directories
[id3fs.git] / bin / id3fs-tag
old mode 100644 (file)
new mode 100755 (executable)
index fbaf58d..6bff1c4
 use lib '/home/ianb/projects/id3fs/id3fs/lib'; # FIXME: remove
 use strict;
 use Getopt::Long qw(Configure);
+use File::Find;
 use ID3FS::AudioFile;
 use vars qw($me);
 $me=($0=~/(?:.*\/)?(.*)/)[0];
 
+my @extensions=qw(mp3); # FIXME:  flac ogg
+my (%argv_tags, %dir_tags, %file_tags);
+my $current_argv;
 my $verbose=0;
 my $help=0;
 my ($artist, $album, $track, $tracknum, $year, $v1genre, $comment,
     $delete_artist, $delete_album, $delete_track, $delete_tracknum,
     $delete_year, $delete_v1genre, $delete_comment, $delete_all,
-    $delete_genre, $genre, $add_tags, $delete_tags, @replace_tags);
+    $delete_genre, $genre, $add_tags, $delete_tags, $overwrite_tagvals,
+    $delete_tagvals, $tag_summary);
 
 Configure(qw(bundling no_ignore_case));
 my $optret=GetOptions(
-    "verbose|v"                    => \$verbose,
-    "help|h"                       => \$help,
-    "artist|a=s"                   => \$artist,
-    "album|l=s"                    => \$album,
-    "song|s=s"                     => \$track,
-    "tracknum|n=s"                 => \$tracknum,
-    "year|y=i"                     => \$year,
-    "v1genre|1=s"                  => \$v1genre,
-    "comment|c=s"                  => \$comment,
-    "delete-artist|A"              => \$delete_artist,
-    "delete-album|L"               => \$delete_album,
-    "delete-song|S"                => \$delete_track,
-    "delete-tracknum|N"            => \$delete_tracknum,
-    "delete-year|Y"                => \$delete_year,
-    "delete-v1genre|0"             => \$delete_v1genre,
-    "delete-comment|C"             => \$delete_comment,
-    "delete|delete-all|D"          => \$delete_all,
-    "delete-genre|G|delete-tags"   => \$delete_genre,
-    "genre|g|replace-all-tags|R=s" => \$genre,
-    "add-tags|tags|t=s"            => \$add_tags,
-    "delete-tags|T=s"              => \$delete_tags,
-    "replace-tags|r=s{2}"          => \@replace_tags,
+    "verbose|v"                      => \$verbose,
+    "help|h"                         => \$help,
+    "artist|a=s"                     => \$artist,
+    "album|l=s"                      => \$album,
+    "song|s=s"                       => \$track,
+    "tracknum|n=s"                   => \$tracknum,
+    "year|y=i"                       => \$year,
+    "v1genre|1=s"                    => \$v1genre,
+    "comment|c=s"                    => \$comment,
+    "delete-artist|A"                => \$delete_artist,
+    "delete-album|L"                 => \$delete_album,
+    "delete-song|S"                  => \$delete_track,
+    "delete-tracknum|N"              => \$delete_tracknum,
+    "delete-year|Y"                  => \$delete_year,
+    "delete-v1genre|0"               => \$delete_v1genre,
+    "delete-comment|C"               => \$delete_comment,
+    "delete|delete-all|D"            => \$delete_all,
+    "delete-genre|delete-all-tags|G" => \$delete_genre,
+    "genre|g|replace-all-tags|R=s"   => \$genre,
+    "add-tags|tags|t=s"              => \$add_tags,
+    "overwrite-tagvals|tagvals|o=s"  => \$overwrite_tagvals,
+    "delete-tags|T=s"                => \$delete_tags,
+    "delete-tags-with-values|O=s"    => \$delete_tagvals,
+    "summary|V"                      => \$tag_summary,
     );
 
 usage() if(!@ARGV || !$optret || $help);
 
-while(my $filename=shift @ARGV)
+while(my $path=shift @ARGV)
 {
-    unless(-f $filename)
+    unless(-e $path)
     {
-       warn("$me: $filename: not found\n");
+       warn("$me: $path: not found\n");
        next;
     }
-    my $file=ID3FS::AudioFile->new($filename);
-    my $changes=0;
-    $changes =  do_deletes($file);
-    $changes += do_adds($file);
-    if($changes)
-    {
-       do_write($file);
-    }
-    else
+    $current_argv=$path; # ick, global nastiness
+    File::Find::find( {wanted => \&wanted, follow => 1, no_chdir => 1}, $path);
+}
+
+summarize_tags() if($tag_summary);
+
+
+sub wanted
+{
+    my $ext='';
+    if(/.*\.(.*)/) { $ext=lc($1); }
+    if(-f && scalar(grep({ $ext eq lc($_);} @extensions)))
     {
-       do_display($file);
+       my $file=ID3FS::AudioFile->new($_);
+       return unless($file);
+       if($tag_summary)
+       {
+           gather_tags($_, $file);
+       }
+       else
+       {
+           my $changes=0;
+           $changes =  do_deletes($file);
+           $changes += do_adds($file);
+           if($changes)
+           {
+               do_write($file);
+           }
+           else
+           {
+               do_display($file);
+           }
+       }
     }
 }
 
 sub do_deletes
 {
     my($file)=@_;
-    $file->delete_artist()           if($delete_artist);
-    $file->delete_album()            if($delete_album);
-    $file->delete_track()            if($delete_track);
-    $file->delete_tracknum()         if($delete_tracknum);
-    $file->delete_year()             if($delete_year);
-    $file->delete_v1genre()          if($delete_v1genre);
-    $file->delete_comment()          if($delete_comment);
-    $file->delete_all()              if($delete_all);
-    $file->delete_genre()            if($delete_genre);
-    $file->delete_tags($delete_tags) if($delete_tags);
-    if(@replace_tags && $replace_tags[0])
+    if($delete_all)
     {
-       $file->delete_tags($replace_tags[0]);
+        $file->delete_all();
+       # we don't want to save the tag if we've deleted it
+       return 0;
     }
-
-    return($delete_artist   || $delete_album  || $delete_track   ||
-          $delete_tracknum || $delete_year   || $delete_v1genre ||
-          $delete_comment  || $delete_all    || $delete_genre   ||
-          $delete_tags     || (@replace_tags && $replace_tags[0]));
+    $file->delete_artist()   if($delete_artist);
+    $file->delete_album()    if($delete_album);
+    $file->delete_track()    if($delete_track);
+    $file->delete_tracknum() if($delete_tracknum);
+    $file->delete_year()     if($delete_year);
+    $file->delete_v1genre()  if($delete_v1genre);
+    $file->delete_comment()  if($delete_comment);
+    $file->delete_genre()    if($delete_genre || $genre);
+    $file->delete_tags($delete_tags, 0)       if($delete_tags);
+    $file->delete_tags($delete_tagvals, 1)    if($delete_tagvals);
+    $file->delete_tags($overwrite_tagvals, 1) if($overwrite_tagvals);
+
+    my $donesomething=($delete_artist   || $delete_album   || $delete_track   ||
+                      $delete_tracknum || $delete_year    || $delete_v1genre ||
+                      $delete_comment  || $delete_genre   || $delete_tags    ||
+                      $delete_tagvals  || defined($genre) || $overwrite_tagvals);
+    return($donesomething ? 1 : 0);
 }
 
 sub do_adds
@@ -113,26 +146,104 @@ sub do_adds
     $file->year($year)         if($year);
     $file->v1genre($v1genre)   if($v1genre);
     $file->comment($comment)   if($comment);
-    $file->genre($genre)       if($genre);
     $file->add_tags($add_tags) if($add_tags);
-    if(@replace_tags && $replace_tags[0])
-    {
-       $file->add_tags($replace_tags[1]);
-    }
-
-    return($artist  || $album || $track    || $tracknum || $year || $v1genre ||
-          $comment || $genre || $add_tags || (@replace_tags && $replace_tags[0]));
+    $file->add_tags($genre)    if($genre);
+    $file->add_tags($overwrite_tagvals) if($overwrite_tagvals);
+
+    my $donesomething=(defined($artist)   || defined($album) ||
+                      defined($track)    || defined($tracknum) ||
+                      defined($year)     || defined($v1genre) ||
+                      defined($comment)  || defined($genre) ||
+                      defined($add_tags) || defined($overwrite_tagvals));
+    return( $donesomething ? 1 : 0 );
 }
 
 sub do_write
 {
     my($file)=@_;
-    $file->write(); 
+    $file->write();
 }
 
 sub do_display
 {
     my($file)=@_;
+    my $artist=$file->artist();
+    my $album=$file->album();
+    my $track=$file->track();
+    my $tracknum=$file->tracknum();
+    my $year=$file->year();
+    my $comment=$file->comment();
+    my $v1genre=$file->v1genre();
+    my @tags=$file->tags();
+    @tags = map { (ref($_) eq "ARRAY") ? join('/', grep {defined;} @{$_}) : $_; } @tags;
+    if($verbose)
+    {
+       print $file->path(), ":\n";
+       print "  tracknum: $tracknum\n" if($tracknum);
+       print "  artist: $artist\n"     if($artist);
+       print "  album: $album\n"       if($album);
+       print "  song: $track\n"        if($track);
+       print "  year: $year\n"         if($year);
+       print "  v1genre: $v1genre\n"   if($v1genre);
+       print "  comment: $comment\n"   if($comment);
+    }
+    else
+    {
+       my @fields=($file->path(), $tracknum, $artist, $album, $track,
+                   $year, $v1genre, $comment);
+       @fields=map { defined($_) ? $_ : ""; } @fields;
+       print join(':', @fields), "\n";
+    }
+    if(@tags)
+    {
+       if($verbose) { print "  tags: "; }
+#      else         { print $file->path() . ":tags:"; }
+       else         { print "tags:"; }
+       print join(", ", @tags), "\n";
+    }
+}
+
+sub gather_tags
+{
+    my($path, $file)=@_;
+    my @tags=$file->tags();
+    @tags=map { join('/', grep { defined; } @$_); } @tags;
+    @tags=ID3FS::AudioFile::uniq(@tags);
+    $file_tags{$path}=\@tags;
+    my @argv_tags=();
+    @argv_tags=@{$argv_tags{$current_argv}} if($argv_tags{$current_argv});
+    $argv_tags{$current_argv}=[ ID3FS::AudioFile::uniq(@tags, @argv_tags) ];
+}
+
+sub summarize_tags
+{
+    my @all_tags=ID3FS::AudioFile::uniq(map { @$_; } values(%argv_tags));
+    # find common tags
+    my @common_tags=();
+OUTER: for my $tag (@all_tags)
+    {
+       for my $taglist (values(%argv_tags))
+       {
+           next OUTER unless(grep { $_ eq $tag; } @$taglist);
+       }
+       push(@common_tags, $tag);
+    }
+    print "ALL: ",    join(', ', @all_tags), "\n";
+    print "COMMON: ", join(', ', @common_tags), "\n";
+
+    use Data::Dumper;
+    # remove common tags from %argv_tags
+    for my $argv (keys(%argv_tags))
+    {
+       next unless(@{$argv_tags{$argv}});
+       $argv_tags{$argv}= [ ID3FS::AudioFile::list_remove(\@common_tags, $argv_tags{$argv}) ];
+    }
+
+    print "PER-DIR: \n";
+    for my $argv (keys(%argv_tags))
+    {
+       print "$argv: ", join(', ', @{$argv_tags{$argv}}), "\n";
+    }
 }
 
 sub usage
@@ -140,49 +251,43 @@ sub usage
     die("Usage: $me [-vhALSNY0CDG] [-a ARTIST] [-l ALBUM] [-s SONG] [-n TRACKNUM] FILES...\n".
        "       $me [-y YEAR] [-g GENRE] [-1 V1GENRE] [-c COMMENT] [--] FILES...\n".
        "       $me [-t TAGS,TO,ADD] [-T TAGS,TO,DELETE]  FILES...\n".
-       "       $me [-r TAGS,TO,DELETE, TAGS,TO,ADD] [-R TAGS,TO,OVERWRITE,WITH] FILES...\n".
-
-       "  verbose|v                 \n".
-       "  help|h                 \n".
-       "  artist|a=s                 \n".
-       "  album|l=s                 \n".
-       "  song|s=s                 \n".
-       "  tracknum|n=s                 \n".
-       "  year|y=i                 \n".
-       "  v1genre|1=s                 \n".
-       "  comment|c=s                 \n".
-       "  delete-artist|A                 \n".
-       "  delete-album|L                 \n".
-       "  delete-song|S                 \n".
-       "  delete-tracknum|N                 \n".
-       "  delete-year|Y                 \n".
-       "  delete-v1genre|0                 \n".
-       "  delete-comment|C                 \n".
-       "  delete|delete-all|D                 \n".
-       "  delete-genre|G|delete-tags                 \n".
-       "  genre|g|replace-all-tags|R=s                 \n".
-       "  add-tags|tags|t=s                 \n".
-       "  delete-tags|T=s                 \n".
-       "  replace-tags|r=s{2}                 \n".
-
-       " -d|--dir=PATH              Base directory of source files (default: ARGV[0])\n".
-       " -f|--database=FILE         Path to database file (default: basedir/.id3fs)\n".
-       " -e|--extensions=EXT1,EXT2  File extensions to index (default: mp3, ogg, flac)\n".
-       " -l|list                    List tags in use\n" .
-       " -v|--verbose               Verbose\n".
-       " -h|--help                  This help\n".
-       " --                         End of options\n");
+       "       $me [-r TAGS,TO,DELETE TAGS,TO,ADD] [-R TAGS,TO,OVERWRITE,WITH] FILES...\n".
+       "With no options, displays current info in tag\n".
+       "Options:\n".
+       " -a|--artist=ARTIST                    Set artist\n".
+       " -l|--album=ALBUM                      Set album\n".
+       " -s|--song=SONG                        Set song\n".
+       " -n|--tracknum=NUM                     Set tracknum\n".
+       " -y|--year=NUM                         Set year\n".
+       " -1|--v1genre=GENRE                    Set ID3v1 genre\n".
+       " -c|--comment=COMMENT                  Set comment\n".
+       " -A|--delete-artist                    Delete artist\n".
+       " -L|--delete-album                     Delete album\n".
+       " -S|--delete-song                      Delete song\n".
+       " -N|--delete-tracknum                  Delete tracknum\n".
+       " -Y|--delete-year                      Delete year\n".
+       " -0|--delete-v1genre                   Delete ID3v1 genre\n".
+       " -C|--delete-comment                   Delete comment\n".
+       " -D|--delete|delete-all                Delete entire ID3 tag\n".
+       " -G|--delete-genre|--delete-all-tags   Delete all tags stored in genre\n".
+       " -g|-R|replace-all-tags|--genre=GENRE  Replace all tags in genre tag\n".
+       " -t|--add-tags|tags=TAG1,TAG2          Add tags to genre tag, merging with existing ones\n".
+       " -T|--delete-tags=TAG1,TAG2            Delete tags from genre\n".
+       " -r|--replace-tags TAGS1 TAGS2         Replace TAGS1 in genre with TAGS2\n".
+       " -v|--verbose                          Verbose display\n".
+       " -h|--help                             This help\n".
+       " --                                    End of options\n");
 }
 
 __END__
 
 =head1 NAME
 
-id3fs-index - Add files to id3fs index
+id3fs-tag - Add files to id3fs index
 
 =head1 SYNOPSIS
 
-B<id3fs-index> [B<-lvh>] S<[B<-d >I<basedir>]> S<[B<-f >I<dbpath>]> S<[B<-e >I<mp3,ogg,flac>]> [B<-->] [I<DIR>...]
+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>...]
 
 =head1 DESCRIPTION
 
@@ -238,24 +343,24 @@ End of options.
 
 Index all files in the current directory:
 
-    id3fs-index .
+    id3fs-tag .
 
 Index current directory, printing each subdirectory as it recurses
 into it:
 
-    id3fs-index -v .
+    id3fs-tag -v .
 
 Just index some sub-directories:
 
-    id3fs-index -d . dir1 dir2
+    id3fs-tag -d . dir1 dir2
 
 Store the database in a custom location:
 
-    id3fs-index -f ~/.id3fs/index.sqlite .
+    id3fs-tag -f ~/.id3fs/index.sqlite .
 
 Only index .mp3 and .flac files:
 
-    id3fs-index -e mp3,flac .
+    id3fs-tag -e mp3,flac .
 
 =head1 BUGS