All right, the other day I posted a tutorial on manipulating the iTunes COM with Python to give you better searching capabilities than iTunes does natively, and how to make a playlist with the resulting tracks. Since then, I’ve managed to change the code a bit because there were a couple of places where it wasn’t working correctly before (eg including duplicate tracks) and some features I felt were missing. So let’s see the changes, shall we?
def union(l1, l2):
pl1 = [i.GetITObjectIDs() for i in l1]
pl2 = [i.GetITObjectIDs() for i in l2]
pl3 = sorted(list(set(pl1+pl2)),
key=lambda x: list(pl1+pl2).index(x))
return [list(l1+l2)[i] for i in
[list(pl1+pl2).index(x) for x in pl3]]
def andnot(l1, l2):
pl1 = [i.GetITObjectIDs() for i in l1]
pl2 = [i.GetITObjectIDs() for i in l2]
pl3 = [i for i in pl1 if i not in pl2]
pl3 = sorted(list(set(pl3)),
key=lambda x: pl3.index(x))
return [l1[i] for i in [pl1.index(x) for x in pl3]]
def intersect(l1, l2):
pl1 = [i.GetITObjectIDs() for i in l1]
pl2 = [i.GetITObjectIDs() for i in l2]
pl3 = [i for i in pl1 if i in pl2]
pl3 = sorted(list(set(pl3)),
key=lambda x: pl3.index(x))
return [l1[i] for i in [pl1.index(x) for x in pl3]]
def difference(l1, l2):
pl1 = [i.GetITObjectIDs() for i in l1]
pl2 = [i.GetITObjectIDs() for i in l2]
pl3 = [[i for i in pl1 if i not in pl2],
[i for i in pl2 if i not in pl1]]
pl3 = [sorted(list(set(pl3[0])),
key=lambda x: pl3[0].index(x)),
sorted(list(set(pl3[1])),
key=lambda x: pl3[1].index(x))]
return [l1[i] for i in
[pl1.index(x) for x in pl3[0]]] +
[l2[i] for i in
[pl2.index(x) for x in pl3[1]]]
Okay, before I talked about how with “OR”, all you had to do was add the two lists together, but that sometimes means duplicates, so I wrote a union function to fix that. Let’s take a look. Like the other functions, it takes in two lists/IITTrackCollections and makes parallel lists containing the IDs of the tracks, this time. Next, it makes a set of the two lists together. However, since sets are unordered, when making pl3 a list again, it sorts it by the index of each track in the combined list, returning it to the correct order. Then it performs the same last step as the others, returning a list which contains the corresponding tracks to the IDs in pl3.
The only change to intersect and difference is a similar fix to pl3. In addition to the previous construction of pl3, I’ve added the same sorting of the list resulting of the set made from pl3.
I also added an andnot function to allow the “not” operator – ie find tracks that match one query, but not another.
Also, as you can see, the duplicates are removed via track ID instead of name, now, so that multiple songs with the same name by different artist, for example, aren’t weeded out.
In addition, I changed custompl to allow for searching not limited to the default searches. For example, you can now search within the year of a track, or its comment, or even the genre or date added, assuming you know the correct format (the field for date added, for example, is “DateAdded” – all the possible fields can be found in the help file for the iTunes COM).
def extend(alist, element):
alist.extend(element)
return alist
def custompl(quer1, logic, quer2):
res = []
temp = [quer1,quer2]
if len(quer1) == 3:
if len(quer2) == 3:
return dic[logic](custompl(*quer1),
custompl(*quer2))
res.append(custompl(*quer1))
temp.remove(quer1)
elif len(quer2) == 3:
res.append(custompl(*quer2))
temp.remove(quer2)
for query in temp:
srchstr, field = query
if field == "Playlist":
res = res+[reduce(extend,extend(res[0:0],
[list(pl.Tracks)
for pl in source.Playlists
if srchstr in pl.Name]))]
elif field in dic:
res.append(list(library.Search(srchstr,
dic[field])))
else:
try:
res.append([track
for track in library.Tracks
if srchstr in eval(
"track.%s"%field)])
except TypeError:
res.append([track
for track in library.Tracks
if srchstr in repr(
eval("track.%s"%field))])
except AttributeError:
__import__("sys").stderr.write("ValueError"+
": Sorry, can't find %s field to search
"in.\n"%field)
return dic[logic](res[0],res[1])
As you can see, instead of just checking for “Playlist” as a field, custompl now also checks if the field is in the dictionary, and if it’s not, tries to find it by accessing it with eval. AGAIN, THIS SHOULD NOT BE ATTEMPTED UNLESS YOU TRUST YOUR END USER. For example, by making the following function call, “bad” gets written to your stdout; and with a few modifications, all sorts of havoc can be wreaked: customplaylist((" ", '__setattr__("Name",__import__("sys").stdout.write("bad"))' )) (Note that this doesn’t actually change the name of any tracks, but rather raises an error after writing to the stdout.) Alternatively, you could make a dictionary with all the possible fields to search in and check if the field is in that dictionary and work through it that way to avoid eval.
In any case, first we try to see if the search string is in the field, then, if the field isn’t a string (eg the DateAdded field, which is a time instance), we see if the search string is in Python’s representation of the field ('<PyTime:4/17/2006 12:05:32 AM>' for a sample DateAdded, for example). At some point I’d like to see if I can make it possible to specify whether you want exact pattern matching for each query, ie if srchstr == eval(...) or if srchstr == repr(eval(...)). Anyway, if the field isn’t available, we write the error code to stderr without stopping the program.
You’ll notice that I changed the treatment of playlists a bit. This is because the previous method only kept the tracks in the first playlist found. This way, all the tracks from all the matching playlists are added together, then that list is added to res. The way I acheived this is by taking an empty list (res[0:0]), and using reduce to extend it by the list of tracks from each matching playlist. Then, I added the current res to it, making sure we didn’t lose the tracks already in it. Since we add [reduce(...)], it keeps all the tracks from this query in a separate list, like it should. The way reduce works is to apply the first parameter (a function) to the elements of the second one in twos. For example, reduce(operator.add,range(10)) is the equivalent of (((((((((0+1)+2)+3)+4)+5)+6)+7)+8)+9). So our code is the equivalent of extend(extend(extend(res[0:0],tracklist1), etc. So we contain that in a list (
tracklist2),tracklist3)[reduce(...)]) and add res to it to get the new res.
I also modified the main function to allow for single query searches:
def customplaylist(querylist,title=None):
pls = []
if type(querylist) is str:
querylist = (eval(
querylist.replace(" XOR ",", 'X',"
).replace(" AND ",", '+',"
).replace(" OR ",", '|',"
).replace('" in "',"', '"
).replace('"',"'"
).replace(" AND NOT ",", '+!',")))
if len(querylist) != 1:
querylist = [querylist]
try:
pls.extend(custompl(*querylist[0]))
except TypeError:
res = []
srchstr, field = querylist[0]
if field == "Playlist":
res.extend(reduce(extend,
extend(res[0:0],
[list(pl.Tracks)
for pl in source.Playlists
if srchstr in pl.Name])))
elif field in dic:
res.extend(list(library.Search(srchstr,
dic[field])))
else:
try:
res.extend([track
for track in library.Tracks
if srchstr in repr(
eval("track.%s"%field)
)])
except TypeError:
res.extend([track
for track in library.Tracks
if srchstr == repr(
eval("track.%s"%field)
)])
except AttributeError:
__import__("sys").stderr.write("ValueError"+
": Sorry, can't find %s field to search in."+
"\n"%field)
pls.extend(res)
plname = str(querylist).replace(", 'X',"," XOR "
).replace(", '+',"," AND "
).replace(", '|',"," OR "
).replace("', '","' in '"
).replace("[",""
).replace("]",""
).replace(", '+!',"," AND NOT")
cpl = win32com.client.CastTo(
iTunes.CreatePlaylist(title or plname),
'IITUserPlaylist')
for track in pls:
cpl.AddTrack(track)
We first try to send querylist to custompl, but if there’s only one query, it’ll raise a TypeError, which we catch; and then we proceed to copy the code from custompl, slightly modified. The only difference is that, since there’s only one query, we don’t need the crap at the beginning of custompl, nor do we need as much fancy footwork to make res have two elements, one from each side. We just extend res by all the tracks found.
Finally, we just need to update dic, and we’re on our way.
dic = {"All":0,"Visible":1,"Artist":2,"Album":3,
"Composer":4,"Name":5,"+":intersect,
"|":union,"X":difference,"+!":andnot}
All right. Head over here to get my updated full code.













0 responses so far ↓
There are no comments yet...Kick things off by filling out the form below.