jwz - AppleScript [entries|archive|friends|userinfo]
jwz

  www.jwz.org
  userinfo
  archive
  rss

Links
[»| [DNA Lounge] [Blog] [iCal] ]
[»| [DNA Lounge Legal Defense Fund] ]
[»| [WebCollage] [LJ WebCollage] ]

AppleScript [Wed, 7-Dec-2005 5:37 AM]
Previous Entry Add to Memories Tell a Friend Next Entry
[Tags|, , ]
[music |(the non-music of iTunes using 78% CPU)]

AppleScript sure is horrible. And slow. I wanted to get a list of the file names of the songs in a given playlist, and doing it like this takes hours when there are a few thousand songs in the playlist:

    tell application "iTunes"
      repeat with f in (every file track of playlist "Foo")
        set output to output & (get location of f) & "\n"
      end repeat
    end tell

Maybe I'd be better off groveling through the "iTunes Music Library.xml" file...

It's pretty crazy that there seems to be no way to write to stdout besides appending to the magic "output" variable (which writes it all at once, at the end) or invoking a sub-shell to run "echo".

Do any of the Perl AppleScript bindings (there seem to be at least two of them?) just invoke "osascript", or do they do something less stupid and more efficient?

linkReply

Comments:
[User Picture]From: [info]ultranurd
Wed, 7-Dec-2005 2:31 PM (UTC)

(Link)

I don't know if you're using 10.4, but I would expect that Automator actions would be faster because they're compiled Objective-C objects. This assumes that an Automator action exists for iTunes that does what you want.
[User Picture]From: [info]wetzel
Wed, 7-Dec-2005 11:18 PM (UTC)

(Link)

i assume you've never actually used automator.

my god it's slow.
[User Picture]From: [info]allartburns
Wed, 7-Dec-2005 3:13 PM (UTC)

(Link)

I think you're better off parsing the xml yourself and if you want updates, checking every N units of time to see if it's changed.
[User Picture]From: [info]darkshadow2
Wed, 7-Dec-2005 3:19 PM (UTC)

(Link)

Here, this bit takes a lot less than hours for me. I left in a couple of debug lines, they add a listing of the time when it starts, and the number of songs, then the time when it ends. Strip those out if you're happy with the script. Also, it doesn't add things to the output variable, it adds them to a list then returns the list. This gives the output as comma separated. Oh, and I set it to output POSIX paths rather than Applescript's default Mac style paths.

tell application "iTunes"
    set returnList to {}
    copy ((current date) as string) to the end of returnList
    set thePlaylist to playlist "Library"
    copy (count of every track in thePlaylist) to the end of returnList
    set allTracks to every track of thePlaylist
    repeat with theTrack in allTracks
        set theLocation to theTrack's location
        copy the POSIX path of theLocation to the end of returnList
    end repeat
    copy ((current date) as string) to the end of returnList
end tell

return returnList


For me, it gives this output (I used the -ss option to osascript to output the list with quotes around the strings):

{"Wednesday, December 07, 2005 9:45:06 AM", 2055, "/Users/poetman/Music/iTunes/iTunes Music/Compilations/Alice In Chains_ Greatest Hits/Would_.m4p", (lots of others deleted for brevity), "/Users/poetman/Music/iTunes/iTunes Music/Echolaylia/The 27th Letter/Pennsylvania.mp3", "Wednesday, December 07, 2005 9:45:42 AM"}

2055 is the number of songs in my music library (OK, so I'm not big on collecting songs). More will add to the time, but it only took 36 seconds to go through all 2055, and I wouldn't think it'd be much more than a minute for double that.
[User Picture]From: [info]jwz
Thu, 8-Dec-2005 12:51 AM (UTC)

-ss

(Link)

Apparently "-ss" is the fastlane to notworkingland. It slows it down a lot, and tends to do this:
    osascript: script had a result, but couldn't display it.: Cannot allocate memory
I imagine they implemented -ss in applescript, sigh.
[User Picture]From: [info]roisnoir
Wed, 7-Dec-2005 3:25 PM (UTC)

(Link)

Is there a reason it needs to be a script?

If it's a one-time thing, or an occasional one, what I do is go to the playlist, make it show me only the fields I'm looking for (usually just track name and artist), then select all, copy, and paste into a text file.
[User Picture]From: [info]coolerq
Wed, 7-Dec-2005 3:27 PM (UTC)

(Link)

Even simpler:

tell application "iTunes" to get the location of every file track of playlist "Foo"

Does it all in one go, as opposed to thousands of individual AppleEvents.

--Quentin
[User Picture]From: [info]spike
Wed, 7-Dec-2005 4:07 PM (UTC)

(Link)

Exactly.

I've been working with AppleScript since it was knee high to a grasshopper, and one of the things I've learned is that you want to minimize the number of operations that you perform. The two biggest ways to do that, IMNSHO, are:

(1) When you want to get the same property from every object in a set, use an approach like [info]coolerq's above:
    set valuelistvar to the property of every element of container

(2) When you want to select certain items from a group, use a "whose" clause instead of a loop..test..accumulate setup:
    set objectlistvar to every element of container whose propertyname is value
You can actually use any expression for the whose clause, not just "is".

You can combine these two approaches to get selected information from selected objects all in one go:
    tell application "iTunes"
        get the location of every file track of playlist "Dark Pulse 4" whose name contains "remix"
    end tell


Many applications have native implementation of whose-clause resolution, which lets them use their own internal indicies, data structures, caches, hacks, and so on to return the answer as fast as possible. (FileMaker is a classic example: if you say 'get every record of database "DB" whose field "area code" is "212"', the result is nearly instant since FileMaker actually uses it's 'database' data structures to find the matching records all at once.) Not all applications support native whose-clause resolution, and applications that do provide it don't have to provide it for every possible query type. If the app doesn't provide a native whose-clause resolver, the AppleScript engine runs the loop for you and accumulates the results, which is still faster than writing the loop in AppleScript yourself.

Now, just so that you don't think I'm some kind of AppleScript apologist, here's the absolute suckiest implementation issue in the whole language at it exists today: the AppleScript "list" object does not support whose clauses. So you can't do the obvious thing you're going to want to:
    get every item of myobjectlist whose property operator value
The omission of this single feature leads directly to the need to iterate over lists with repeat and accumulate the results "by hand", all of which slows everything down and causes most AppleScript programs to revert to their most base, vile, and slothful nature.
[User Picture]From: [info]darkshadow2
Wed, 7-Dec-2005 4:05 PM (UTC)

(Link)

Well, that beats my script. Gah, why didn't I think of that?

jwz, if you like the idea of getting back the POSIX paths, then this script'll do it. Same setup on my end as before, but this only takes 6 seconds rather than 36. I'd say that's a win.

tell application "iTunes"
    set returnList to {}
    set locationList to the location of every file track of playlist "Library"
    repeat with theLocation in locationList
        copy the POSIX path of theLocation to the end of returnList
    end repeat
end tell

return returnList

[User Picture]From: [info]ivan_ghandhi
Wed, 7-Dec-2005 4:40 PM (UTC)

(Link)

Maybe you'd better use some more civilized (Ruby, Python) or less, but still civilized languages (Perl)?
[User Picture]From: [info]crimethnk
Wed, 7-Dec-2005 5:05 PM (UTC)

civilization--

(Link)

Mac::Glue is a perl module I wrote some years ago, which is now included in Tiger. It uses AppleScript vocabulary with Perl syntax, combining the "best" of both worlds ...

The slight downside is you need to create a "glue" for each app prior to using it. Docs are included with the module, and I can be asked for more information.

	use Mac::Glue ':glue';
	my $itunes    = new Mac::Glue 'iTunes';
	my $tracks    = $itunes->obj(file_tracks => library_playlist => 1);
	my $locations = $tracks->prop('location');

	for my $location ($locations->get) {
		print "$location\n";
	}


"whose" clauses are also possible:

	my $tracks    = $itunes->obj(
		file_track       => whose(NOT =>
			[ artist => contains => 'Kenny G' ]
		),
		library_playlist => 1
	);

And so on.
[User Picture]From: [info]mutiny
Wed, 7-Dec-2005 6:19 PM (UTC)

(Link)

That's what's beautiful about Appscript.

For example, to get the value of the first paragraph of the topmost document in TextEdit:

app('TextEdit').documents[1].paragraphs[1].get()

This is equivalent to the AppleScript statement:

get paragraph 1 of document 1 of application "TextEdit"
[User Picture]From: [info]marapfhile
Wed, 7-Dec-2005 5:05 PM (UTC)

AppleScript as Functional Language?

(Link)

Interesting approach--sounds almost like treating AppleScript as a functional language with techniques like map and filter.
[User Picture]From: [info]ckd
Wed, 7-Dec-2005 7:19 PM (UTC)

(Link)

You could use Python with PyObjC. Bob Ippolito's posted some sample code on his blog. Using the native XML parser to turn it into a Python dict is far, far faster than anything using AppleScript/osascript/the Python appscript bindings/etc.
From: [info]effbot
Sat, 10-Dec-2005 11:31 AM (UTC)

(Link)

You don't really need PyObjC to do this; any XML toolkit can deal with the iTunes file. With this PLIST parsing recipe (scroll down a bit) and few lines of dictionary drilling and ugly search loops, I can pull out the files for a playlist in just over a second for a 5 megabyte iTunes file. Most of the time is spent on PLIST processing, so you could probably speed this up 2-4 times by operating directly on the XML structure.

However, I suspect it will take more than j"ust over a second" to persuade JWZ to start tinkering with Python, so I'm not sure it would be a net win ;-)
From: [info]primitiveworker
Wed, 7-Dec-2005 10:06 PM (UTC)

(Link)

From the above code, except using a string (as you originally wanted to), taking the playlist as a commandline argument. 134 seconds for a smartplaylist with 2000 songs in it.

#!/bin/sh
osascript \
-e 'tell application "iTunes"' \
-e '    set returnString to ""' \
-e "    set locationlist to the location of every file track of playlist \"$1\"" \
-e '    repeat with theLocation in locationList' \
-e '        set returnString to returnString & the POSIX path of theLocation & "\n"' \
-e '    end repeat' \
-e 'end tell' \
-e 'return returnString'

Same, except using the original list from the above code and sed. It's a little more than twice as fast on my craptastic iBook2k. 63 seconds.

#!/bin/sh
osascript \
-e 'tell application "iTunes"' \
-e '    set returnList to {}' \
-e "    set locationlist to the location of every file track of playlist \"$1\"" \
-e '    repeat with theLocation in locationList' \
-e '        copy the POSIX path of theLocation to the end of returnList' \
-e '    end repeat' \
-e 'end tell' \
-e 'return returnList' | \
sed 's/,\ \//\
\//g'

Rather than use applescript's "quoted form" or other such yuck, I would continue piping through sed to transform the paths if need be.

Too bad iTunes doesn't natively dump .m3u playlists. Or, too bad I'm too dumb to figure out how to get iTunes to dump .m3u playlists. Maybe this is why you wanted to write this script in the first place?
[User Picture]From: [info]jwz
Thu, 8-Dec-2005 12:56 AM (UTC)

#!

(Link)

BTW, the trick to writing shell scripts directly in AppleScript is:
    #!/bin/sh
    exec osascript <<\EOF
    tell application "iTunes"
     ...

The backslash prevents sh expansion of $. You don't actually need to end the file with "EOF".

[User Picture]From: [info]baconmonkey
Thu, 8-Dec-2005 12:09 AM (UTC)

(Link)

the more applescript I see, the more I'm convinced that the design doc said nothing more than the following:

1. Watch all 80s movies that feature computers.
2. Create a syntax parser that will run every program or command line featured in those movies.
3. profit.
[User Picture]From: [info]jwz
Thu, 8-Dec-2005 12:57 AM (UTC)

(Link)

It's like some demented chimera of COBOL and SQL.
[User Picture]From: [info]ch
Thu, 8-Dec-2005 4:25 AM (UTC)

(Link)

ding! the (bacon)monkey wins the prize.
[User Picture]From: [info]avirr
Sun, 11-Dec-2005 2:00 AM (UTC)

(Link)

Close ;-) The folks who wrote it were designing an English-like scripting language based on... LISP!

The other main issue is that they didn't like multiple verbs, so everything is just set the foo to bar, and you have to guess about foo and bar. ARGHHHHH!
[User Picture]From: [info]kitten_moon
Thu, 8-Dec-2005 3:21 AM (UTC)

XSLT is also horrible

(Link)

... but in different ways.

Behold, a stylesheet:

<?xml version="1.0"?>
<xsl:stylesheet version="1.0" id="stylesheet"
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output method="text"/>
  <xsl:template match="xsl:stylesheet"/>
  
  <xsl:param name="playlist" select="'Bootylicious'"/>

  <xsl:variable name="plarray"
    select="/plist/dict/array[preceding-sibling::key[1]='Playlists']"/>

  <xsl:key name="tracks"
    match="/plist/dict/dict[preceding-sibling::key[1]='Tracks']/key"
    use="."/>

  <xsl:template match="/">
    <xsl:apply-templates
      select="$plarray/dict[key[.='Name' and following-sibling::string[1]=$playlist]]"
      mode="playlist"/>
  </xsl:template>

  <xsl:template match="dict" mode="playlist">
    <xsl:for-each select="array/dict">
      <xsl:variable name="trackid" select="integer"/>
      <xsl:variable name="track"
        select="key('tracks',$trackid)/following-sibling::dict[1]"/>

      <xsl:value-of select="$track/string[preceding-sibling::key[1]='Name']"/><xsl:text>
</xsl:text>
    </xsl:for-each>
  </xsl:template>

</xsl:stylesheet>


Save this to your iTunes directory, then apply it to your XML file like so:

xsltproc --novalid --stringparam playlist "Name of your playlist here" \
makeplaylist.xsl "iTunes Music Library.xml"


Output will be a list of file://localhost/ URLs, which approximates what you want I think?

(This script adapted from some other stuff I had lying around, hence the wierd mode="playlist" stuff. Whatever, it works for me)
[User Picture]From: [info]kitten_moon
Thu, 8-Dec-2005 3:36 AM (UTC)

Re: XSLT is also horrible

(Link)

xsltproc available here

Or use another XSLT engine.
[User Picture]From: [info]brianfeldman
Thu, 8-Dec-2005 4:54 AM (UTC)

Oh jesus. Here.

(Link)

ITunes.rb: pastebin'd!
DB_SQL.rb: pastebin'd!
[User Picture]From: [info]brianfeldman
Thu, 8-Dec-2005 4:55 AM (UTC)

Re: Oh jesus. Here.

(Link)

Oh, and you can do, like... queries on it. And stuff.
SELECT SUBSTRING(artist FROM 1 FOR 30) AS artist,
           CAST(CAST(AVG(CASE WHEN rating = 0 THEN
                              (SELECT AVG(rating)
                               FROM tracks r
                               WHERE r.rating <> 0 AND
                                     r.archive_id = ot.archive_id) 
                          ElSE
                              rating
                         END) AS INTEGER)
                AS CHAR(4)) || '%' as atr,
           COUNT(*) AS considered,
           (SELECT COUNT(u.*)
            FROM tracks u
            WHERE (u.archive_id, u.artist, rating) =
                  (ot.archive_id, ot.artist, 0)) AS unrated
FROM tracks ot
WHERE archive_id = (SELECT archive_id
                    FROM archives
                    JOIN users USING (user_id)
                    WHERE (users.name,
                           host,
                           source_name,
                           source_kind) =
                          ('brianfeldman',
                           'macintosh.green.homeunix.org',
                           'Brian Feldman',
                           'iPod'))
GROUP by ot.artist, ot.archive_id
HAVING (SELECT COUNT(*) FROM (SELECT a.album, COUNT(*)
                              FROM tracks a
                              WHERE a.archive_id = ot.archive_id AND
                                    a.artist = ot.artist
                              GROUP BY a.artist, a.album) AS real_albums) > 1
ORDER BY atr DESC, artist
LIMIT 25;
[User Picture]From: [info]baconmonkey
Thu, 8-Dec-2005 7:17 AM (UTC)

(Link)

itunes:
File > Export Song list
Save as Type "text file"
filename: "poop.txt"

command line:
> cut -f 25 poop.txt
From: [info]jjminer
Tue, 13-Dec-2005 12:07 AM (UTC)

Presentation on XSL using iTunes Library

(Link)

On June 9th, one of our developers (Ogden Kent) did a great presentation on XSL using the iTunes Music Library XML document as an example.. He was entirely on Windows, but the XPath examples should work with anything.

http://www.doit.wisc.edu/webdev/archive/

Specifically:
http://www.doit.wisc.edu/webdev/archive/presentations/xsl_june_2005.ppt
http://www.doit.wisc.edu/webdev/archive/presentations/XSL-Presentation.zip

jon