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