Wednesday, January 25, 2006

Taming iTunes with Python

Thanks to the gazillions of iPods sold, iTunes is arguably the world's most popular organizer for music, among other things. That's just fine, as iTunes has a fairly intuitive and polished interface, and handles most organizing tasks with ease. But sometimes iTunes behaves like a toddler throwing a tantrum that makes you want to pull all your hair out.

Case in point. I have a bunch of mp3 files I ripped from audiobook CDs I borrowed from the library (this is fair use, right, RIAA?). To facilitate listening to audiobooks, the newer versions of iTunes added two options for each track, "Remember playback position" and "Skip when shuffling", which allow the track to be bookmarkable, and not interrupt your music play when you shuffle.

The problem is these options can only be changed track by track. When you select multiple tracks and right-click to get the options panel, these two checkboxes are not in there. So if you have 500 audiobook tracks to change, you need to click your mouse 1,500 times (two for the checkboxes and one for the "next" button).

That was exactly what I did the first time around, and I swore I'd never do it again. But somehow after I changed some settings in iTunes and upgraded to a newer version, many, but not all, of the audiobook tracks lost those options. They became unchecked.

So here is the pulling hair out part. I'd rather be bald than click through those tracks again. A quick googling led me to some AppleScripts, but unfortunately I am on Windows so they are not much use to me. Further digging turned up Apple's iTunes COM for Windows SDK. Now I am not really a Windows COM guy, but this looked simple enough.

My first attempt was to use javascript to automate the mouse clicking. That didn't go very far as for some weird reason you have to downcast an IITTrack interface to IITFileOrCDTrack in order to access the bookmarkable and no shuffle options. I couldn't figure out how to type cast in javascript.

For the next step I could have used C++ or C# to access the COM interfaces, but that seems so heavy-handed for something simple like this. More googling showed, aha, of course, python is the perfect language for this. I've been trying to learn python and been reading the excellent online book Dive into Python, so this is the perfect chance to practice.

After looking at a few examples and some false starts, I finally got this to do what I wanted. Without further ado, I give you the python script to automate those 1,500 mouse clicks:
import win32com.client

def main():

iTunes = win32com.client.gencache.EnsureDispatch("iTunes.Application")
tracks = iTunes.SelectedTracks
track_count = tracks.Count

for track_index in range(1, track_count+1):
track = win32com.client.CastTo(tracks.Item(track_index), 'IITFileOrCDTrack')
track.RememberBookmark = True
track.ExcludeFromShuffle = True

if __name__ == '__main__':
main()

16 Comments:

Anonymous Ting said...

Can iPod work with a big file? (I can neither afford one nor afford more hearing loss with one, so I have no idea.) If yes, you can join the 500 tracks together with some freeware out there, and then you'll need only 3 clicks!:)

6:18 PM  
Blogger Rong said...

Yeah, joining tracks alleaviates some of the problem, but doesn't solve it completely. You can't make the files too big or too long, otherwise your iPod would have problem playing them. An unabridged audiobook tends to have more than a dozen CDs, so even if you join all the tracks together on each CD (iTunes actually support this when you import through it), you still end up with lots of files pretty quickly. Besides, if you have some old mp3 files lying around that were encoded in VBR, I never quite found a utility that can join them reliably.

This was really just an example showing what you can do with python to control iTunes. The possiblities are endless. :)

3:33 PM  
Anonymous Anonymous said...

Here's the javascript version for people with mixed content (music, podcasts, audiobooks...) Long, I know, but
commented and produces output of what's been done.

Just copy and past into your favorite text editor, edit if needed and save.
---------------------------------------------------------------------------

/* Rename script to Audiobooks.js
From Explorer: Double-click to run script.

Script by Spencer Carter - spencer_dot_carter-at-gmail_dot_com

02/03/2006 - Initial release
02/05/2006 - Added 'gType' variable for genre check
02/09/2006 - Added the 'Exclude from Shuffle' setting

Although Audible content is already set to be bookmarkable and
to be excluded from shuffling, this script should not be used on
Audible Audio content as those files are write protected and will
cause the script to fail. It is recommended to set the genre type
of these files to something other than what is set in the 'gType'
variable so as not to be included.

This script will set the 'RememberBookmark' tag, also called the
'Remember playback position' on the 'Options' tab of the
'Get Info' window in iTunes. It also sets the 'Exclude from Shuffle'
option to prevent playback when shuffling music.

This script needs to have all tracks you would like to be tagged as
bookmarkable to be of the genre type defined by the variable 'gType' */

var gType = "Audiobook"; /* Other types could be: 'Spoken Word',
'Books', 'Voice & Spoken', 'Speech',
etc... It is case-sensitive. */

/* --- DO NOT MODIFY BELOW UNLESS YOU KNOW WHAT YOU ARE DOING --- */
var fso, txt, i;
var iTunesApp = WScript.CreateObject("iTunes.Application");
var tracks = iTunesApp.LibraryPlaylist.Tracks;
var numTracks = tracks.Count;
var wereBookmarked = 0;
var nowBookmarked = 0;
var wereExcluded = 0;
var nowExcluded = 0;
var gTypeItems = 0;

fso = new ActiveXObject("Scripting.FileSystemObject");
txt = fso.CreateTextFile("Altered Tracks.txt", true);

for ( i = 1; i <= numTracks; i++ )
{
var currTrack = tracks.Item(i);
if ( currTrack.Genre == gType )
{
gTypeItems++;
/* Check for bookmark setting and make any changes. */
if ( currTrack.RememberBookmark )
{
wereBookmarked++;
}
else
{
currTrack.RememberBookmark = "1";
nowBookmarked++;
txt.WriteLine("Bookmarked: " + currTrack.Artist + "," + currTrack.Album + "," + currTrack.Name);
}
/* Check for shuffle setting and make any changes */
if ( currTrack.ExcludeFromShuffle )
{
wereExcluded++;
}
else
{
currTrack.ExcludeFromShuffle = "1";
nowExcluded++;
txt.WriteLine("Shuffle Excluded: " + currTrack.Artist + "," + currTrack.Album + "," + currTrack.Name);
}
}
}

/* Record Bookmark stats to file */
if (nowBookmarked > 0)
{
if (nowBookmarked == 1)
{
WScript.Echo("UPDATE: Set 1 track to bookmarkable out of "+ gTypeItems +" "+ gType +" items. "+ wereBookmarked +" bookmarked items existed.");
txt.WriteLine("UPDATE: Set 1 track to bookmarkable out of "+ gTypeItems +" "+ gType +" items. "+ wereBookmarked +" bookmarked items existed.");
}
else
{
WScript.Echo("UPDATE: Set "+ nowBookmarked +" tracks to bookmarkable out of "+ gTypeItems +" "+ gType +" items. "+ wereBookmarked +" bookmarked items existed.");
txt.WriteLine("UPDATE: Set "+ nowBookmarked +" tracks to bookmarkable out of "+ gTypeItems +" "+ gType +" items. "+ wereBookmarked +" bookmarked items existed.");
}
}
else
{
WScript.Echo("No changes... There were "+ wereBookmarked +" bookmarked items found in "+ gTypeItems +" "+ gType +" items in "+ numTracks +" tracks.");
txt.WriteLine("No bookmark changes were made to any tracks.");
txt.WriteLine("There were "+ wereBookmarked +" bookmarked items found in "+ gTypeItems +" "+ gType +" items in "+ numTracks +" tracks.");
}

/* Record Shuffle stats to file */
if (nowExcluded > 0)
{
if (nowExcluded == 1)
{
WScript.Echo("UPDATE: Set 1 track to exclude from shuffle out of "+ gTypeItems +" "+ gType +" items. "+ wereExcluded +" excluded items existed.");
txt.WriteLine("UPDATE: Set 1 track to exclude from shuffle out of "+ gTypeItems +" "+ gType +" items. "+ wereExcluded +" excluded items existed.");
}
else
{
WScript.Echo("UPDATE: Set "+ nowExcluded +" tracks to exclude from shuffle out of "+ gTypeItems +" "+ gType +" items. "+ wereExcluded +" excluded items existed.");
txt.WriteLine("UPDATE: Set "+ nowExcluded +" tracks to exclude from shuffle out of "+ gTypeItems +" "+ gType +" items. "+ wereExcluded +" excluded items existed.");
}
}
else
{
WScript.Echo("No changes... There were "+ wereExcluded +" exclude from shuffle items found in "+ gTypeItems +" "+ gType +" items in "+ numTracks +" tracks.");
txt.WriteLine("No shuffle changes were made to any tracks.");
txt.WriteLine("There were "+ wereExcluded +" exclude from shuffle items found in "+ gTypeItems +" "+ gType +" items in "+ numTracks +" tracks.");
}
txt.Close();

7:31 PM  
Blogger Rong said...

That's pretty cool. I think I've tried something similar, but for some reason couldn't get access to those attributes ("RememberBookmark" and "ExcludeFromShuffle").

Can you get this to work with selected tracks? That might be more convenient than dealing with the "gType" variable.

8:01 AM  
Anonymous Anonymous said...

Here it is... This one will work on just the selected tracks.

---------------------------------------------------------------------
/* Rename script to BmExSelected.js
From Explorer: Double-click to run script.

Script by Spencer Carter - spencer_dot_carter-at-gmail_dot_com

This script will operate on the tracks seleted in the playlist window.

Although Audible content is already set to be bookmarkable and
to be excluded from shuffling, this script should not be used on
Audible Audio content as those files are write protected and could
cause the script to fail.

This script will set the 'RememberBookmark' tag, also called the
'Remember playback position' on the 'Options' tab of the
'Get Info' window in iTunes. It also sets the 'Exclude from Shuffle'
option to prevent playback when shuffling music.
*/
var verbose = true; /* Set to 'false' to disable prompts and text file logging */

/* --- DO NOT MODIFY BELOW UNLESS YOU KNOW WHAT YOU ARE DOING --- */
var fso, txt, i;
var iTunesApp = WScript.CreateObject("iTunes.Application");

if (iTunesApp.SeletedTracks < 1)
{
WScript.Echo(" No tracks seleted!");
}

var tracks = iTunesApp.SelectedTracks;
var numTracks = tracks.Count;
var wereBookmarked = 0;
var nowBookmarked = 0;
var wereExcluded = 0;
var nowExcluded = 0;

fso = new ActiveXObject("Scripting.FileSystemObject");
if (verbose)
txt = fso.CreateTextFile("Altered Tracks.txt", true);

for ( i = 1; i <= numTracks; i++ )
{
var currTrack = tracks.Item(i);
/* Check for bookmark setting and make any changes. */
if ( currTrack.RememberBookmark )
{
wereBookmarked++;
}
else
{
currTrack.RememberBookmark = "1";
nowBookmarked++;
if (verbose)
txt.WriteLine("Bookmarked: " + currTrack.Artist + "," + currTrack.Album + "," + currTrack.Name);
}
/* Check for shuffle setting and make any changes */
if ( currTrack.ExcludeFromShuffle )
{
wereExcluded++;
}
else
{
currTrack.ExcludeFromShuffle = "1";
nowExcluded++;
if (verbose)
txt.WriteLine("Shuffle Excluded: " + currTrack.Artist + "," + currTrack.Album + "," + currTrack.Name);
}
}

if (verbose)
{
/* Record Bookmark stats to file */
if (nowBookmarked > 0)
{
if (nowBookmarked == 1)
{
WScript.Echo("UPDATE: Set 1 track to bookmarkable out of "+ numTracks +" selected items. "+ wereBookmarked +" bookmarked items existed.");
txt.WriteLine("UPDATE: Set 1 track to bookmarkable out of "+ numTracks +" selected items. "+ wereBookmarked +" bookmarked items existed.");
}
else
{
WScript.Echo("UPDATE: Set "+ nowBookmarked +" tracks to bookmarkable out of "+ numTracks +" selected items. "+ wereBookmarked +" bookmarked items existed.");
txt.WriteLine("UPDATE: Set "+ nowBookmarked +" tracks to bookmarkable out of "+ numTracks +" selected items. "+ wereBookmarked +" bookmarked items existed.");
}
}
else
{
WScript.Echo("No changes... There were "+ wereBookmarked +" bookmarked items found in "+ numTracks +" selected items.");
txt.WriteLine("No bookmark changes were made to any tracks.");
txt.WriteLine("There were "+ wereBookmarked +" bookmarked items found in "+ numTracks +" selected items.");
}

/* Record Shuffle stats to file */
if (nowExcluded > 0)
{
if (nowExcluded == 1)
{
WScript.Echo("UPDATE: Set 1 track to exclude from shuffle out of "+ numTracks +" selected items. "+ wereExcluded +" excluded items existed.");
txt.WriteLine("UPDATE: Set 1 track to exclude from shuffle out of "+ numTracks +" selected items. "+ wereExcluded +" excluded items existed.");
}
else
{
WScript.Echo("UPDATE: Set "+ nowExcluded +" tracks to exclude from shuffle out of "+ numTracks +" items. "+ wereExcluded +" excluded items existed.");
txt.WriteLine("UPDATE: Set "+ nowExcluded +" tracks to exclude from shuffle out of "+ numTracks +" items. "+ wereExcluded +" excluded items existed.");
}
}
else
{
WScript.Echo("No changes... There were "+ wereExcluded +" exclude from shuffle items found in "+ numTracks +" selected items.");
txt.WriteLine("No shuffle changes were made to any tracks.");
txt.WriteLine("There were "+ wereExcluded +" exclude from shuffle items found in "+ numTracks +" selected items.");
}
}
if (verbose == false)
{
WScript.Echo(nowBookmarked +" tracks changed out of "+ numTracks +" selected tracks.");
}
else
{
txt.Close();
}

7:34 PM  
Blogger Jay Penney said...

Thanks! You just saved me HOURS of time!

8:16 AM  
Anonymous Matt said...

Thank you for making both your Python and the JS scripts available. They both work perfectly and have saved me hours of time and stress.

2:40 AM  
Anonymous Anonymous said...

This is exactly what I have been looking for, however I cannon get any of the above scripts to function. I get the following error: "Automation server can't creat object" any help would be greatly appricated.

11:37 AM  
Blogger JAG_67 said...

This is exactly what I have been looking for, however I cannon get any of the above scripts to function. I get the following error: "Automation server can't creat object" any help would be greatly appricated.

11:41 AM  
Blogger Rong said...

Some googling leads me to believe you need to upgrade your Windows Scripting version. You can download it here: http://msdn.microsoft.com/library/default.asp?url=/downloads/list/webdev.asp

6:11 PM  
Blogger JAG_67 said...

Thanks a million, works like a champ, also got the python script working as well.

5:28 PM  
Anonymous Anonymous said...

Unfortunately I wasn't able to get it to work either.

I installed python, ran the script, but I still wasn't able to get iTunes to recognize the command.

I'm running the most current version of iTunes on Windows XP.

Can you please walk through, step by step, on how to accomplish this? Seriously, I would be so grateful! I have 876 comedy songs that I don't want to include when shuffling!

3:20 PM  
Blogger Rong said...

The only thing I can think of is you need to first select all the tracks you want to change inside iTunes, then run the script. The script looks at all the selected tracks as the starting point, so if you don't select anything in iTunes, it's not going to work.

3:38 PM  
Anonymous Anonymous said...

Here's what I did.

1) Installed Python.
2) Copied the above sript into Notepad, and renamed it iTunes.py
3) Selected the songs in iTunes, then double-clicked the .py (icon looks like a snake).
4) Right-clicked the songs and did not have the "Skip when Shuffling" option when I selected "Get Info"

Am I doing something wrong?

Thank you!

12:16 PM  
Blogger Rong said...

Doesn't sound you are doing anything wrong. Do you see any error messages anywhere? In the python console?

3:32 PM  
Blogger Omer said...

I have been looking for this for the last Two hours

Typecasting ...

You saved my goshdarned life.

God bless you

[Here's for hoping you read this...]

-Omer

4:21 AM  

Post a Comment

<< Home