Saturday, November 18, 2017
  • Index
  •  » unScripted
  •  » Improve your Applescripts: Return to iTunes Scripting

#1 2008-02-18 07:00:52 am

Kevin Bradley
Administrator
From:: Independence, MO
Registered: 2006-03-13
Posts: 548
Website

Improve your Applescripts: Return to iTunes Scripting

"Four and twenty blackbirds, baked in a pie."
In my previous column I had to eat my own words where Automator was concerned.  This month, apparently, I get to eat a dish of crow yet again, but on a different subject.  Stick with me and I'll show you the error of my ways and perhaps you can profit from my meal of bird feet and black feathers.

In March of last year, in a column entitled Advanced iTunes Scripting with Libraries, I talked about a script that I'd written to help fill my iPod Nano with entire albums by "last played date" instead of just individual songs, something that iTunes' smart playlists can't do.  The script made use of the priority queue library from my Nite Flite set of library scripts, and it got the job done, albeit a bit slowly.  It took almost 2 minutes  to generate the list of albums.  Since then, I've doubled the size of my iTunes library, so it was now taking over 6 minutes!

Over the last year, I've become increasingly annoyed at the length of time that this script takes to run.  I've pulled the script into Script Editor on several occasions, and while I was able to give it a slight tweak from time to time, there was no way I could avoid the Big "O" issues (see my previous column Sorting and The Big "O") that the script incurred by looping through the raw list of tracks to winnow down to just one instance of an album's name, then looping through the smaller album list, then looping through all the tracks of the album to get the earliest date of any track on the album within the larger loop, which multiplies the inner loop processing by the outer loop!  Beyond that, then enqueuing the album/date data (which required processing to sort the priority list in the separate library script) and subsequently dequeuing the data in the proper date order just couldn't be done as quickly as I wanted to do it.

No Single Servings, Please
Added to this difficulty are the 89 free iTunes singles I've downloaded that don't really count toward an album quota, various TV shows, movies, podcasts, and other flotsam and jetsam that I don't need in a first generation iPod Nano (but they will take up room in the playlists that you sync to the Nano, leaving your Nano with a bunch of leftover room).  Add the fact that at the time I wrote the script, I had to compute my own average album rating for each album and now iTunes supports album ratings, and you begin to see what I saw, and that is:  That this script needed a serious re-write.

In my efforts to tweak the script, at one point I removed the section that pulled all the iTunes' file tracks and replaced it with a more selective set of code.  Here's the original script:

Applescript:


--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 {}
(*begin*)
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
(*end*)
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

Notice the chunk of code between the (*begin*) and (*end*) tags.  I never liked this code, the second section is very unclear, but it works and it works fairly fast, so I put up with it.  But in thinking about reworking the script, I realized that I could limit my choices to:
<ul><li>1 track from an album - why not track 1? (I'll show you why later)</li><li>Tracks that acknowledged a track count > 1 (eliminates singles)</li><li>Tracks whose disc number was either 1 or 0 (keeps us from processing more than 1 disc of a mulit-disc set)</li><li>Tracks whose (new) album rating was greater than or equal to 60%</li><li>Tracks that were not members of unwanted categories (TV, movies, etc.)</li></ul>
Well, the original script still tested for prohibited genres after the initial selection, so the last item on the above list was pretty much handled - all that was necessary was to limit the tracks' kind to "audio."  So revising the section above between the begin and end tags rendered the somewhat improved version of the block:

Applescript:


--This is a code snippet, it won't run by itself!
   (*begin*)
   set allalbumList to album of file tracks of library playlist 1 whose (kind contains "audio" and track count > 1 and track number = 1 and (disc number = 1 or disc number = 0) and comment does not contain "omit")
   set tcount to count allalbumList
   (*end*)

This allowed me to remove the extra repeat that pared the list down to only a single occurrence of a given album.  However, this version of the script still takes a long time to execute if you have 100+ albums (about 1500 tracks) in your iTunes library.  The main reason is that it still loops through all the albums that are selected, then loops through each track of each album.  With an average of 12 tracks per album (and that's low for some double CD collections) and 100 albums, that requires 1200 iterations to find the lowest "last played date" for the albums in question.

Back to the Future
So, now back to the current rewrite.  It occurred to me (finally) that what was needed was iTunes' smart playlist ability to select tracks based on "last played date," a function that (obviously) can't be duplicated in Applescript without a lot of overhead. 

And then lightening struck.  Why not use a smart playlist to do the first half of the work for me?  Once iTunes has selected the tracks with the oldest "last played date," then I can process that list (as long as it is also sorted by "last played date").

The idea had a lot of allure.  Let the application I'm scripting do what it does best, then use Applescript to augment the application with something that the app can't do.  So I created a smart playlist in iTunes named "Overdue Albums" with the following criteria:

<center><img src="http://files.macscripter.net/unscripted/nw98autoimg/TNsmartlist.png"></center>
<center>Smart Playlist info for the "Overdue Albums" playlist</center>

I then rewrote the script from scratch, using the smart playlist as my source for the list of album tracks that hadn't been played recently.  The end result was a script that whipped together my list of albums for the Nano in less than a minute - about 45 seconds! Yep, you read that right.  This is less than 1/10th the time the old script took.

Here are three versions of the script, the old one, the slightly tweaked version, and the new script.  Note - I've added some code to time each script and show you the time it takes to generate the new playlist.  I've also edited the parameters of each one to try and get them to pull the same list of albums even though I've changed the parameters of my own scripts over the last year for various reasons, mostly personal preference.  More on this after the scripts:

Version 1 - The Original (runtime: 6:31)

Applescript:


set beginTime to (current date)
--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.0 * 20 --iTunes uses 0 to 100 internally, not 1-5 stars, so each star is 20
set prohibitedGenres to {"Podcast", "Standup Comedy"}
set allAlbumsList to {}
with timeout of 1000 seconds
   (*begin*)
   tell application "iTunes"
       set albumList to album of file tracks of library playlist 1 whose kind contains "audio" and comment does not contain "omit" and comment does not contain "single" and comment does not contain "christmas"
       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
   (*end*)
   display dialog "Processing " & (count of allAlbumsList) & " Albums" giving up after 1
   
   --Let's see if the album meets our standards
   tell application "iTunes"
       set itemCount to 0
       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 "Monday, January 1, 1900 12:00:00 AM"
                   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)
                   set itemCount to itemCount + 1
                   if itemCount mod 10 = 0 then display dialog "Processing item " & itemCount buttons {"OK"} default button 1 giving up after 1
                   
               end if
           end if
       end repeat
       
       if (priorityLib's priQueueIsEmpty(albumsPriQueue)) then
           display dialog "Empty Queue."
       else
           --set up playlist
           if exists user playlist "1 iPod Albums" then
               delete every file track of user playlist "1 iPod Albums"
           else
               make new user playlist at folder playlist "Test" with properties {name:"1 iPod Albums"}
           end if
           repeat while ((size of user playlist "1 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 and kind contains "audio" and comment does not contain "omit") to user playlist "1 iPod Albums"
           end repeat
       end if
       set endTime to (current date)
       set deltaTime to (endTime - beginTime) div minutes
       set modSeconds to (endTime - beginTime) - (deltaTime * minutes)
       display dialog "Run time: " & deltaTime & " minutes " & modSeconds & " seconds."
   end tell
end timeout

Version 2 - No Real Improvement (runtime: 6:31)

Applescript:


set beginTime to (current date)
--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.0 * 20 --iTunes uses 0 to 100 internally, not 1-5 stars, so each star is 20
set prohibitedGenres to {"Podcast", "Standup Comedy"}
set allAlbumsList to {}
--set shortAlbumList to {}

with timeout of 1000 seconds
   --get album list from iTunes
   tell application "iTunes"
       if exists user playlist "2 iPod Albums" then
           delete every file track of user playlist "2 iPod Albums"
       else
           make new user playlist at folder playlist "Test" with properties {name:"2 iPod Albums"}
       end if
       
       (*begin*)
       set allalbumList to album of file tracks of library playlist 1 whose (kind contains "audio" and track count > 1 and track number = 1 and (disc number = 1 or disc number = 0) and comment does not contain "omit")
       set tcount to count allalbumList
       (*end*)
   end tell
   
   display dialog "Processing " & tcount & " Albums" giving up after 1
   
   --Let's see if the album meets our standards
   tell application "iTunes"
       set itemCount to 0
       repeat with anAlbum in allalbumList
           set albumTracks to (every file track of playlist 1 whose album is anAlbum and comment does not contain "omit")
           
           --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 or �
                       kind of aTrack does not contain "audio" then set prohibited to true
                   if played date of aTrack is missing value then
                       set lastPlayed to date "Monday, January 1, 1900 12:00:00 AM"
                   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)
                   set itemCount to itemCount + 1
                   if itemCount mod 10 = 0 then display dialog "Processing item " & itemCount buttons {"OK"} default button 1 giving up after 1
               end if
           end if
       end repeat
       
       if (priorityLib's priQueueIsEmpty(albumsPriQueue)) then
           display dialog "Empty Queue."
       else
           --set up playlist
           repeat while ((size of user playlist "2 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 and kind contains "audio" and comment does not contain "omit") to user playlist "2 iPod Albums"
           end repeat
           set endTime to (current date)
           set deltaTime to (endTime - beginTime) div minutes
           set modSeconds to (endTime - beginTime) - (deltaTime * minutes)
           display dialog "Run time: " & deltaTime & " minutes " & modSeconds & " seconds."
       end if
   end tell
end timeout

Version 3 - Vastly Improved (runtime: 1:22)

Applescript:


(*Make sure the "Overdue Albums" playlist in iTunes is sorted by
the "Last Played" date column before you run this script!*)


set beginTime to (current date)
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

with timeout of 1000 seconds
   tell application "iTunes"
       if exists user playlist "3 iPod Albums" then
           delete every file track of user playlist "3 iPod Albums"
       else
           make new user playlist at folder playlist "Test" with properties {name:"3 iPod Albums"}
       end if
       repeat with aTrack in (file tracks of user playlist "Overdue Albums")
           set tempAlbum to album of aTrack
           --Make sure we haven't already added this album
           set testAlbum to count (every file track of user playlist "3 iPod Albums" whose album is tempAlbum)
           if testAlbum = 0 then
               -- Nope, it's not in the playlist
               -- Let's check and see if there's room to add it
               if ((size of user playlist "3 iPod Albums") ≤ NanoThreshold) then
                   display dialog "Adding album " & tempAlbum giving up after 1
                   duplicate (every file track of library playlist 1 whose album is tempAlbum and comment does not contain "omit") to user playlist "3 iPod Albums"
               else
                   exit repeat
               end if
           end if
       end repeat
       set endTime to (current date)
       set deltaTime to (endTime - beginTime) div minutes
       set modSeconds to (endTime - beginTime) - (deltaTime * minutes)
       display dialog "Run time: " & deltaTime & " minutes " & modSeconds & " seconds."
   end tell
end timeout

To test them, create a folder in iTunes' sidebar called "Test."  Now, if you run the three scripts, they will each create their own playlist in the test folder and fill the playlist with what they think are the least recently played albums.  Why do I say, "..what they think are the least recently played?"  Because, if you look at the playlist contents for each script you'll see some differences. 

Why is this?  The minor differences are due to the way in which the initial set of tracks is generated.  While the original script looped through:

Applescript:


file tracks of library playlist 1 whose kind contains "audio" and comment does not contain "omit" and comment does not contain "single" and comment does not contain "christmas"

and the second script pulled an initial set based on these criteria:

Applescript:


file tracks of library playlist 1 whose (kind contains "audio" and track count > 1 and track number = 1 and (disc number = 1 or disc number = 0) and comment does not contain "omit")

the current version pulls tracks much like the second version, but uses the range from 1 to 3 for the track number.  Why is this important?  Because, if you're like me, you sometimes don't rip or buy ALL the tracks from an album.  In other words, the second version of the script will pull only albums where there is a track 1.  So if you have an album that doesn't have track one, that album will never make it to the final playlist since the script never sees it.

Further, the first two scripts are generating their own album ratings, while the last script uses the album rating from the track record.  But you can see that the majority of albums in the playlists that these three scripts generate are the same.

Washing the Dishes
So, now I've eaten my crow for dinner.  Some days it just doesn't pay to have an iPod Nano, or at least to know anything about scripting. (sigh)

My original script wasn't very efficient and it took me an entire year to find a better alternative.  I'm not above admitting that I missed the obvious, or doing a code rewrite when it is necessary.

The whole point of this exercise is this: Never assume that you have to do everything in your scripts.  Most applications are written in Cocoa (or C++) and can run circles around an Applescript.  So look for ways that you can get the target application to do the majority of the work and write the bits of Applescript that are the minimum you might need to get your task accomplished.

'Nuff said.

Dessert
As a postscript, I wanted to mention something I've found really useful in my scripting.  The item is Red Sweater Software's FastScripts.  It's a much better script menu than Apple's, in my opinion.  It allows you to assign keyboard shortcuts and you can edit a script simply by holding down the option key when you select it from the menu.  It also gives detailed error messages when a script crashes - that alone makes FastScripts worthwhile!

Another feature I like is that when you have a script running, the menu icon changes color to let you know that the script is still running and the menu becomes "Abort running script" so you can kill a runaway script.  Add to that the fact that it's only $14.95 and that you can try the Lite version for free, and there's no reason not to go get a copy.

That's all this time.  Have fun scripting - go crunch some code!


Nitewing '98
--
I distrust morning people, largely because I suspect them of getting together early one day while the rest of us were asleep and setting up the rules of civilization.


Filed under: iTunes

Offline

 

#2 2009-01-31 02:47:47 pm

doclaphroaig
Member
Registered: 2009-01-31
Posts: 1

Re: Improve your Applescripts: Return to iTunes Scripting

Great tutorial, however you're missing something important when using smart playlists: they can themselves rely upon other smart playlists.  So the first thing you should have done, is create a generic "Music Only" smart playlist.  Then from that, you can very easily make a "Only Unrated Songs" playlist, a Holiday playlist, your "Least  Listened To" playlist (I have one myself), and so on...

Offline

 
  • Index
  •  » unScripted
  •  » Improve your Applescripts: Return to iTunes Scripting

Board footer

Powered by FluxBB

RSS (new topics) RSS (active topics)