5

Playing with the new JS for automation using Script Editor. I'm getting an error on the final line of the following:

var iTunes = Application("iTunes");
var sources = iTunes.sources();
var library = sources.whose({name : "Library"});

Confirmed that the sources array is as expected (two elements, one with name "Library" and one "Internet Radio"). But that final line chokes with Error on line 3: TypeError: undefined is not a function (evaluating 'sources.whose({name : "Library"})').

As far as I can tell, I'm using the right syntax for the whose function. (I also tried with an explicit _equals clause to the same result.) What am I doing wrong?

foo
  • 3,171
  • 17
  • 18
Ben Zotto
  • 70,108
  • 23
  • 141
  • 204

4 Answers4

10

What am I doing wrong?

Short answer: it's not your fault. The JXA documentation is a sack of lies.

Longer explanation: an object's elements have nothing to do with Arrays. They represent a one-to-many relationship in an object graph, in this case, between a source object and zero or more library objects.

While many relationships may reflect the underlying implementation's containment hierarchy, there is no obligation to do so; e.g. Finder allows you to identify objects on the desktop in multiple ways:

items of folder "Desktop" of folder "jsmith" of folder "Users" of disk "Macintosh HD" of app "Finder"
items of folder "Desktop" of folder "jsmith" of folder "Users" of startup disk of app "Finder"
items of folder "Desktop" of home of app "Finder"
items of folder "Macintosh HD:Users:jsmith:Desktop" of app "Finder"
items of desktop of app "Finder"
items of app "Finder"
[etc.]

Apple event-based application scripting is based on remote procedure calls plus simple first-class queries. It's not OOP, regardless of superficial appearance: that's just syntactic sugar to make its queries easy to read and write.

...

In this case, your second line is telling iTunes to get a list (Array) of query objects (ObjectSpecifiers) that identify each of the source objects in your iTunes application:

var iTunes = Application("iTunes");
var sources = iTunes.sources();

Once you've got an Array, you can't use it to construct further queries, because JavaScript doesn't know how to build queries itself. What you actually want is this:

var iTunes = Application("iTunes");
var sourcesSpecifier = iTunes.sources;
var librarySpecifier = sourcesSpecifier.whose({name : "Library"});

That will give you an object specifier that identifies all of the source objects whose name is "Library". (If you only want to specify the first source object named "Library", use the byName method instead of whose; it's simpler.)

--

Personally, I consider all this somewhat academic as JXA's Apple event bridge implementation, like its documentation, is mostly made of Lame and Fail anyway. It mostly works up to a point, then poops on you beyond that. If your needs are modest and it's "good enough" to do then more power to you, but for anything non-trivial stick with AppleScript: it's the only supported solution that works right.

(The AppleScript/JXA team has no excuse for such crap work either: I sent them an almost-finished JavaScriptOSA reference implementation months ago to study or steal from as they wished, which they totally ignored. So you'll excuse my pissiness, as this was a solved problem long ago.)

foo
  • 3,171
  • 17
  • 18
  • Many thanks for the detailed response. This makes sense, or as much sense as I guess the underlying concepts allow. Appreciate the info and stability opinion before I've got into any large projects. – Ben Zotto Nov 23 '14 at 23:07
  • Can you help me understand one more thing? The `byName` hint here works great, but the `whose` syntax doesn't result in an object I know what to do with. E.g. in your snippet, once I have `librarySpecifier`, I don't know how to use it. Presumably I want to turn that thing into an array of actual objects at some point (or continue to build a deeper specifier with it) but doing pretty much anything with it that triggers an event (like calling `.name()`, with or without first appending `at(0)`/`[0]`) results in an "Error -1700: Can't convert types". Enlightenment appreciated. – Ben Zotto Nov 24 '14 at 04:19
  • 2
    Huh. Setting Script Editor's result window to show the outgoing events proves instructional. Even the simplest use case, `Application("iTunes").sources.whose({name:'Library'}).get()`, somehow gets interpreted as `app.sources.whose({name: "Library"}).sources.byName("get")()`. In other words, it's broken. I'm not even going to bother guessing how it manages to arrive at this nonsense - JXA's entire AE bridge and OSA component implementations are so fundamentally incompetent the only way it'll ever work correctly is to throw the whole lot out and redo from scratch. – foo Nov 24 '14 at 09:28
  • 1
    FWIW, compare the reference implementation I sent them: `app("iTunes").sources.where(its.name.isEqual("Library")).get()` -> `[app("iTunes").sources.ID(73)]`, `app("iTunes").tracks.where(its.artist.contains("Sigur")).name.get()` -> `["(Intro)", "Svefn-g-englar", "Starálfur", ...]`, `app("iTunes").tracks.where(its.artist.isEqual("Sigur Ros")).duplicate({to: app("iTunes").playlists.named("Icelandic")})`, etc. (Still has some bugs, mind; I stopped work on it once I knew I was wasting my time. But a rock-solid production-quality design, proven and polished over the last 10 years.) – foo Nov 24 '14 at 09:51
  • OK, fair enough. I thought the events listed looked suspect, but wasn't sure how much was my misunderstanding. The same behvaior manifests on the command line with the `osascript` tool. I'll file a radar against this, although presumably they already know. This does render the environment unusable for anything beyond the fairly trivial sample snippets that Apple provides. – Ben Zotto Nov 24 '14 at 14:30
  • 1
    @foo, you are obviously very knowledgable about JXA. But I'd like to ask you about this statement you made above: >"Personally, I consider all this somewhat academic as JXA's Apple event bridge implementation, like its documentation, is mostly made of Lame and Fail anyway. It mostly works up to a point, then poops on you beyond that." Does that mean that you recommend not using JXA? – JMichaelTX Jan 11 '16 at 01:05
  • 1
    If you want something that speaks Apple events correctly, and has at least a modicum of community resources and user support to help you out when you get stuck, use AppleScript. It's a lousy language in every other respect, but it's the only currently supported option that does this stuff right. – foo Jan 11 '16 at 17:09
  • Your manual is extremely helpful. Can't recommend it enough. Thank you. – Michael Scott Asato Cuthbert Jun 26 '16 at 18:52
  • @foo, while I admire and often agree with your rantings about the deficiency of OSA-based languages, it appears you made a simple user error above (Huh.) by inserting a superficial "get" statement. This works just fine: `Application("iTunes").sources.whose({name:'Library'})()` and returns the correct result `[Application("iTunes").sources.byId(64)]` Did you mix your own reference implementation syntax with the official Apple syntax and break it yourself? – Ron Reuter Jul 11 '16 at 20:10
  • No, it's a design defect in JXA. I just used the `get` command because it's the simplest example to demonstrate the flaw. That there's a workaround in that particular case - using the contracted `ref()` form for `ref.get()` - is irrelevant, because it still breaks on every other command, e.g.: `iTunes.sources[0].tracks.whose({name:"bar"}).duplicate({to:iTunes.sources[0].playlists.byName("baz")})` -> `app.sources.at(0).tracks.whose({_match: [ObjectSpecifier().name, "bar"]}).tracks.byName("duplicate")()`. It's simple incompetence; if AppleScript can do it right, JXA has no excuse. – foo Jul 12 '16 at 10:03
2

This now works as theory would predict.

(function () {
    'use strict';

    var iTunes = Application('iTunes'),
        filtered = iTunes.sources.whose({
            name: 'Library'
        });

    return filtered().length;

})();
Ben Zotto
  • 70,108
  • 23
  • 141
  • 204
houthakker
  • 688
  • 5
  • 13
0

Based on the JavaScript for Automation Release Notes Mail.inbox.messages.whose(...) example, the following should work:

var iTunes = Application('iTunes');
var filtered = iTunes.sources.whose({name : 'Library'});

The apparent goal for the "special whose method" with "an object containing the query" is to be efficient by only bringing select items (or item references) from the OS X object hierarchy into the resulting JavaScript array.

However, ... the whose feature of JXA appeared to have some bugs in the earlier OS X v10.10 release.

So, since the array in question is small (i.e. 2 items), filtering can be done fast & reliably on the JavaScript side after getting the elements as a JS array.

Workaround Example 1

var iTunes = Application('iTunes');
var sources = iTunes.sources();
var librarySource = null;
for (key in sources) {
    var name = sources[key].name();
    if (name.localeCompare("Library") == 0) {
        librarySource = sources[key];
    }
}

Workaround Example 2

var iTunes = Application('iTunes');
var sources = iTunes.sources();
function hasLibraryName(obj) {
    var name = obj.name();
    if (name.localeCompare("Library") == 0) {
        return true;
    }
    return false;
}
var filtered = sources.filter(hasLibraryName); 
marc-medley
  • 8,931
  • 5
  • 60
  • 66
0

In OS X 10.11.5 this seems to be working just fine.

library = Application("iTunes").sources.whose({name:'Library'})()[0]

library.properties()

// {"class":"source", "id":64, "index":1, "name":"Library", "persistentID":"2D8F973150E0A3AD", "kind":"library", "capacity":0, "freeSpace":0}

Note the addition of () after the whose clause to resolve the object specifier into an array of references and then the [0] to get the first (and only, in my case) library object reference, which can then be used to get the properties of that library.

In case the source is not named "Library" in other languages or regions, I would probably use this instead:

library = Application("iTunes").sources.whose({kind:"kLib"})()[0]
Ron Reuter
  • 1,287
  • 1
  • 8
  • 14