A blow-by-blow breakdown of how to use the iTunes COM to search for and make playlists out of songs that meet specific criteria through Python (much more complex than the native iTunes search capabilities). I’ll be doing the step-through in a rather legible, but not very line-efficient method; also, I’d like to apologize in advance for the horrible formatting – the columns in this theme have a fixed size, so text just gets cut off if it’s too long; my actual final script is available here (rename the resulting file to .py instead of .py.txt): custompl.py. If you want to look without downloading, it’s also up here.
If you don’t have the iTunes SDK, get it here. If you don’t have Python, or don’t know what it is, check here (Wikipedia entry) and here (Python’s official homepage).
If you already know what you’re doing with Python, skip to the code below. Once you have Python installed, fire up your favorite IDE, or just use IDLE, the one Python’s bundled with, and start typing. If you use IDLE, it will be an interactive prompt, so while you can see the results of what you do immediately, you can’t very easily save what you do directly from the prompt. Instead, you should go to File -> New Window and save the blank page as “<insert script name here>.py”. This will highlight everything for you the same way the interactive prompt will. I suggest doing both – experimenting with the prompt, then saving what works – but you can do it either way.
import win32com.client
iTunes = win32com.client.gencache.EnsureDispatch(
"iTunes.Application")
Okay, first thing we have to do is import win32com.client, so that we can access the iTunes COM. Next, we connect to iTunes, using win32com.client’s gencache.EnsureDispatch so that we don’t get the funky behavior that Dispatch can sometimes cause.
source = iTunes.LibrarySource
library = iTunes.LibraryPlaylist
All right, now we have the iTunes source (contains all the playlists) and the main Library playlist. Now, if you want to search for songs using the iTunes COM, you have to specify which fields to search in as well as the search string (this is documented in the help file that comes with the iTunes SDK). So what we’re going to do is make a dictionary that will convert the string parameter of which field to search in into a number that iTunes can understand:
field_dict = {"All":0,"Visible":1,"Artist":2,
"Album":3,"Composer":4,"Name":5}
We’ll worry about searching in playlist names later. Basically, the default choices available for searching iTunes are All fields, all Visible fields, Artist, Album, Composer, or Name of the song. All right, time to get down to some actual searching – just one query at a time to start with (we’ll add multiple searches at once later, and smart titling at the same time):
custom_playlist = iTunes.CreatePlaylist("untitled")
custom_playlist = win32com.client.CastTo(custom_playlist,
'IITUserPlaylist')
query = "Imogen Heap"
field = "Artist"
results = library.Search(query, field_dict[field]))
All right, first we used iTunes to create an untitled playlist. Now, unfortunately, the default when creating a playlist this way, for some reason, doesn’t give you access to all the methods that the documentation suggests. So you have to use win32com.client.CastTo to, well, cast the result to a proper iTunes IITUserPlaylist (documented, again, in the help file). Next we can perform the search and start populating the playlist. Okay, so we searched the library for any tracks whose artist is Imogen Heap – a freakishly good singer, by the way – she used to be part of a band called Frou Frou before going solo . . . Coming back to the code, all we have left to do is populate the playlist we made with the tracks we found.
for track in tracks:
custom_playlist.AddTrack(track)
And that’s it. Those are the basics. To be able to use multiple queries, quite a few additions to the above basics need to be made. We’ll start with adding logical operators – because there’s nothing more irritating than having a program assume you want all of the conditions to be met when there’s a specific order to which you want and which you can do without:
field_dict = {"All":0,"Visible":1,"Artist":2,"Album":3,
"Composer":4,"Name":5,"+":intersect,
"|":__import__("operator").add,"X":difference}
First thing we have to do is update the dictionary to allow for the logical operators: “+” for AND, “|” for OR, “X” for XOR. For those not familiar with logical operators, AND, appropriately enough, requires the fulfillment of both conditions (a AND b), OR requires one or the other or both, and XOR (aka exclusive or) requires one or the other but not both. Now, this may still seem a bit odd as there are no “intersect” or “difference” functions yet, and what’s this business with __import__? Well, we’ll work backwards. The expression __import__("operator").add is roughly equivalent to operator.add, once operator has been imported. I say roughly because __import__ doesn’t permanently import its parameter, it merely allows one to access the method of a class not in the present namespace. Anyway, by putting this reference to operator.add in field_dict, we can achieve the following: field_dict["|"](a,b) will produce the same as a+b. Now, just to be perfectly clear, this is NOT the same as field_dict["+"](a,b). I’m talking about putting a+b in the interactive prompt; actually adding them together. There’s a reason for this, which I’ll explain in due time. First, to define intersect and difference:
def intersect(tracks1, tracks2):
names1 = [i.Name for i in tracks1]
names2 = [i.Name for i in tracks2]
names3 = [i for i in names1 if i in names2]
return [tracks1[i] for i in
[names1.index(x) for x in names3]]
def difference(tracks1, tracks2):
names1 = [i.Name for i in l1]
names2 = [i.Name for i in l2]
names3 = [[i for i in names1 if i not in names2],
[i for i in names2 if i not in names1]]
return [tracks1[i] for i in
[names1.index(x) for x in names3[0]]]+
[tracks2[i] for i in
[names2.index(x) for x in names3[1]]]
Well, well. This is confusing indeed. Let’s take a look at intersect first. This is meant to mirror the intersect method of the built in set class. There are two parameters: each a list of tracks or an IITTrackCollection, composed of IITTracks. Each IITTrack has a Name attribute, so the first thing we do is make some parallel lists containing the Names of the tracks in the arguments. Once these have been made, we make a list of tracks which are in both. Then, we make and return a list of the actual tracks (as opposed to just the Names) which are represented by the Names in the list we just made. Simple enough, right?
Okay, now we get to the really confusing bit. Again, this is meant to mirror the set method of the same name. The parameters are the same as in the previous function, as is the next step. Once we have two lists of Names which correspond to the provided lists, we make a third list which has two elements. The first is a list of all the Names in the first Names list which don’t appear in the second, and the second element is the reverse. With this in hand, we proceed to perform the same step as in the last function – using the Names we want to refer back to the original IITTracks. The only difference is that, since this time we have two lists to refer from (names3[0] and names3[1]), we have to collect the IITTracks from both and add them together. All right, now we can get to the fun part: parsing user input.
def customplaylist(querylist,title=None):
pls = []
if type(querylist) is str:
querylist = (eval(querylist.replace(" XOR ",", 'X',"
).replace(" AND ",", '+',"
).replace(" OR ",", '|',"
).replace('" in "',"', '"
).replace('"',"'")))
if len(querylist) != 1:
querylist = [querylist]
pls.extend(custompl(*querylist[0]))
plname = str(querylist).replace(", 'X',"," XOR "
).replace(", '+',"," AND "
).replace(", '|',"," OR "
).replace("', '","' in '"
).replace("[",""
).replace("]","")
cpl = win32com.client.CastTo(
iTunes.CreatePlaylist(title or plname),
'IITUserPlaylist')
for track in pls:
cpl.AddTrack(track)
Okay, one thing at a time. Some of this should look familiar, since we’ve used some of the pieces before. First off, we take in a list of queries, which should look vaguely like this: ("<search string>", "<field>") or '"<search string>" in "<field>"'. The queries will be interspersed with logical operators where necessary. To see a working example of a call to customplaylist, look at the bottom of the final code, which is available by clicking on the link at the top of this post.
The second parameter is a title, should the user want to name the playlist something other than the string form of querylist, which tends to look like '(("Toasty" in "Playlist") XOR ("Something" in "Artist")) AND (("Sum" in "Playlist") OR ("Killer" in "Album"))', which is the “smart titling” I was talking about before. (Another of the features of the final product is that the search acts like an iTunes search, providing matches of “All Killer, No Filler”, say, for the search string of “Killer”. This means the user gets to be all kinds of lazy when searching.)
Our next step is to create an empty list, which will eventually hold the IITTracks we want to populate our playlist with. What follows that is an ugly way to parse the string version of querylist: you replace all the human legible stuff with the Python equivalent where it needs to be, then eval it so it’s a list and not a string. There are hundreds of rants and essays about this everywhere, so I’ll just put a small word in here. DO NOT USE THIS TECHNIQUE IF YOU DON’T TRUST THE END USER ON YOUR COMPUTER. eval has the potential to do a lot of damage to your computer in the hands of the right, or rather, wrong, person. If you don’t trust the end user, just change the function to look like this, and it won’t support strings anymore:
def customplaylist(querylist,title=None):
pls = []
if len(querylist) != 1:
querylist = [querylist]
pls.extend(custompl(*querylist[0]))
plname = str(querylist).replace(", 'X',"," XOR "
).replace(", '+',"," AND "
).replace(", '|',"," OR "
).replace("', '","' in '"
).replace("[",""
).replace("]","")
cpl = win32com.client.CastTo(
iTunes.CreatePlaylist(title or plname),
'IITUserPlaylist')
for track in pls:
cpl.AddTrack(track)
Okay, now the next thing we see here is that we check the length of querylist, and if it’s not 1, we make it one by making it the only element of a new list. Why? Because the custompl function, which we haven’t seen yet, works recursively and requires there to be one group to begin with. That much is fairly straightforward.
Next, though, we see an odd call to custompl. What the hell does it mean when there’s an asterisk in front of a parameter? Well, when that parameter is a list, it separates the elements of that list so that each element is treated as a separate argument by the function you’re calling. For example, let’s say you have a function printparams that takes in three parameters, a, b, and c and prints them out. And let’s further say you had a list, foo, with elements "b", "a", and "r". The function call to printparams would look like this: printparams(*foo), and would output b a r. Instead of branching off into custompl here, I’m going to finish up the rest of the main function, and then talk about what’s going on behind the scenes.
Once we’ve finished collecting the tracks we want, we make the name of the playlist by reversing what we saw before for parsing the string version of querylist. Then, we create the playlist and cast it to an IITUserPlaylist in one step. The only thing you haven’t seen is title or plname. The way Python treats True and False, None is equivalent to False, so if the user doesn’t enter a title in the parameter list, we end up with None or plname, which Python evaluates to get plname. This works even if a title has been provided because Python only evaluates the second half of an and expression if the first condition is false. After that, like you saw before, we add our tracks to the playlist and that’s that.
All right, so the last thing to look at here is custompl, the function we’re using to really parse querylist and return the right tracks:
def custompl(query1, logic, query2):
results = []
temp = [query1,query2]
if len(query1) == 3:
if len(query2) == 3:
return field_dict[logic](custompl(*query1),
custompl(*query2))
results.append(custompl(*query1))
temp.remove(query1)
elif len(query2) == 3:
results.append(custompl(*query2))
temp.remove(query2)
for query in temp:
search_string, field = query
if field == "Playlist":
results.extend([list(pl.Tracks)
for pl in source.Playlists
if search_string in pl.Name])
else:
results.append(list(
library.Search(search_string,
field_dict[field])))
return field_dict[logic](results[0],results[1])
Whoo boy, here we go. Remember how I said that the asterisk makes the function treat the list as its component elements? Well here’s where it comes into play. When it comes down to it, every group, no matter how complicated, has three parts. The left side, the logical operator, and the right side. So first we make an empty list to hold our results. Then we make another list to hold the left and right side. Then we check each side to see if it is, itself, a group, or if it’s really a query (remember, groups have three components, and queries have two: search string and field). If they’re both groups, it applies the function that corresponds with the operator on the result of breaking down the group again by running *it* through the function. And so on until they both aren’t groups anymore. Once only the first one is a group, it adds the results of breaking down that group to the main results and removes it from temp to let the function know not to try to treat it as a query. On the other hand, if only the *right* side is a group, it does the same on that side. Once there are no more groups, it gets to the for loop. This looks at whichever side is left, or both, if they happened to have the same level of grouping, and breaks it down into search string and field.
Here’s where we deal with searching in Playlist names. We extend the results by the list of Tracks of each Playlist whose name even partially matches the search string. What this means is, it adds each track in that list to the end of results, one by one, instead of just adding the whole list as a list. This way, all the tracks on this side are at the same level.
Now, if we’re *not* searching in the Playlist name, we do what we did way back when. We search the library for the search string in the field, and append the list to results, or add it to the end. Now we should have two elements in results: one from each side. Even if there’s an uneven level of grouping on each side, the way we set it up, we still have two elements. Remember? If one side is a group and the other isn’t, we add the results of breaking it down to results, then get to here and add the results of breaking the non-group side down. Two elements. If it’s hard for you to visualize, try coming up with the most convoluted querylist you can imagine and working through custompl to see how it works. By the way, this type of programming, wherein a function calls itself, is called “Recursive” programming. To see simpler examples of this, just google “recursive” and the programming language of your choice. Since we have two elements now, we can apply the logical operator to them to get what we want. Oh yeah, I never explained why “OR” corresponds to “+”. Well, think about it. “OR” means “one or the other or both”. Which means that if we have two lists, every track in each list will be in either one, or the other, or both. We don’t have to do anything fancy to them, just add them together.
Anyway, that’s the whole cake. Once you have the results, you return them back to the main function, where we extend pls by the results, and add the tracks to our iTunes playlist.
Whew! That was a mouthful.