| Chess | Tools | Data | Blog | Poetry | Why? | Wiki | Admin | Logout |

Download


#!/usr/bin/perl -w
# $Revision: 1.28 $
# $Date: 2006-12-15 18:28:24 $
#
# Luis Mondesi < lemsx1@gmail.com >
#
# DESCRIPTION: A simple script to rename Music files in a consistent manner
# USAGE: cd ~/Music; normalize_music.pl or simply: $0 --help
# LICENSE: GPL

use strict;
$|++;

my $revision='1.28'; # version

# standard Perl modules
use utf8;
use Getopt::Long;
Getopt::Long::Configure('bundling');
use File::Spec::Functions qw/ splitdir catdir catfile / ;  # abs2rel() and other dir/filename specific
use File::Find;     # find();
use File::Basename; # basename() && dirname()
#use FileHandle;     # for progressbar

my $got_ogg = 0;
my $got_mp3 = 0;

eval "use MP3::Tag";
if ( not $@ )
{
    $got_mp3 = 1;
}

eval "use Ogg::Vorbis::Header::PurePerl";
if ( not $@ )
{
    $got_ogg = 1;
}

die("We need Ogg::Vorbis::Header::PurePerl or MP3::Tag installed
") 
    if (not $got_ogg and not $got_mp3);

# Globals (no need to change any variables @see $0 --help)
my $MUSIC_FILES = '\.(mp3|ogg)$'; # files we will find
my @TAGS = ('song','track','artist','album');

my @ls=();

# allows for removing dirs
my @ls_dirs=();
my $longest_path = 0;

# Args:
my $PVERSION=0;
my $HELP=0;
my $DEBUG=0;
my $VERBOSE=0;
my $SHOW_DUPS=0;
my $REMOVE_EMPTY_DIRS=0; 
my $REMOVE_DUPS=0; 
my $FILE=undef;

# get options
GetOptions(
    # flags
    'v|version'             =>  \$PVERSION,
    'h|help'                =>  \$HELP,
    'D|debug'               =>  sub { $DEBUG++; $VERBOSE++; $SHOW_DUPS++; },
    'V|verbose'             =>  sub { $VERBOSE++; $SHOW_DUPS++; },
    'S|show-duplicates'     =>  \$SHOW_DUPS,
    'R|remove-empty-dirs'   =>  \$REMOVE_EMPTY_DIRS,
    'r|remove-duplicates'   =>  \$REMOVE_DUPS,
    # strings
    #'o|option=s'           =>  \$NEW_OPTION,
    # numbers
    #'a|another-option=i'   =>  \$NEW_ANOTHER_OPTION,
) and $FILE = shift;

if ( $HELP ) { 
    use Pod::Text;
    my $parser = Pod::Text->new (sentence => 0, width => 78);
    $parser->parse_from_file($0,\*STDOUT);
    exit 0;
}

if ( $PVERSION ) { print STDOUT ($revision,"
"); exit 0; }

# main
umask(0022); # fix anal permissions

if ( defined ($FILE) and $FILE !~ /^\s*$/ )
{
    die ("No such file $FILE
") if ( not -f $FILE );
    my $err = _rename($FILE);
    print STDERR ("$err
") if ( $VERBOSE );
    # if we were passed more files from the command line, do those as well:
    foreach ( @ARGV )
    {
        next if ( ! -f $_ );
        my $err = _rename($_);
        print STDERR ("$err
") if ( $VERBOSE );
    }
} else {
    # if we were passed a directory name, use it, else defaults to current directory
    my $_root = ( $FILE !~ /^\s*$/ and -d $FILE ) ? $FILE : ".";
    # are we running from Nautilus?
    # Get Nautilus current working directory, if under Natilus:
    if ( exists $ENV{'NAUTILUS_SCRIPT_CURRENT_URI'} 
        and $ENV{'NAUTILUS_SCRIPT_CURRENT_URI'} =~ m#^file:///# ) 
    {
        $_root = $ENV{'NAUTILUS_SCRIPT_CURRENT_URI'};
        $_root =~ s#%([0-9A-Fa-f]{2})#chr(hex($1))#ge; # fixes %20 and other URL thingies
        $_root =~ s#^file://##g;
    }
    chdir($_root) or die ("Could not change to directory $_root. $!
"); 
    my $aryref = do_file_ary(".");
    foreach(@$aryref)
    {
        my $err = _rename($_);
        print STDOUT ($err,"
") if ( $DEBUG or $VERBOSE );
    }
    _remove_empty_dirs() if ($REMOVE_EMPTY_DIRS > 0);
}

# support functions
sub do_file_ary {
    # uses find() to recur thru directories
    # returns an array of files
    # i.e. in directory "a" with the files:
    # /a/file.txt
    # /a/b/file-b.txt
    # /a/b/c/file-c.txt
    # /a/b2/c2/file-c2.txt
    # 
    # my $aryref = do_file_ary(".");
    # 
    # will yield:
    # a/file.txt
    # a/b/file-b.txt
    # a/b/c/file-c.txt
    # a/b2/c2/file-c2.txt
    # 
    my $ROOT = shift;
    my %opt = (wanted => \&process_file, no_chdir=>1);
    
    find(\%opt,$ROOT);
    return \@ls;
}

sub process_file {
    # remove empty dirs before we rename files (in case there is no files in this)
    if ($REMOVE_EMPTY_DIRS > 0 and -d $_)
    {
        my @_dirs = splitdir($_);
        $longest_path = ($#_dirs > $longest_path) ? ($#_dirs+1) : $longest_path;
        push(@ls_dirs,$_);
        return;
    }
    push (@ls,$_) if ( $_ =~ m($MUSIC_FILES)i and -f $_ );
}

sub _remove_empty_dirs
{
    # removes all empty directories from our list of dirs
    # you have to do $longest_path number of passes using rmdir() 
    # to get them all
    for (my $i=0;$i<$longest_path;$i++)
    {
        for (my $j=0;$j<$#ls_dirs;$j++)
        {
            warn("removing dir $ls_dirs[$j]
") if ($DEBUG);
            rmdir($ls_dirs[$j]) and
            splice(@ls_dirs,$j,1); # remove this item from our array
        }
    }
}

sub _mkdir
{
    # @desc implements `mkdir -p`
    my $path = shift;
    my $root = ( $path =~ m,^([/|\\|:]), ) ? $1 : ""; # relative or full path?
    my @dirs = splitdir($path);
    my $last = "";
    my $flag=1;
    foreach (@dirs)
    {
        next if ( $_ =~ m/^\s*$/ );
        $last = ( $flag > 1 ) ? catdir($last,$_) : "$root"."$_" ;
        mkdir ($last) if ( ! -d $last);
        $flag++;
    }
    return $flag; # number of directories created
}

sub _rename
{
    my $orig_filename=shift;
    my ($_name,$_path,$_suffix) = fileparse($orig_filename,('\.mp3','\.ogg'));
    my $hashref = undef;
    # deal with ogg files
    if ( $_suffix eq ".ogg" )
    {
        my $ogg = Ogg::Vorbis::Header::PurePerl->new($orig_filename);
        foreach my $k ($ogg->comment_tags) {
            foreach my $v ( $ogg->comment($k) )
            {
                $hashref->{lc($k)} = $v;
                $hashref->{'song'} = $v if ( lc($k) eq 'title' );
            }
        } 
    } elsif ( $_suffix eq ".mp3" ) {
        # deal with mp3 files
        no warnings;
        my $mp3 = MP3::Tag->new($orig_filename);
        $hashref = $mp3->autoinfo();
    } else {
        print STDERR ("Unknown fileformat for $orig_filename
");
        return "";
    }
    print STDOUT ("_"x69,"
") if ( $VERBOSE );
    print STDOUT ("file\t$orig_filename
") if ( $VERBOSE );
    if ( $DEBUG )
    {
        use Data::Dumper;
        print Dumper($hashref);
    }
    # tracks,artist,album are not that essential:
    #'song','track','artist','album'
    if ( ! defined($hashref->{'track'}) or $hashref->{'track'} =~ m/^\s*$/ )
    {
        $hashref->{'track'}="00/00";
    }
    if ( ! defined($hashref->{'artist'}) or $hashref->{'artist'} =~ m/^\s*$/ )
    {
        $hashref->{'artist'} = "noartist";
    }
    if ( ! defined($hashref->{'album'}) or $hashref->{'album'} =~ m/^\s*$/ )
    {
        $hashref->{'album'} = "noalbum";
    }
    foreach(@TAGS)
    {
        return "
*** $_ tag missing for file '$orig_filename'
Bailing out
" 
            if ( ! defined($hashref->{$_}) or $hashref->{$_} =~ m/^\s*$/ );
        # clean chars that might not be good for filenames
        #?|?|?|?|?|?|
        $hashref->{$_} =~ s/([^[:alnum:]\!\@\*\#\%\(\)\[\]\_\-\:\,\.\'\"\{\}\=\+ ])//gi;
        print STDOUT ($_, "\t", $hashref->{$_}, "
") if ( $VERBOSE );
    }
    my ($track,$garbage) = split(/\//,$hashref->{'track'});
    $track =~ s/^(\d{1,2}).*$/$1/g;
    my $path = lc( catdir($hashref->{'artist'},$hashref->{'album'}) );
    my $new_filename = lc( catfile($path,$track."-".$hashref->{'song'}.$_suffix) );
    print STDOUT ("to file\t$new_filename
") if ( $VERBOSE );
    # silently bail out if we have done this file before
    $orig_filename =~ s,^(\./|\.),,;
    if ( $new_filename eq $orig_filename )
    {
        print STDERR ("Skipping filename: same file ...
") if ($DEBUG);
        return "";
    }
    if ( -f $new_filename )
    {
        print STDERR ("New filename already exist ...
") if ($DEBUG);
        print STDOUT ("DUP:\t$orig_filename\t$new_filename
") if ( $SHOW_DUPS );
        unlink($orig_filename) if ( $REMOVE_DUPS );
        return "";
    }
    # make the path where we will be putting our new_filename
    _mkdir($path) if ( ! -d $path );
    if ( ! rename ( $orig_filename,$new_filename ) )
    {
        print STDOUT ("Renaming $orig_filename to $new_filename failed. Do you have permissions to write in $path?
");
        return "";
    }
}

__END__

=head1 NAME

normalize_music.pl - a simple mp3/ogg file renaming script

=head1 SYNOPSIS

B<normalize_music.pl>  [-v,--version]
                [-D,--debug] 
                [-h,--help]
                [-V,--verbose]
                [-S,--show-duplicates]
                [-R,--remove-empty-dirs]
                [-r,--remove-duplicates]
                [[directory] | [file1 [file2] [...]]]

=head1 DESCRIPTION 

This script finds all music files in a given directory and renames them according to the tags found in them. Renaming is consistent with iTunes naming convention with minor additions:
    Artist/Album/track-song_name-artist.$ext

If a directory name is given as argument, then it will rename music files found in that path, otherwise the current directory is used.

If a file name is given as argument, or more a series of files, only those files will be processed.

=head1 EXAMPLES

cd /path/to/dir && normalize_music.pl

normalize_music.pl /path/to/dir

normalize_music.pl file.ogg file.mp3 ...

=head1 OPTIONS

=over 8

=item -v,--version

prints version and exits

=item -D,--debug

enables debug mode

=item -h,--help

prints this help and exits

=item -V,--verbose

print all tags about each file

=item -S,--show-duplicates

print files which have the same id3 tags but on different locations. The lines will be prefix with "DUP:"

=item -R,--remove-empty-dirs

removes empty directories found in given path

=item -r,--remove-duplicates

removes duplicate files. This option implies --show-duplicates. Backup your files first. Be extra careful with this option! Use --show-duplicates to know what will be deleted.

=back

=head1 AUTHORS

Luis Mondesi <L<lemsx1@gmail.com>>

=cut


Advertisement