#!/usr/bin/perl
# Filename:	mzmv
# Author:	David Madison
# Description:	Moves an mp3/ogg file according to it's tags
# Requires:  vorbiscomment for oggs
# Requires:  id3tool for mp3s
use strict;

##################################################
# Setup the variables
##################################################
my $PROGNAME = $0;
$PROGNAME =~ s|.*/||;

my $ILL_CHARS	= '~()!*?\"\'\/&\[\]:’';

my $DEFAULT_DEST = "%artist%/%year,-%%album%/%0.2d=track,-%%title%.%post%";

##################################################
# Usage
##################################################
sub usage {
	my $msg;
	foreach $msg (@_) { print "ERROR:  $msg\n"; }
	print <<USAGE;

Usage:\t$PROGNAME [-d] [-v] <song> ..
  Moves an mp3/ogg file according to it's tags
  -dest <fmt>  Specify destination format
  -v           Set verbose mode
  -d           Set debug mode
  -cp          Copy, not move
  -spaces      Don't convert spaces to '_'

  Destination Formats
  -------------------
    The destination format can have %keys% that are replaced by the tag values
	  Ex:  %artist%/%title%  ->  U2/So_Cruel

    Special formats:
      %post% is the postfix (mp3/ogg/..)
      %year% is the year (calculated from date tag for oggs)
      %bpm%  is set if found in the notes as "#bpm"
      %track% is guessed for mp3s if possible (from comments, etc..)

    You can specify sprintf formats for the tag:
    Ex:  %0.3d=track%-%title%  ->  006-So_Cruel

	  You can also specify a trailing string, only if the tag exists:
    Ex:  %track,-%%title%  ->  6-So_Cruel, Another_Song

	  Directories will be made as needed,
    and directories moved out of will be erased if empty.

  Default destination:
    $DEFAULT_DEST

USAGE
	exit -1;
}

sub parse_args {
	my @songs;
	my $dest = $DEFAULT_DEST;
	my $arg;
	while ($#ARGV>=0) {
		$arg=shift(@ARGV);
		if ($arg =~ /^-h$/) { usage(); }
		if ($arg =~ /^-dest$/) { $dest = shift @ARGV; next; }
		if ($arg =~ /^-spaces$/) { $MAIN::SPACES = 1; next; }
		if ($arg =~ /^-cp$/) { $MAIN::COPY = 1; next; }
		if ($arg =~ /^-d$/) { $MAIN::DEBUG=1; next; }
		if ($arg =~ /^-v$/) { $MAIN::VERBOSE=1; next; }
		if ($arg =~ /^-/) { usage("Unknown option: $arg"); }
		push(@songs,$arg);
	}
	usage("No songs defined") if (!@songs);
	
	($dest,@songs);
}

sub clean {
	my ($str) = @_;
	
	$str =~ s/\s+$//;
	$str =~ s|/|-|g;
	$str =~ s/[$ILL_CHARS]/_/g;
	$MAIN::SPACES ? ($str =~ s/\s+/ /g) : ($str =~ s/\s+/_/g);
	$str =~ s/_+/_/g;
	$str =~ s/_+$//; $str =~ s/^_+//;
	$str =~ s/\-+/\-/g;
	$str =~ s/\-+$//; $str =~ s/^\-+//;
	$str =~ s/è/e/g;
	$str =~ s/ü/u/g;
	$str;
}

##################################################
# Song info
##################################################
sub ogg_info {
	my ($ogg) = @_;

	my %i;
	# Get song tags
	open(TAGS,"vorbiscomment \Q$ogg\E |") || die("[$PROGNAME] Couldn't run vorbiscomment [$ogg]\n");
	($i{title},$i{artist},$i{album},$i{note}) = ("unknown","unknown","unknown");
	while(<TAGS>) {
		chomp;
		next if /^\s*$/;
		die("[$PROGNAME] Bad vorbiscomment: [$_]") unless /^(\S+)=(.*)$/;
		my ($key,$val) = (lc($1),clean($2));
		$i{$key} = $val;
	}
	close(TAGS);

	$i{year} = $i{date};
	$i{year} =~ s/(\d{4}).*/$1/;
	$i{track} = $i{tracknumber};
	
	\%i;
}

sub mp3_info {
	my ($mp3) = @_;

	my %i;
	# Get id3 info
	`which id3tool 2>&1 >/dev/null`;
	($? ? open(ID3,"id3v2 -l \Q$mp3\E |") : open(ID3,"id3tool \Q$mp3\E |")) || die("[$PROGNAME] Couldn't run id3tool [$mp3]\n");
	my $note;
	($i{title},$i{artist},$i{album},$i{note}) = ("unknown","unknown","unknown");
	while(<ID3>) {
		s/\s+$//;
		$i{title}=clean($2) if (/^(Song Title|TIT2[^:]+):\s+(\S.*)/);
		$i{artist}=clean($2) if (/^(Artist|TPE1[^:]+):\s+(\S.*)/);
		$i{album}=clean($2) if (/^(Album|TALB[^:]+):\s+(\S.*)/);
		$i{year}=clean($2) if (/^(Year|TYER[^:]+):\s+(\S.*)/);
		$i{track}=clean($1) if (/^Track:\s+(\S.*)/);
		$i{track}=clean($2) if (/^TRCK([^:]+):\s+(0*\d+)/);
		$i{comment}=$1 if (/^Note:\s+(\S.*)/);
	}
	close(ID3);
	
	# Hack - save the track number if ripped by mp3c and filename is <track>-..
	if ($i{comment} =~ /gen by.*mp3c/i && $mp3 =~ m|/(\d+)-[^/]+$|) {
		$i{track} = $1;
		#system("id3tool \Q$mp3\E -n 'Track $i{track}'");
		system("id3tool \Q$mp3\E -c $i{track}");
	} elsif ($i{comment} =~ /Track #?(\d+)/i) {
		$i{track} = $1;
	}
	\%i;
}

my %NOTAGWARN;
sub tagReplace {
	my ($tag, $i) = @_;
	my $format = ($tag =~ s/^([^=]+)=//) ? '%'.$1 : '%s';
	my $post = ($tag =~ s/,(.+)//) ? $1 : "";
	my $pre = ($tag =~ s/^(.+)<<//) ? $1 : "";	# Hack
	my $val = $i->{$tag};
	unless ($val) {
		print STDERR "[$PROGNAME] Warning:  No tag $tag [$i->{path}]\n"
			unless $NOTAGWARN{$tag}++;
		return "";
	}
	$pre.sprintf("$format", $val).$post;
}


sub dir {
	my ($path) = @_;
	return undef unless $path =~ m|/|;
	$path =~ s|/+$||;
	$path =~ s|/[^/]+$||;
	$path;
}

sub mkdirOf {
	my ($path) = @_;
	return if !defined $path || -d $path;
	mkdirOf(dir($path));
	mkdir($path)
}

sub rmdirR {
	my ($path) = @_;
	return unless defined $path && -d $path;
	return unless rmdir($path);
	rmdirR(dir($path));
}

##################################################
# Main code
##################################################
sub handle {
	my ($old,$new) = @_;
	$MAIN::COPY ?
		system("/bin/cp",$old,$new) :
		rename($old,$new);
}

sub main {
	my ($dest,@songs) = parse_args();
	
	foreach my $song ( @songs ) {
		if (! -f $song) {
			print STDERR "[$PROGNAME] Warning:  Couldn't find song [$song]\n";
			next;
		}

		#########################
		# Get the info
		#########################
		my $i;
		if ($song =~ /\.mp3$/) {
			$i = mp3_info($song);
			$i->{post} = "mp3";
		} elsif ($song =~ /\.ogg$/) {
			$i = ogg_info($song);
			$i->{post} = "ogg";
		} else {
			print STDERR "[$PROGNAME] Error:  Unknown song type [$song]\n";
			next;
		}

		if ($i->{artist} eq "unknown" || ($i->{title} eq "unknown" && $i->{album} eq "unknown")) {
			print STDERR "[$PROGNAME] Error:  No tags for song [$song]\n";
			#next;
		}
		print STDERR "[$PROGNAME] Warning:  Missing tag(s) for song [$song]\n"
			if ($i->{title} eq "unknown" || $i->{artist} eq "unknown" || $i->{album} eq "unknown");

		$i->{artist} =~ s/^The_//ig;
		$i->{path} = $song;
		$i->{bpm} = ($i->{comment} =~ /(\d+)\s?bpm/) ? $1 : undef;

		#########################
		# Calculate the path
		#########################

		my $new = $dest;
		while ($new =~ s/%([^%]+)%/tagReplace($1,$i)/eg) {}

		# Make the directories
		mkdirOf(dir($new));

		if (-f $new) {
			print STDERR "[$PROGNAME]  ERROR: $new already exists.\n";
			next;
		}

		#########################
		# Move it
		#########################
		print "$song -> $new\n" if $MAIN::VERBOSE || $MAIN::DEBUG;
		handle($song,$new) unless $MAIN::DEBUG;
		print STDERR "[$PROGNAME]  ERROR: $new already exists.\n"
			if $? && !$MAIN::DEBUG;
		
		# Remove the directory if it's empty
		rmdirR(dir($song));
	}
} main();
