Advanced iTunes Scripting with Libraries

Pre-Fab Scripting

In my previous columns in this series I’ve talked about ways of improving your scripts by using “pre-fab” pieces that you can use in more than one script. A good example of this was the sorting of your list of artists in iTunes so that you can present the user with a more friendly-looking list than if it were unsorted. This time we’ll be building a more complicated script using priority queues from my Nite Flite library.

The Problem

Like a lot of folks from the baby boom generation, I grew up on “album rock”. Some albums were made to be listened to as a cohesive whole. A good example of this would be Pink Floyd’s “Dark Side of the Moon” or Electric Light Orchestra’s “Time”. So I like to listen to entire albums sometimes, instead of just a random selection of tracks. And while you can certainly use iTunes to hunt up an entire album, the problem becomes more tricky when you’re trying to automate the filling of an iPod whose size isn’t large enough to swallow your entire iTunes library.

To further complicate matters, I like to rotate my music on the basis of “least recently played.” iTunes handles this natively for tracks, but can’t pull entire albums this way. For that, we need Applescript!

So, the problem before us is this: Select several albums that have not been played recently to fill up an iPod playlist of a given size. We also want to filter out unpopular albums (i.e. - the average rating of all tracks is below 3.5 stars) and any genres that we don’t want (country, TV Shows, etc.). A tall order, no?

First, let’s do a thumbnail sketch of the work ahead (See my article on Structured Programming:

–get album list from iTunes
–remove duplicates
–Let’s see if the album meets our standards
–Get the average rating of the album
–strip out undesirable tracks
–reorder albums by last played date
–set up playlist

Now that we have a roadmap, things don’t seem to difficult. We’ll also want the priority queue library, so we’ll load it first:


--load the library
set scriptPath to (path to scripts folder from user domain) & "Script Library:"
set priorityLib to load script ((scriptPath as text) & "priorityLib.scpt") as alias

And we’ll create a new priority queue:


--create a newpriQueue
copy priorityLib's newPriQueue to albumsPriQueue
set priorityLib's newPQItem to {priority:"", cargo:""}

And we need to get some user input, namely, how large a playlist to create. I’ve included definitions for kilobytes, megabytes, and gigabytes mostly for convenience:


--definitions
set kb to 1024
set mb to 1000 * kb
set gb to 1000 * mb
display dialog "How many gigabytes should I gather?" default answer "1.0"
set inputGB to text returned of the result
set NanoThreshold to (inputGB as real) * gb

And we have some definitions that we’ll need further on:


--more definitions
set albumRatingMin to 3.5 * 20 --iTunes uses 0 to 100 internally, not 1-5 stars, so each star is 20
set prohibitedGenres to {"Country", "video", "TV Shows", "Podcast", "Holiday", "Christmas", "Comedy"}
set allAlbumsList to {}

With all that housekeeping out of the way, now we can get down to business! We’ll start by getting a list of albums from iTunes.


--get list
tell application "iTunes"
	set albumList to album of file tracks of library playlist 1 whose podcast is false
	set tcount to count albumList
end tell

Since iTunes will return us a bunch of repeated album names, we’ll remove any duplicates.


--remove duplicates
repeat with i from 1 to tcount
	tell my albumList's item i to if it is not in my allAlbumsList and it ≠ "" then set end of my allAlbumsList to it
end repeat
display dialog "Processing " & (count of allAlbumsList) & " Albums" giving up after 5

Now we need to take the winnowed-down list and ask iTunes to fetch the tracks for each album so we can figure out the average rating for the album. If it’s below 3.5 stars, we don’t want to use it.


--Let's see if the album meets our standards
tell application "iTunes"
	repeat with anAlbum in allAlbumsList
		set albumTracks to (every file track of playlist 1 whose album is anAlbum)
		
		--Get the average rating of the album
		set numTracks to count albumTracks
		set albumRating to 0
		repeat with aTrack in the albumTracks
			set albumRating to albumRating + (rating of aTrack)
		end repeat
		set albumRating to albumRating / numTracks
--this much will not compile; continued below

We didn’t mention it earlier, but we also don’t want solitary tracks, so we’ll ask for only albums with more than one track. And we’ll now jump into actually choosing tracks and enqueueing them based on the oldest “last played” date of all tracks in the album. If any track doesn’t have a “last played” date, then we will assign the date it was added as its “last played” date.


--script continued from above
if albumRating > albumRatingMin and numTracks > 1 then
				
				set lastPlayed to (current date)
				set prohibited to false
				
				repeat with aTrack in albumTracks
					--strip out undesirable tracks
					if genre of aTrack is in prohibitedGenres then set prohibited to true
					if played date of aTrack is missing value then
						set lastPlayed to date added of aTrack
					else if (played date of aTrack) < lastPlayed then
						set lastPlayed to (played date of aTrack)
					end if
				end repeat

				--reorder albums by last played date	 	
				if not prohibited then
					-- if we've made it this far, add the album to the queue
					set albumsPriQueue to priorityLib's priEnqueue(albumsPriQueue, anAlbum, lastPlayed)
				end if
			end if
		end repeat
		--still not complete; more to come

We’re now able to remove items from the queue and build our playlist! If (God forbid!) we don’t wind up with ANY albums in our queue, we need to display a dialog to the user, otherwise, we’ll de-queue the albums one at a time and add them to the playlist until we reach our quota or we run out of albums. We also want to check to see if the playlist exists, and if it does, clear it before we begin filling it. If it doesn’t exist yet, let’s create it:


--continuing
	if (priorityLib's priQueueIsEmpty(albumsPriQueue)) then
		display dialog "Empty Queue."
	else
		--set up playlist
		if exists user playlist "iPod Albums" then
			delete every file track of user playlist "iPod Albums"
		else
			make new user playlist with properties {name:"iPod Albums"}
		end if
		repeat while ((size of user playlist "iPod Albums") ≤ NanoThreshold) and (not (priorityLib's priQueueIsEmpty(albumsPriQueue)))
			set albumsPriQueue to priorityLib's priDequeue(albumsPriQueue)
			set theResult to priorityLib's priQueueResult
			set theAlbum to cargo of theResult
			display dialog "Adding album " & theAlbum giving up after 1
			duplicate (every file track of library playlist 1 whose album is theAlbum) to user playlist "iPod Albums"
		end repeat
	end if
end tell --done!

“And the rest”, as they say, “is history.” This script can take a while to run, if you have a lot of albums in your library. If you want to spiff up this script, you can add more user interaction and perhaps find places to cut corners and speed it up. This is a “brute force” method, but it gets the job done. I’ll leave you with the finished script, and (hopefully) the urge to polish this a bit for your own uses.

--load the library
set scriptPath to (path to scripts folder from user domain) & "Script Library:"
set priorityLib to load script ((scriptPath as text) & "priorityLib.scpt") as alias

--create a newpriQueue
copy priorityLib's newPriQueue to albumsPriQueue
set priorityLib's newPQItem to {priority:"", cargo:""}

set kb to 1024
set mb to 1000 * kb
set gb to 1000 * mb
display dialog "How many gigabytes should I gather?" default answer "1.0"
set inputGB to text returned of the result
set NanoThreshold to (inputGB as real) * gb

set albumRatingMin to 3.5 * 20 --iTunes uses 0 to 100 internally, not 1-5 stars, so each star is 20
set prohibitedGenres to {"Country", "video", "TV Shows", "Podcast", "Holiday", "Christmas", "Comedy"}
set allAlbumsList to {}

tell application "iTunes"
	set albumList to album of file tracks of library playlist 1 whose podcast is false
	set tcount to count albumList
end tell

--remove duplicates
repeat with i from 1 to tcount
	tell my albumList's item i to if it is not in my allAlbumsList and it ≠ "" then set end of my allAlbumsList to it
end repeat
display dialog "Processing " & (count of allAlbumsList) & " Albums" giving up after 5

--Let's see if the album meets our standards
tell application "iTunes"
	repeat with anAlbum in allAlbumsList
		set albumTracks to (every file track of playlist 1 whose album is anAlbum)
		
		--Get the average rating of the album
		set numTracks to count albumTracks
		set albumRating to 0
		repeat with aTrack in the albumTracks
			set albumRating to albumRating + (rating of aTrack)
		end repeat
		set albumRating to albumRating / numTracks
		
		if albumRating > albumRatingMin and numTracks > 1 then
			
			set lastPlayed to (current date)
			set prohibited to false
			
			repeat with aTrack in albumTracks
				--strip out undesirable tracks
				if genre of aTrack is in prohibitedGenres then set prohibited to true
				if played date of aTrack is missing value then
					set lastPlayed to date added of aTrack
				else if (played date of aTrack) < lastPlayed then
					set lastPlayed to (played date of aTrack)
				end if
			end repeat
			
			--reorder albums by last played date	 	
			if not prohibited then
				-- if we've made it this far, add the album to the queue
				set albumsPriQueue to priorityLib's priEnqueue(albumsPriQueue, anAlbum, lastPlayed)
			end if
		end if
	end repeat
	
	if (priorityLib's priQueueIsEmpty(albumsPriQueue)) then
		display dialog "Empty Queue."
	else
		--set up playlist
		if exists user playlist "iPod Albums" then
			delete every file track of user playlist "iPod Albums"
		else
			make new user playlist with properties {name:"iPod Albums"}
		end if
		repeat while ((size of user playlist "iPod Albums") ≤ NanoThreshold) and (not (priorityLib's priQueueIsEmpty(albumsPriQueue)))
			set albumsPriQueue to priorityLib's priDequeue(albumsPriQueue)
			set theResult to priorityLib's priQueueResult
			set theAlbum to cargo of theResult
			display dialog "Adding album " & theAlbum giving up after 1
			duplicate (every file track of library playlist 1 whose album is theAlbum) to user playlist "iPod Albums"
		end repeat
	end if
end tell