AppleScript for Beginners IV - Records & Repeats

by Craig Smith

I hope that you played around a little bit with the last script from AppleScript Tutorial for Beginners III - The Power of Lists:

tell application "iTunes"
	make new playlist with properties {name:"Fav Long A Songs"}
	duplicate (every track of playlist 1 whose name starts with "A" and duration is greater than (3 * 60) and rating is greater than or equal to 60) to playlist "Fav Long A Songs"
end tell

When you run it, it will create a new playlist entitled “Fav Long A Songs,” and place in it any tracks from your library that start with the letter “A,” are longer than 3 minutes, and have a rating of more than 3 stars. The two pertinent commands are make new playlist and duplicate. When you peruse the iTunes AppleScript dictionary, you will find those commands listed in the Standard Suite.

Let’s take a look at the ‘make’ command. When you scroll to make on the list of Standard Suite commands, you see a few important explanations. The first is that my usage of the term ‘new’ is defined as optional, which it is, if you re-write the previous script (or delete the new playlist) and run it without the term. It works either way. We will ignore the second term (‘at’) since it doesn’t apply here, and focus some time on the third listing, with properties record, since we used that phrase in the script.

Records are a very useful tool for AppleScript, and although you may never use them yourself, it is important to understand what they are and how they work. For instance, every track and every playlist in iTunes has a set of properties contained within a record. Just look again at the dictionary listing for either playlist or track in the iTunes Suite section. See the list of PROPERTIES for each? That means that each of those pieces of information is contained within a record for each track or each playlist.

Records have a specific pattern of construction, which is what makes them a record in the first place. Now would be a good time to punch up a record so you can see with your own eyes what that structure looks like. Would you like to do that? Sure you would. Unfortunately, you can’t do it with iTunes. It is one of those annoying little AppleScript weaknesses that crop up from time to time. We can, however, do it with iPhoto, so try this little script out:

tell application "iPhoto"
	get properties of album 1
end tell

You do not need to have any photos at all in your iPhoto application for this to work, so don’t worry about that, and although your results will not be just like mine, here is what I get:

{class:album, children:{album id 4.295966305E+9 of application “iPhoto”, album id 4.295966309E+9 of application “iPhoto”, album id 4.29596631E+9 of application “iPhoto”, album id 4.295966311E+9 of application “iPhoto”, album id 4.295966312E+9 of application “iPhoto”}, type:photo library album, name:“Library”, id:4.295966296E+9, url:missing value, parent:missing value}

If you compare this list with the items listed under PROPERTIES for an iPhoto album (see the AppleScript iPhoto dictionary), you will see that all the elements are there, but not necessarily in the same order as in the dictionary. The key pattern to pay attention to is that the record is really nothing more than a list of items where each item has a specific label. Each label name has a colon “:” immediately following the label name, and then the value of that named item is displayed. For example, in my record list here, you see that my iPhoto album 1 is identified as being classified as an album (class:album), has 5 children albums (each with an individual id number), is of the photo library album type (type:photo library album), is named “Library” (name:“Library”), has its own id number (id:4.295966296E+9), and is missing both its url and parent information (url:missing value, parent:missing value). That is the structure of a record. It is just like a list, except with labels. And, like a list, it can contain any sort of information; strings, numbers, other lists, or even other records.

You can make your own records as well, simply by following the same rules you already know about AppleScript lists and variable names. Remember that you cannot use any AppleScript terms, nor can your variables start with a number. You cannot use a dash (-), but you can use an underscore (_). With that in mind, let’s make a record:

set my_first_Record to {item_1:"A string", item_2:25, item_3:{"A", "mixed", "List", 21}}

We can get information about a record in similar fashion to that of a list. Try adding this line right under the line defining the record, and run the script:

count my_first_Record's items

You get the result of 3. The record my_first_Record contains three items. Now, try this on the next line:

get my_first_Record's item 2

Whoops. Didn’t like that one, did it? This is an important point to understand. You cannot access record items by number as you can in a list. You need to know their name(s). Replace that last line with this one:

get my_first_Record's item_2

Now, you get the result of 25, which is the labelled item item_2. If you desire, you can also get the data of every item, just without the labels:

get my_first_Record's every item

This line yields a list of all items in the record: {“A string”, 25, {“A”, “mixed”, “List”, 21}}. So, to really use a record well, you must know the names of the items within that record, because you cannot use AppleScript to extract them:

get the name of my_first_Record's every item

If you try that, you will receive an error.

Let’s spend a little more time with records to learn about repeats. Looping through a long list of items to do something is just what computers were made for. For instance, I know you want to keep a database of songs in your iTunes library, so I am going to show you how to do it, utilizing records. As I sit here reading your mind, I think you are interested in tracks that are more than 2 minutes long, but less than 3 minutes. You want every artist except for The Beatles. No sweat. First, tell iTunes to make your list of tracks:

tell application "iTunes"
set the_No_Beatles_List to every track of library playlist 1 whose duration is greater than 120 and duration is less than 180 and artist does not contain "Beatles"
end tell

Perfect. When you run that, and look at your Results pane, you get that most confusing list of file track id… junk that is totally incomprehensible to humans. Since it is a list, however, you can make a repeat statement to process it and make a list of records containing the data you want. Since we know that we are going to create a second list, let’s come up with a unique variable name for this list (itunes_Records_No_Beatles). We are going to use a repeat loop to build this list, so we will begin by setting this variable to an empty list with this line:

set itunes_Records_No_Beatles to {}

The empty brackets indicate an empty list. We now have somewhere to put all these records we are going to process. I will now proffer up the statements necessary to perform the processing, and then explain how it works:

repeat with a_Track in the_No_Beatles_List
set end of itunes_Records_No_Beatles to {TrackName:a_Track's name, TrackArtist:a_Track's artist, TrackAlbum:a_Track's album, TrackRating:((a_Track's rating) / 20)}
end repeat

The first line starts off the loop. We need to repeat with a variable (a_Track) in a list of items (the_No_Beatles_List). It makes no difference how long the list is. This command will start with the first item in the list, and cycle through the list, one item at a time, and do whatever commands come next until it reaches the end repeat. We could put dozens of commands in this section, and the loop would do them all over for each item in the list. In our script, we are simply building the list we defined earlier (itunes_Records_No_Beatles) with a record for each track in the list we are looping through (the_No_Beatles_List). Our records will each contain the track’s name, its artist, its album, and how many stars you have rated that track. You see that when we access that data, we refer to the variable that we have set up to repeat with, which is a_Track. The first item in our record is the TrackName, which we get by retrieving a_Track’s name. In similar fashion, we extract the track’s artist and album. We know (from our last discussion) that the iTunes rating is a number from 0 to 100, so we divide that by 20 to get the number of stars to put in the record as TrackRating.

When you run this script, you end up with a list of records that contain the desired track information, organized neatly into well labeled records. In a future episode, we will discuss how to save this data for later analysis. For now, let’s talk some more about repeats.

The method I introduced earlier is what I call the referenced repeat. (I don’t know if there are official names for these things, so I just make up my own.) Whenever you use the syntax of:

repeat with Some_Variable in Your_List

your repeat loop does not actually hold the value of the item in the list in it’s memory, it holds a reference to the item in the list. This is the easiest and most intuitive method of repeating in AppleScript, and it is used much more than other methods. There are a few situations where this method does not work well, but we’ll save the details on that for a later time, so that we can cover what I call direct repeats. A direct repeat cycles through the loop, and grabs the data itself for the commands in the loop. It is also much more wordy; as in this example:

set repeat_List to {1, 2, 3, 5, 7, 11, 13}
set new_List to {}
repeat with an_Item from 1 to (count repeat_List)
	set end of new_List to (item an_Item of repeat_List) * 2
end repeat

We start with our main list (repeat_List) and an empty list (new_List). This time, however, we use our repeat variable (an_Item) as a counter. We cycle through our list, starting at item number 1 (from 1 to) and continuing all the way through the list by simply counting the items in the list (count repeat_List) and using that number as the end number for the repeat.

Once inside the loop, we set the end of the new_List to item number an_Item of of the list repeat_List times 2. Since our variable (an_Item) is a counter, we have to say ‘multiply item number an_Item of repeat_List by 2, and put that at the end of new_List.’ Do you see how wordy and cumbersome this seems, compared to our earlier method? Even so, it is often the only way around certain types of data, so it is important to understand. In addition, you have quite a bit more creativity in how you construct the repeat, when you use your repeat variable as a counter. For instance, you can loop through and only process every other item. Try replacing the ‘repeat with’ line above with this line, and run it again:

repeat with an_Item from 1 to (count repeat_List) by 2

That little by 2 at the end tells the loop to increase the counter by 2, thus only processing every 2nd item in the list. As you have imagined already, you can set your interval number to whatever you want.

There are other kinds of repeats available for your use. You can do the eternal repeat, wherein you simply type

end repeat

This will go on forever until you click the Stop icon in Script Editor. You can decide to repeat a loop only a certain number of times, like this:

repeat 6 times
--do something
end repeat

You can repeat while a certain test is true, or until a certain test is true. Let’s go back to our original loop script. This time, we will set it to go as long as the value of the current item in repeat_List is less than 10. This is a bit wordy as well, so watch out:

set repeat_List to {1, 2, 3, 5, 7, 11, 13}
set new_List to {}
set the_Counter to 1
repeat while (item the_Counter of repeat_List) < 10
	set end of new_List to (item the_Counter of repeat_List) * 2
	set the_Counter to the_Counter + 1
end repeat

We needed one more variable to make this work, the_Counter. Since we are using the repeat line to identify the desired test, we have to use the variable the_Counter to iterate through the list. See how every time the loop runs, we set the_Counter to add 1 to itself? As long as that numbered item in the list is less than 10, the loop continues. In a similar fashion, we can tell the loop to go until we hit something in the list that is greater than 5. Replace that repeat line with this line:

repeat until (item the_Counter of repeat_List) > 5

As soon as the loop hits an item in the list that is greater than 5, it quits. It just so happens that I put that list together in numerical order, but that does not matter. The repeat loop does not evaluate your entire list and sort it out for you. It will not simply go through your entire list and only process those numbers that are less than 5, or less than 10 in the previous example. Once the test has been met, the repeat quits.

Well, I don’t know about you, but I’m getting pretty tired right of all this looping around right about now. I think it is time to call it the end for today, and leave you with another mind-boggling script to cogitate over until next time:

tell application "iTunes"
	set tracks_To_Play to (every track of playlist 1 whose played count = 0)
	repeat with play_This in tracks_To_Play
		if play_This's rating = 0 then
			play play_This
		end if
		delay ((play_This's duration) - 8)
		set {play_This's played count, play_This's played date} to {1, (current date)}
	end repeat
end tell