#!/usr/bin/perl -w

# Query iTunes (via AppleScript) for a song list.  
# Optionally change song ratings.
# 
# In short, this is a command-line interface for querying the iTunes
# library.  Why would you want such a thing when you can just use iTunes? 
# Answer: when you can't use iTunes.  For example, I listen to music at work
# on my iPod.  If I want to see a playlist, or more importantly, rate a song
# (my original purpose for writing this script), I can do so from an SSH
# session I have from work to my Mac at home. 
# 
# Copyright 2004, Mark T. Abbott

use strict;
use Getopt::Std;
require 'text_table.pm';
require 'tell_itunes.pm';
my $g_default_playlist = 'New Jukebox';


###  Begin main code.  ###

# Getopt likes its options to be the first thing in @ARGV, so
#  shift non-option patterns to the end of the list.
while ( @ARGV and $ARGV[0] !~ m/^-/ and grep { m/^-/ } @ARGV )
{
    push @ARGV, shift @ARGV;
}

# Parse the arguments. 
our ($opt_h, $opt_P, $opt_a, $opt_p, $opt_r, $opt_v);
getopts('hPv' . 'a:p:r:');
if ( $opt_h )
{
	print

<<HELP;
Usage:
  $0 [-h] [-P] [-v] [-p PLAYLIST] [-a ALBUM] [-r RATING] pattern1 pattern2...

	-h	print this Help
	-p	look within Playlist PLAYLIST (defaults to '$g_default_playlist')
	-a	look within Album ALBUM (shows all albums by default)
	-P	print a list of available Playlists and exit
	-r	set the Rating of the matched track to RATING (0-5)
	-v	Verbose output

Tracks within PLAYLIST which match all patterns 
(case insensitive) will be printed.

NB: all patterns are treated as regular expressions.  
HELP

	exit;
}
if ( $opt_P )
{
	my @playlists = &get_playlists;
	map { print "$_\n" } @playlists;
	exit;
}


my $playlist = $opt_p || $g_default_playlist;
my $new_rating = $opt_r;
my $verbose = $opt_v;
my $album = $opt_a;
my @patterns = @ARGV;
# End of command-line argument processing.  

# Define variables.  
my (@fields) = qw(name artist album size rating);
my (@print_fields) = qw(name artist album stars);
my $cols = &cols;
my %field_length;

# Get the playlist and filter it (with the user's patterns).  
my @tracks = &get_tracks($playlist, $album, @patterns);
my (@matched_tracks, @matched_track_hashes);
foreach my $t (@tracks)
{
	my %h = %{$t};
	#next	if	grep { $h{'name'} !~ m/$_/i } @patterns;
	#next	if	$albumpattern and $h{'album'} !~ m/$albumpattern/i;
	push @matched_tracks, [ map { $h{$_} } @print_fields ];
	push @matched_track_hashes, $t;
}

print &text_table(\@print_fields, @matched_tracks);

if ( defined $new_rating )
{
	my $n_tracks = scalar(@matched_track_hashes);
	if ( $n_tracks == 0 )
	{
		warn "No tracks to be rated.\n";
	}
	elsif ( $n_tracks != 1 )
	{
		warn "More than one track matched ($n_tracks); won't rate.\n";
	}
	else
	{
		my %h = %{$matched_track_hashes[0]};
		warn "Setting rating to $new_rating for track \"$h{'name'}\".\n";
		&rate_track($new_rating, $playlist, %h);
	}
}

exit;


###  Primary routines.  ###

sub get_playlists
{
	my $script =

<<SCRIPT;
set myString to ""
set myList to playlists
repeat with i from 1 to count of myList
        set thisName to name of item i of myList
        set myString to myString & thisName & return
end repeat
return myString
SCRIPT

	return &tell_itunes($script);
}

sub get_tracks
{
	my ($playlist, $album, @patterns) = @_;
	$playlist ||= 'Library';
	$album ||= '';
	my $patterns = join ' ', @patterns;
	my $for_string = $patterns? "for \"$patterns\"" : '';
	my $list_expression = $patterns? 
		"search playlist myPlaylist $for_string" :
		"tracks in playlist myPlaylist";

	my $if_album = $album? 
		'if album of thisTrack is myAlbum' : 
		'';
	my $endif_album = $album? 'end if' : '';

	my $sets = join "\n", 
		map { "\t\tset this$_ to $_ of thisTrack" }
			@fields;
	my $joined = join ' & "::" & ', map { "this$_" } @fields;
	my $trackscript =

<<SCRIPT;
set myPlaylist to "$playlist"
set myAlbum to "$album"
set myString to ""
set trackList to $list_expression
repeat with i from 1 to count of trackList
	set thisTrack to item i of trackList
	$if_album
$sets
	        set myString to myString & $joined & return
	$endif_album
end repeat
return myString
SCRIPT

	my @tracks = &tell_itunes( $trackscript );
	my @hashes = ();
	foreach my $t (@tracks)
	{
		my @values = split '::', $t;
		my %h;
		for (0..$#fields)
		{
			$h{$fields[$_]} = $values[$_];
		}
		$h{'stars'} = '*'x(int($h{'rating'}/20));
		push @hashes, \%h;
	}
	return @hashes;
}

sub rate_track
{
	my ($rating, $playlist, %h) = @_;
	$playlist ||= 'Library';
	$rating *= 20	if	$rating < 10;
	die "invalid rating $rating"
		unless	grep { $rating eq $_ } qw(0 20 40 60 80 100);

	# Quotes appear to be escaped in iTunes-provided
	#  strings.  Commenting this line out for now.
	#map { die "$_ ($h{$_}} contains quotes" if $h{$_} =~ m/"/ } keys %h;

	my $checks = join ' and ', 
		map { "$_ is \"$h{$_}\"" } 
		grep { $h{$_} !~ m/"/ } 
		sort @fields;
	my $track_description = 
		"some file track of playlist \"$playlist\" whose $checks";

	my $rscript =

<<RSCRIPT;
if exists ($track_description) then
	set theTrack to ($track_description)
	tell theTrack
		set rating to $rating
	end tell
end if
RSCRIPT

	return &tell_itunes($rscript);
}

