#!/usr/bin/perl -w

###############################################################################
# This is mycopycover.pl v1.10 (Thu, 15 Feb 2007 01:06:41 +0100)
#
# Brought to you by Sébastien Phélep <seb@le-seb.org>.
# License: GPL
#
# This scripts copies Amarok's album covers to the album's directory.
# It is freely inspired from copycover-offline.py script by Aurelien Bompard.
#
# Main enhancements over Aurelien's 1.5 Python script are :
# - Supports Amarok's Advance File Tracking (needs Amarok >= 1.4.2)
# - Supports copying covers of sampler albums
# - Supports artist name and album title substitutions in cover filename
# - Creation of .directory entry where missing
# - Gets all the jobs done really fast
#
# Changes for version 1.10
# - One can specify albums to retrieve on the command line
# - Few cosmetic changes
#
# Try "mycopycover.pl -h" for command usage
###############################################################################

# Required packages ========================================================= #
use strict;
use Getopt::Std;
use Digest::MD5 qw(md5_hex);
use File::Basename;
use File::Copy;
use Locale::gettext;
use POSIX qw(setlocale);

# Constants ================================================================= #
my $COVERS_DIR = "$ENV{'HOME'}/.kde/share/apps/amarok/albumcovers/large";
my $PROGNAME = basename($0);
my $VERSION = "v1.10";
my $SCHEME_DEFAULT = "%a - %t.png";	# Default filename scheme
my $ARTIST_DEFAULT = "Various Artists";	# Default artist name for samplers

# Globals =================================================================== #
my %options = ();	# Will store command line options
my %albums = ();	# Will store album
my %devices = ();	# Will store devices
my %actions = ();	# Will keep trace of actions taken
my @filesystems = ();	# Will store mounted filesystems
my @results = ();	# Will store result of commands
my $cover = undef;	# Filename scheme of album covers
my $direntry = undef;	# Flag for directory entries creation
my $force = undef;	# Flag for overwriting of existing covers / directory entries
my $remove = undef;	# Flag for removal of cached covers once copied
my $test = undef;	# Flag for testing mode
my $various = undef;	# Name to use as artist for samplers
my $verbose = undef;	# Flag for verbose output
my $locwarning = undef;	# Flag for warning about locale issues
my $locale = undef;	# Will handle Amarok's locale
my $translated = undef;	# Translation for $ARTIST_DEFAULT in Amarok's locale

# Functions ================================================================= #

# Default handler for --help
sub HELP_MESSAGE
{
	usage(0);
}

# Default handler for --version
sub VERSION_MESSAGE
{
	usage(0);
}

# Command execution
sub execute
{
	# Get command
	my $command = shift;

	# Execute it
	my @results = `$command 2>&1`;

	# Check for return code
	if( ($?>>8) != 0 )
	{
		print STDERR "Something went wrong with the '$command' command :\n@results\n";
		exit(1);
	}

	# Return results
	return(@results);
}

# Command line options processing
sub init
{
	# Use getopts for processing command line options
	usage(1) unless( getopts("c:dhfrs:tvw",\%options) );

	# Need command usage ?
	usage(0) if($options{'h'});

	# All options must have been processed by getopts
	#usage(1,"what do you want me to do with '@ARGV' ?") if( @ARGV );

	# Initialize values
	$cover = $options{'c'} || $SCHEME_DEFAULT;	# Defaults to $SCHEME_DEFAULT filename scheme
	$direntry = $options{'d'} ? 0 : 1;		# Defaults to .directory entries creation
	$force = $options{'f'} ? 1 : 0;			# Defaults to no overwriting of existing files
	$remove = $options{'r'} ? 1 : 0;		# Defaults to no removal of copied covers from Amarok's cache
	$test = $options{'t'} ? 1 : 0;			# Defaults to really performing actions
	$various = $options{'s'} || $ARTIST_DEFAULT;	# Defaults to using $ARTIST_DEFAULT for samplers
	$verbose = $options{'v'} ? 1 : 0;		# Defaults to quiet mode
	$locwarning = $options{'w'} ? 0 : 1;		# Defaults to warn about locale issues

	# Print settings if verbose mode's on
	if( $verbose )
	{
		print STDOUT "Done with command line options parsing and settings initialization.\n";
		print STDOUT "\n";
		print STDOUT "Here are the settings that will be used during this run :\n";
		print STDOUT "- Cover filename scheme :\t\t\t\"$cover\"\n";
		print STDOUT "- Name to use for artists in samplers :\t\t\"$various\"\n";
		print STDOUT "- Create .directory entries :\t\t\t".($direntry ? "Yes" : "No")."\n";
		print STDOUT "- Remove cached covers once copied :\t\t".($remove ? "Yes" : "No")."\n";
		print STDOUT "- Overwrite existing covers / entries :\t\t".($force ? "Yes" : "No")."\n";
		print STDOUT "- Only simulate actions to be taken :\t\t".($test ? "Yes" : "No")."\n";
		print STDOUT "- Warn about possible locale problem :\t\t".($locwarning ? "Yes" : "No")."\n";
		print STDOUT "\n";
	}
}

# Script usage
sub usage
{
	# Get exit status
	my $status = shift;
	my $errmsg = shift;

	# We've been called with an error message.
	# Print it to STDERR with short usage
	if( $errmsg )
	{
		print STDERR "${PROGNAME}: $errmsg\n\n";
		print STDERR "Usage: ${PROGNAME} [-dfhrtv] [-c <name>] [-s <string>] [<key> ... <key>]<\n";
		print STDERR "Try '-h' for help.\n";
	}
	else
	{
		# Print full usage to STDOUT
		print STDOUT << "EOF";

${PROGNAME} - ${VERSION}
This script copies album covers found in Amarok's cache to the album directories.

Usage: ${PROGNAME} [-dfhrtvw] [-c <scheme>] [-s <string>] [<key> ... <key>]

  -c <scheme>	: use <scheme> as cover filename scheme (default: '%a - %t.png')
		  recognized jokers in <scheme> are :
		    '%a', which will be expanded to artist's name
		    '%t', which will be expanded to album's title
  -d		: do not create .directory entries
  -f		: force replacement of existing covers / .directory entries
  -h		: this (help) message
  -r		: remove covers from Amarok's cache once copied into albums' directories
  -s <string>	: use <string> as artist name for sampler albums (default: 'Various Artists')
  -t		: test mode, won't perform any action
  -v		: verbose output
  -w		: don't warn about possible problems with locale

  <key>	:	: (perl) regular expression used to match a particular album.
		  album keys are in ''artist - title'' format.
		  search is case insensitive.
EOF
	}

	# Exit with specified status
	exit($status);
}

# Locale initialization
sub init_locale
{
	# Use default locale
	setlocale(LC_MESSAGES, "");

	# Get translated string for $ARTIST_DEFAULT 
	textdomain("amarok");
	$translated = gettext($ARTIST_DEFAULT);
	print STDOUT "Will use '$translated' as locale translation for Amarok's '$ARTIST_DEFAULT' string.\n" if( $verbose );

	# Only english-speaking people should get an untranslated string
	if( $translated eq $ARTIST_DEFAULT )
	{
		my $locale_okay = 0;
		my $locale_set = 0;

		# Check for LANG, LANGUAGE, LC_MESSAGES or LC_ALL in environment
		foreach my $var ( "LANG", "LANGUAGE", "LC_MESSAGES", "LC_ALL" )
		{
			next unless( defined($ENV{$var}) );
			$locale_set++;
			$locale_okay = 1 if( grep(/^en(_\S+)*/,$ENV{$var}) );
		}

		if( $locale_set and not($locale_okay) and $locwarning )
		{
			print STDERR << "EOF" ;

*** Locale warning ************************************************************
Your environment seems to have a locale defined, but I wasn't able to find a 
translated string for '$ARTIST_DEFAULT', which is used by Amarok as the artist
name for sampler albums. This may lead to problems in finding covers for
sampler albums in Amarok's cache directory.

Sorry for the inconvenience for native english-speaking people.
You can use the '-w' switch to get rid of this warning.
*******************************************************************************

EOF
		}
	}
}
# Main starts here =========================================================== #

# Process command line options
init();

# Use dcop to list active apps, and check for Amarok's presence
@results = execute("dcop");
unless( grep(/^amarok/,@results) )
{
	# Can't find amarok in dcop apps list
	print STDERR "Amarok must be running. Please launch it and restart this script.\n";
	exit(1);
}
print STDOUT "Amarok's reachable via dcop, good !\n" if( $verbose );

# Check that we're running against Amarok version >= 1.4.2
@results = execute("dcop amarok player version");
unless( $results[0] ge "1.4.2" )
{
	# Version test failed
	print STDERR "You need AmaroK >= 1.4.2 to run this script. Please update.\n";
	exit(1);
}
print STDOUT "Amarok's version control passed : version ".chomp($results[0])." running, wee-aah !\n" if( $verbose );

# Try to get locale translation used by Amarok for the ${ARTIST_DEFAULT} string
init_locale();

# Get list of mounted filesystems
@filesystems = execute("mount");

# Get id and mountpoint for every devices Amarok knows about
print STDOUT "Querying for known devices ...\n" if( $verbose );
@results = execute("dcop amarok collection query \"select id,lastmountpoint from devices\"");
{
	my $id = undef;
	foreach ( @results )
	{
		my $data = $_;
		chomp($data);

		# Data to be read now : device's id
		unless( defined($id) )
		{
			$id = $data;
			next;
		}

		# We only need to bother about actually mounted filesystems
		if( grep(/\s+$data\s+/,@filesystems) )
		{
			# Data to be read now : device's mount point
			$devices{$id} = $data;
		}
		else
		{
			print STDOUT "Device '$data' does not seem to be mounted, will skip albums attached to it.\n" if( $verbose );
		}

		# Be prepared for next results
		undef($id);
	}
}

# Get title, artist, sampler status, device id and directory of every albums Amarok knows about
print STDOUT "Querying for known albums ...\n" if( $verbose );
@results = execute("dcop amarok collection query \"SELECT DISTINCT album.name, artist.name, tags.sampler, tags.deviceid, tags.dir FROM album, artist, tags WHERE album.id = tags.album AND artist.id = tags.artist\"");
{
	my ( $title, $artist, $sampler, $device, $relpath, $altname ) = undef;
	foreach ( @results )
	{
		my $data = $_;
		chomp($data);

		# Data to be read now : album's title
		unless( defined($title) )
		{
			$title = $data;
			next;
		}

		# Data to be read now : artist's name
		unless( defined($artist) )
		{
			$artist = $data;
			next;
		}

		# Data to be read now : sampler status' of the album
		unless( defined($sampler) )
		{
			$sampler = $data;

			# Is this a sampler album ?
			if( $sampler )
			{
				$altname = $translated;	# As known in Amarok's DB
				$artist = $various;	# New name, as chosen by user / default
			}
			next;
		}

		# Data to be read now : device's id
		unless( defined($device) )
		{
			$device = $data;
			next;
		}

		# We won't do anything unless device's id = -1 (root) or the device's mounted
		if( ($device == -1) or (defined($devices{$device})) )
		{
			# Data to be read now : album's relative path from device
			$relpath = $data;

			# Remove leading './' from directory name
			$relpath =~ s/^\.\///;

			# Hash keys in %albums are made of both the artist name and album's title.
			# This is because you can have albums from different artists that share the same title (at least, I do).
			my $key = "$artist - $title";

			# Default is to keep track of all albums
			my $to_process = 1;

			# Unless album key(s) were given on the command line
			if( scalar(@ARGV) > 0 )
			{
				$to_process = 0;

				# Look for a matching key pattern
				foreach my $search_pattern ( @ARGV )
				{
					if( $key =~ m/$search_pattern/i )
					{
						# Found, will process this album
						$to_process = 1;
						last;
					}	
				}
			}

			# Need to process this album ?
			if( $to_process )
			{
				# Yes, we can fill the hash with our data :
				# Album's artist name as known in Amarok's DB, and as wished by user, album title and full path to album's directory
				$albums{$key}->{'altname'} = $altname || $artist;
				$albums{$key}->{'artist'} = $artist;
				$albums{$key}->{'title'} = $title;
				$albums{$key}->{'directory'} = join('/',($devices{$device} || ""),$relpath);
			}
		}

		# Be prepared for next results
		undef($title);
		undef($artist);
		undef($sampler);
		undef($device);
		undef($relpath);
		undef($altname);
	}
}

# Initialize action counters
$actions{'copies_done'} = 0;
$actions{'entries_done'} = 0;
$actions{'removals_done'} = 0;
$actions{'copies_failed'} = 0;
$actions{'entries_failed'} = 0;
$actions{'removals_failed'} = 0;

# This flag will help grouping actions
my $did_something = 0;

# Sort albums found by artist/title
foreach my $key ( sort { $albums{$a}->{'artist'} cmp $albums{$b}->{'artist'} || $albums{$a}->{'title'} cmp $albums{$b}->{'title'} } keys(%albums) )
{
	# Print a new line if an action was done previously
	print STDOUT "\n" if( $did_something );
	$did_something = 0;

	# Filename of cover stored in Amarok's cache is a MD5 sum of the artist name and album's title
	my $cached = ${COVERS_DIR}."/".md5_hex( lc($albums{$key}->{'altname'}.$albums{$key}->{'title'} ) );
	print STDOUT "Cached cover for '$key' should be : '$cached'\n" if( $verbose );

	# Substitute jokers in filename scheme
	my $destfile = $cover;
	$destfile =~ s/%a/$albums{$key}->{'artist'}/g;
	$destfile =~ s/%t/$albums{$key}->{'title'}/g;

	# The cover will be copied into the album's directory
	$destfile = $albums{$key}->{'directory'}."/".$destfile;

	# Check for cover presence in Amarok's cache
	if( -f $cached )
	{
		print STDOUT "Found cover for '$key' : '$cached'.\n" if( $verbose );

		# Check for cover's existence in the album's directory
		if( (-f $destfile) and ! $force )
		{
			# File exists and we've not been told to overwrite it
			print STDOUT "File '$destfile' already exists, skipping.\n" if( $verbose );
		}
		else
		{
			print STDOUT "Copying cover for '$key' to '$destfile' ... ";
			$did_something = 1;
			if( $test )
			{
				# Test mode only, we won't do anything
				print STDOUT "NOT ";
			}
			else
			{
				# If file exists, we need to delete it first
				if( -f $destfile )
				{
					unless( unlink($destfile) )
					{
						# Failed, try with next album
						print STDOUT "failed !\nCannot delete existing file : $!\n";
						$actions{'copies_failed'}++;
						next;
					}
				}

				# Copy the file 
				unless( copy($cached,$destfile) )
				{
					# Failed, try with the next album
					print STDOUT "failed !\nCannot delete existing file : $!\n";
					$actions{'copies_failed'}++;
					next;
				}

				# Went fine
				$actions{'copies_done'}++;
			}
			print STDOUT "done.\n";

			# Does the user want us to remove the cache file ?
			if( $remove )
			{
				print STDOUT "Removing cover cache file '$cached' ... ";
				$did_something = 1;
				if( $test )
				{
					# Test mode only, we won't do anything
					print STDOUT "NOT ";
				}
				else
				{
					# No, go ahead
					unless( unlink($cached) )
					{
						# Failed, try with the next album
						print STDOUT "failed !\n";
						print STDERR "Unable to remove file '$cached' : $!\n";
						$actions{'removals_failed'}++;
						next;
					}

					# Went fine
					$actions{'removals_done'}++;
				}
				print STDOUT "done.\n";
			}
		}
	}
	else
	{
		print STDOUT "No cover found for '$key', skipping.\n" if( $verbose );
	}

	# Does the cover exist in the album's directory
	if( -f $destfile )
	{
		# Does the user want us to (re-)create the .directory entry ?
		if( $direntry )
		{
			my $entry = $albums{$key}->{'directory'}."/.directory";

			# Does the .directory entry exist ?
			if( -f $entry and !$force )
			{
				# File exists and we've not been told to overwrite it
				print STDOUT "File '$entry' already exists, skipping.\n" if( $verbose );
				next;
			}
			else
			{
				print STDOUT "Creating '$entry' ... ";
				$did_something = 1;
				if( $test )
				{
					# Running in test mode, we won't do anything
					print STDOUT "NOT ";
				}
				else
				{
					unless( open(DIRENTRY,">$entry") )
					{
						# Failed, try with the next album
						print STDOUT "failed !\n";
						print STDERR "Unable to open '$entry' for writing : $!\n";
						$actions{'entries_failed'}++;
						next;
					}

					print DIRENTRY "[Desktop Entry]\n";
					print DIRENTRY "Icon=./".basename($destfile)."\n";

					close(DIRENTRY);

					# Went fine
					$actions{'entries_done'}++;
				}
				print STDOUT "done.\n";
			}
		}
	}
	elsif( not($direntry) )
	{
		# Cover not found in the album's directory
		print STDOUT "File '$destfile' does not exist, skipping directory entry creation.\n" if( $verbose );
		next;
	}
}

# Print stats
my $copies_total = $actions{'copies_done'} + $actions{'copies_failed'};
my $removals_total = $actions{'removals_done'} + $actions{'removals_failed'};
my $entries_total = $actions{'entries_done'} + $actions{'entries_failed'};
if( $copies_total > 0 )
{
	print STDOUT "Successfuly copied ".$actions{'copies_done'}." out of ".$copies_total." album cover".($copies_total > 1 ? "s" : "").".\n";
}
elsif( ($removals_total + $entries_total) == 0 )
{
	print STDOUT "No new album cover ";
	print STDOUT "matching specified pattern(s) " if( scalar(@ARGV) > 0 );
	print STDOUT "found in Amarok's cache.\n";

}
if( $removals_total > 0 )
{
	print STDOUT "Successfuly removed ".$actions{'removals_done'}." out of ".$removals_total." album cover".($removals_total > 1 ? "s" : "").".\n";
}
elsif( $verbose || $remove )
{
	print STDOUT "No album covers were removed from Amarok's cache.\n";
}
if( $entries_total > 0 )
{
	print STDOUT "Successfuly created ".$actions{'entries_done'}." out of ".$entries_total." .directory entr".($entries_total > 1 ? "ies" : "y").".\n";
}
elsif( $verbose && not($direntry) )
{
	print STDOUT "No directory entries were created.\n";
}

# Compute exit code
my $status = ( $actions{'copies_failed'} + $actions{'removals_failed'} + $actions{'entries_failed'} ) ? 1 : 0;

# Back to the shell
exit($status);
