#!/usr/bin/perl -w

# Script to import FLAC and SNH archives (as found 
#  on archive.org) to iTunes.  
#
# Note that these archives are built by humans, so they
#  vary a lot.  This script can handle every archive I
#  have come across, but doubtless will choke on some
#  archives.  Please email me with details if it cannot
#  handle an archive you find: mark-abbott@earthlink.net.
# 
# Copyright 2004 by Mark T. Abbott.


###  Setup (includes, variable initialization).  ###

use strict;
require 'flac2itunes.pm';
require 'tell_itunes.pm';
use Getopt::Std;
our ($opt_h, $opt_t, $opt_d, $opt_f);
getopts('htf' . 'd:');
if ( $opt_h )
{
	print

<<HELP;
Usage: $0 [-h] [-t] [-d RECOVER_DIR] archive.zip

	-h	print this Help
	-t	Testing mode; do everything except import to iTunes 
		 (instead, prints the AppleScripts to be used)
	-d	recover from a previous run, using Directory RECOVER_DIR
	-f	Force import, even if we can't find or parse a metadata file
HELP

	exit;
}

my $g_testing = $opt_t;
my $g_recovering = $opt_d;
my $g_forcing	= $opt_f;
my ($g_d_temp, @g_f_wav);


###  Main.  ###

$g_d_temp = $g_recovering || &make_tempdir;
&expand_zipfile;
&decode_files;
my (@g_metadata) = &get_metadata;
&import_and_rename(@g_metadata);
exit;


###  Primary routines.  ###

sub make_tempdir
{
	my $d = sprintf '/tmp/flac2itunes-%d', $$;
	&fail("temp directory $d already exists")	if	-e $d;
	&log("making temp directory $d");
	mkdir $d	or	&fail("unable to mkdir $d: $!");
	return $d;
}

sub expand_zipfile
{
	return	if	$g_recovering and glob "$g_d_temp/*";
	my $f_input = $ARGV[0];
	&fail("not a zipfile: $f_input")	unless	$f_input =~ m#zip$#i;
	&log("expanding zipfile $f_input");
	my $unzip = &which('unzip');
	system($unzip, $f_input, '-d', $g_d_temp) == 0
		or	&fail("couldn't unzip $f_input with $unzip: $! [$?]");
}

sub decode_files
{
	foreach ( &find_glob($g_d_temp, '(flac|shn)$') )
	{
		my ($ext) = m#\.([^.]+)$#;
		my $f_wav = $_;
		$f_wav =~ s#$ext$#wav#i;
		next	if	$g_recovering and -e $f_wav;
		&log("decoding $ext file $_");
		if ( $ext eq 'flac' )
		{
			my $flac = &which('flac');
			system($flac, '-s', '-d', $_) == 0
				or	&fail("couldn't decode with $flac: $! [$?]");
		}
		elsif ( $ext eq 'shn' )
		{
			my $shorten = &which('shorten');
			system($shorten, '-x', $_) == 0
				or	&fail("couldn't decode with $shorten: $! [$?]");
		}
		else
		{
			&fail("don't know how to decode file type $ext for file $_");
		}
	}
}

sub get_metadata
{
	@g_f_wav = &find_glob($g_d_temp, 'wav$');
	my (@f_txt) = &find_glob($g_d_temp, 'txt$');
	my ($artist, $album, $n_tracks_ref, @tracks, @n_tracks, @discs);
	if ( scalar @f_txt == 1 )
	{
		# We found a text data file.

		($artist, $album, $n_tracks_ref, @tracks) = 
			&parse_metadata($f_txt[0]);
		@n_tracks = @{$n_tracks_ref};

		# Figure out which name goes with which WAV file.
		# Typically the filename format is like
		#  d1t13, or just t13 for single-disc albums.

		my $n_disc = 1;
		my $last_track = 0;
		foreach my $t (@tracks)
		{
			my ($n_track, $track) = @{$t};
			if ( $n_track < $last_track )
			{
				++$n_disc;
			}
			${$discs[$n_disc]}[$n_track] = $track;
			$last_track = $n_track;
		}

		map { s#"#\\"#g } ($artist, $album);
	}
	else
	{
		my $error = "can't find a unique .txt file in $g_d_temp; unable to determine metadata.";
		if ( $g_forcing )
		{
			&log($error);
			&log("importing files without metadata");
		}
		else
		{
			&fail("$error\nTo force import (without metadata), re-run with the -f flag.");
		}
	}

	return($artist, $album, \@tracks, \@n_tracks, \@discs);
}

sub import_and_rename
{
	my ($artist, $album, $r_tracks, $r_n_tracks, $r_discs) = @_;
	my @tracks	= @{$r_tracks};
	my @n_tracks	= @{$r_n_tracks};
	my @discs	= @{$r_discs};

	# Create the generic comment for these tracks.  
	my @clt = localtime($^T);
	my $comment = sprintf 
		'Imported on %4d-%02d-%02d using the command: %s',
		$clt[5]+1900,
		$clt[4]+1,
		$clt[3],
		join(' ', $0, @ARGV);

	my $script_pattern =

<<SCRIPT;
try
	with timeout of 1500 seconds
		convert POSIX file "\%s"
		set t_converted to (some file track of playlist "Library" whose name is "\%s")
		tell t_converted
\%s
\%s
			set comment to "$comment"
		end tell
	end timeout
end try
SCRIPT

	# Import the files, using the Applescript pattern defined above.

	TRACK:
	foreach my $f_wav (@g_f_wav)
	{
		# Examples:
		# dbr2002-07-19d3t09.wav
		$f_wav =~ m#(?:d(\d+))?t0?(\d+)#	or	&fail("no match for $f_wav");
		my ($disc, $track) = ($1, $2);
		$disc ||= 1;

		my @f_wav = split '/', $f_wav;
		my $imported_name = $f_wav[$#f_wav];
		$imported_name =~ s#.wav$##i;

		# If we were able to import track metadata, 
		#  add it to the import script.
		my $name_info = '';
		if ( @discs )
		{
			my $name = ${$discs[$disc]}[$track];
			if ($name)
			{
				$name =~ s#"#\\"#g;
				$name_info =

<<NAME_INFO;
			set artist to "$artist"
			set album to "$album"
			set name to "$name"
NAME_INFO

			}
			else
			{
				&log("no track name found for $f_wav");
				if ( $g_forcing )
				{
					&log("importing this track without metadata");
				}
				else
				{
					&log("To force import of this track (without metadata), re-run with the -f flag.");
					next TRACK;
				}
			}
		}

		my $disc_info =

<<DISC_INFO;
			set disc number to $disc
			set disc count to $#n_tracks
			set track number to $track
			set track count to $n_tracks[$disc]
DISC_INFO

		my $script = sprintf(
			$script_pattern, 
			$f_wav, 
			$imported_name, 
			$name_info, 
			$disc_info
		);

		if ( $g_testing )
		{
			&log("[testing mode] script to import $f_wav[$#f_wav]:");
			&log($script);
		}
		else
		{
			&log("importing $f_wav[$#f_wav]");
			&tell_itunes($script);
		}
	}
}


###  Secondary routines.  ###

sub fail
{
	die

<<DEATH;
The script failed with the following error:

@_

Please contact the author at mark-abbott\@earthlink.net
to report the error, including this error message.
DEATH
}

BEGIN
{
	# Private variable for &log()
	my $last_timestamp;

	sub log
	{
		my $time = time();
		my $elapsed = defined($last_timestamp)? 
			$time-$last_timestamp : '';
		$last_timestamp = $time;
		warn join("\t", $time-$^T, $elapsed, @_) . "\n";
	}
}

sub which
# Check for the location of an executable for the system() call.
{
	my ($cmd) = @_;
	my $env_path_org = $ENV{'PATH'};
	$ENV{'PATH'} .= ':/usr/local/bin';
	chomp (my $which = `which $cmd`);
	unless ( $which and -e($which) )
	{
		&fail(

<<FAIL);
Couldn't locate the executable for the command '$cmd'.
Either it is not installed, or its location is not in your PATH.
FAIL

	}
	$ENV{'PATH'} = $env_path_org;
	return $which;
}

sub find_glob
{
	my ($dir, $pattern) = @_;
	my @globbed;
	open FIND, "find $dir -type f |";
	while (<FIND>)
	{
		chomp;
		next	unless	m#$pattern#i;
		push @globbed, $_;
	}
	close FIND;
	return @globbed;
}


