Apple's iTunes program is a reasonably good MP3 player for casual music listening, but at present (v2.0.3, released in December 2001) it has a number of annoying limitations, shared by Apple's hardware MP3 player iPod. (Here's why I think so: the present page is an attempt at slightly more constructive criticism.) Promisingly, iTunes has become Scriptable, meaning that in principle it's now possible to write AppleScripts which automate tasks, perform useful tidying-up exercises and so on, making good some of the missing features in the original program. The present exercise seemed like a good test case.
I listen to a lot of audio books, and since I haven't normally got 11 hours 15 minutes to spare at a single stretch, this means listening in installments. Typical MP3 files here are, say, 40 to 70 minutes long. What I need is a way of marking my place and returning to it.
Installing Bookmark. Download the script (and a spare copy of this page) by clicking this link. The compiled script should be placed in your personal
Library/iTunes/Scripts
folder (you will need to create this folder if your "Library/iTunes" folder doesn't contain it already). Note that this is the Library folder in your home folder, not the system's Library folder at the root of the machine. You then need to make one small modification to the script. Double-click on it, and it will open in the Script Editor program, which is supplied as part of every OS X installation. Alter the filename at the top of the file, changing "Macintosh HD" to the name of your own hard disc (in the unlikely event that your disc isn't called this anyway), and changing "gnelson" to your own user name (in the highly likely event that you aren't also called G. Nelson). Save and close.
Using Bookmark. Run iTunes, and the Script menu, which looks like a scroll, should now offer Bookmark as an option. When you choose this, a dialogue box appears which lists the current bookmarks and offers three options: Add, Remove and Play. The Add option will only work if you're currently playing a track, for obvious reasons.
How are the bookmarks stored? I tried all kinds of cute tricks with playlists, but eventually gave up and stored the information in a separate file (i.e., not in ID3 information, etc.). Bookmarks are therefore stored at
Documents/iTunes/iTunes Bookmarks.txt
with each bookmark looking like this:
Steve Jobs vs Godzilla at 10:05
557
605
The first line is what the user sees, but is otherwise ignored; the second is the iTunes database ID number of the track; the third is the position within that track, in seconds. Thus 605 seconds equals 10 minutes, 5 seconds, or "10:05".
Can I steal, improve, etc., the script? Be my guest; it's freeware and so forth, not that it's much of a ware. Please email me if you do improve it, though; I'd like to know where I went wrong.
What needs improving? Firstly, the filename nonsense in the installation could be avoided if I could work out how to get the pathname of the user's home folder in Mac OS X; the "~" used in Posix-style pathnames doesn't seem to work in AppleScript's rather OS 9-like way of thinking. The Finder has "home", but switching to the Finder doesn't work well in an iTunes script, so I don't want to do that unless necessary. There must be an easy way. Secondly, the browser should switch to the rest of the current album when a bookmarked track is resumed, so that play flows forward into the next tracks of an audio book (e.g.) in a natural way. But iTunes just isn't scriptable enough. Thirdly, the front end is a little crude? Fourthly, there must surely be a way to avoid the strange business of telling iTunes to play in one tell, and then set the player position to X in another tell altogether. (I was forced into this by some very buggy behaviour from iTunes.)
So iTunes is wonderfully adaptable now, is it? That's the theory. The reality is a little different. The implementation feels shoddy and the internal data structures don't quite behave as implied by the dictionary entries. For instance the track whose database ID is X fails, but every track whose database ID is X reliably produces a list of size 1 containing the unique such track. Conversely every playlist whose name is not "Library" fails because of a bug (see the comments in Apple's sample scripts) and bugs further impede rapid switching between Finder and iTunes. Testing a script by running it from the Script Editor (free with all copies of Mac OS X: see the AppleScript folder in the Applications folder) is of little use, because iTunes behaves and even displays quite differently on the same code when the script is run from its own menu. The upshot is that it took about two whole days to write this rather trivial script.
The ID3 tagging in iTunes (that is, the naming of tracks, their albums, genres, track numbers, etc.) is highly scriptable, with the major exception that the version of ID3 used is invisible and unscriptable; but almost nothing else is available, even single-mouse-click or menu-choice actions. You can't command iTunes to convert a file to MP3, for instance, so writing AppleScripts to batch process this most boringly batchy of jobs isn't possible. You can't change the selection displayed on iTunes's window(s), you can't go in or out of browse view, you can't re-sort the stuff showing; it isn't even all that easy to play a given track. Of the 28 sample iTunes scripts by Apple, not one of them plays a different track to the one currently playing. You can't create new Libraries, though you can create new playlists.
But for all the critical comments on this page, scriptability is still a very welcome development to a basically good if aggravatingly simplistic program. It's just that it's only about 25% finished.
What does the script look like? Like this:
-- Bookmark v1.0: a very small addition to iTunes
--
-- By Graham Nelson, but freeware, and please feel free to copy,
-- adapt, etc., at your own risk.
--
-- This is an AppleScript for iTunes 2.0.3 or later, running under Mac OS 10.1.2
-- or later. The compiled script should be placed in your personal
-- "Library:iTunes:Scripts folder" (you will need to create this
-- folder if your "Library:iTunes" folder doesn't contain it already).
-- Note that this is the Library folder in your home folder, not the
-- system's Library folder at the root of the machine.
-- And you need to change one detail in this script: your hard disc
-- and user name in the filename a few lines below this. Sorry, but
-- ridiculous as it is, I can't find how to access this automatically
-- within AppleScript, which won't accept ~/... style filenames.
property required_version : "2.0.3"
property bookmark_filename : "Macintosh HD:users:gnelson:Documents:iTunes:iTunes Bookmarks.txt"
property no_bookmarks : 0
property jump_to_position : 0
--tell application "Finder"
-- try
-- set frog_filename to ((path to desktop) & "Documents:iTunes:iTunes Bookmarks.txt")
-- display dialog frog_filename buttons {"OK"} default button 1
-- on error error_message number error_number
-- if the error_number is not -128 then
-- beep
-- display dialog error_message buttons {"OK"} default button 1
-- end if
-- end try
--end tell
tell application "iTunes"
activate
set jump_to_position to 0
try
-- VERSION CHECK
set this_version to the version as string
if this_version is not greater than or equal to the required_version then
beep
display dialog "This script requires iTunes version: " & required_version & ¬
return & return & ¬
"Current version of iTunes: " & this_version buttons {"Update", "Cancel"} default button 2 with icon 2
if the button returned of the result is "Update" then
my access_website("http://www.apple.com/itunes/download/")
return "incorrect version"
end if
end if
-- stop
-- Now attempt to read in the Bookmarks file
try
set bookmark_list to {}
set no_bookmarks to 0
set choosefrom_list to {}
set show_list to ""
set the target_file to the bookmark_filename as text
set the bookmark_file to open for access file target_file
set line1 to "at"
repeat while line1 contains "at"
try
set line1 to read bookmark_file before return
set line2 to read bookmark_file before return
set line3 to read bookmark_file before return
on error
set line1 to ""
end try
set bookmark_list to (bookmark_list & {line1, line2, line3})
set choosefrom_list to (choosefrom_list & {line1})
set no_bookmarks to no_bookmarks + 1
set old_show_list to show_list
set show_list to show_list & return & (no_bookmarks as string) & ": " & line1
end repeat
set no_bookmarks to no_bookmarks - 1
set show_list to old_show_list
close access the bookmark_file
on error error_message
-- This occurs presumably because there is no Bookmarks file
-- display dialog "Early err " & error_message buttons {"OK"} default button 1
try
close access file target_file
end try
set no_bookmarks to 0
end try
if no_bookmarks is 0 then
set show_list to return & "No bookmarks have been set"
end if
-- Now offer the main choice
display dialog "iTunes Bookmarks Service 1.0" & return & show_list & return ¬
buttons {"Add", "Remove", "Play From"} default button 3
set main_choice to the button returned of the result
if main_choice is "Add" then
try
set the total_seconds to the duration of the current track
set the total_seconds to the total_seconds as integer
on error
error "There is no current track."
return
end try
set the current_time to the player position
set the current_id to the database ID of the current track
set the current_name to the name of the current track
set no_seconds to the current_time as number
set no_minutes to no_seconds div 60
set no_seconds to no_seconds mod 60
if no_seconds < 10 then
set seconds_text to "0" & (no_seconds as string)
else
set seconds_text to no_seconds as string
end if
set the display_name to (current_name as string) & ¬
" at " & (no_minutes as string) & ":" & seconds_text
set the bookmark_data to display_name & return & (current_id as string) ¬
& return & (current_time as string) & return
display dialog "Bookmarking" & ¬
return & return & ¬
display_name ¬
buttons {"Cancel", "OK"} default button 2
if the button returned of the result is "OK" then
set the target_file to bookmark_filename
try
set the target_file to the target_file as text
set the open_target_file to ¬
open for access file target_file with write permission
write bookmark_data to the open_target_file starting at eof
close access the open_target_file
on error error_message
try
close access file target_file
end try
return "File access error"
end try
end if
return "Bookmark added"
end if
if main_choice is "Remove" then
if no_bookmarks is 0 then
display dialog "No bookmarks have been set." buttons {"OK"} default button 1
return
end if
set choice to (choose from list choosefrom_list ¬
with prompt ¬
"Remove which bookmarks?" OK button name ¬
"Remove" with multiple selections allowed)
if choice is false then
return "User decided not to"
end if
display dialog choice as string buttons {"OK"} default button 1
set new_bookmark_file to ""
repeat with m from 1 to no_bookmarks
set line1 to item ((3 * m) - 2) of bookmark_list
set line2 to item ((3 * m) - 1) of bookmark_list
set line3 to item (3 * m) of bookmark_list
set still_allowed to true
repeat with n from 1 to (count of choice)
if line1 is equal to (item n of choice) then
set still_allowed to false
end if
end repeat
if still_allowed is true then
-- display dialog (m as string) & " allowed " buttons {"OK"} default button 1
set new_bookmark_file to new_bookmark_file & line1 & return & line2 & return & line3 & return
else
-- display dialog (m as string) & " disallowed " buttons {"OK"} default button 1
end if
end repeat
try
set the target_file to bookmark_filename
set the target_file to the target_file as text
set the open_target_file to ¬
open for access file target_file with write permission
set eof open_target_file to 0
write new_bookmark_file to the open_target_file
close access the open_target_file
on error error_message
try
close access file target_file
end try
return "File access error"
end try
-- display dialog "Remove not available yet" buttons {"Sorry!"} default button 1
return
end if
if main_choice is "Play From" then
if no_bookmarks is 0 then
display dialog "No bookmarks have been set." buttons {"OK"} default button 1
return
end if
set choice to (choose from list choosefrom_list ¬
with prompt ¬
"Play from which bookmark?" OK button name ¬
"Play" without multiple selections allowed)
if choice is false then
return "User decided not to"
end if
set choice to the first item of choice
repeat with n from 1 to (count of bookmark_list)
if choice is equal to (item n of bookmark_list) then
set unique_id to item (n + 1) of bookmark_list
set seconds_in to item (n + 2) of bookmark_list
set unique_id to unique_id as number
set seconds_in to seconds_in as number
end if
end repeat
tell source "Library"
tell playlist "Library"
--
-- We are forbidden to say"the track whose database ID is X" as there
-- may be multiple copies of the same MP3 file in the database,
-- i.e., database IDs are not (as the dictionary implies) unique;
-- instead"every track whose database ID is X" successfully
-- produces a list of size 1, containing the answer.
--
set inefficient to (every track whose database ID is unique_id)
repeat with i in inefficient
-- display dialog (name of i as string) buttons {"OK"} default button 1
set track_in_question to i
end repeat
end tell
end tell
-- Finally instruct iTunes to act
-- stop
play track_in_question
set jump_to_position to seconds_in
end if
on error error_message number error_number
if the error_number is not -128 then
beep
display dialog error_message buttons {"OK"} default button 1
end if
end try
end tell
-- A variety of baffling behaviours result from attempting to
-- set the player position in the same tell as the play command,
-- once the script is compiled (it works fine if run from the editor);
-- hence this clumsy second tell. Presumably another iTunes bug.
tell application "iTunes"
activate
if jump_to_position &Mac179; 0 then
set the player position to jump_to_position
end if
end tell
on access_website(this_URL)
ignoring application responses
tell application "Finder"
open location this_URL
end tell
end ignoring
end access_website