9

I'm using key-value-coding to get all the artists from iTunes:

[self.iTunes valueForKeyPath:@"sources.@distinctUnionOfArrays.playlists.@distinctUnionOfArrays.tracks.artist"];

Now, this works fine. This is very efficient. I would also like to do the same with the album.

[self.iTunes valueForKeyPath:@"sources.@distinctUnionOfArrays.playlists.@distinctUnionOfArrays.tracks.album"];

The problem here is, that there are multiple albums with the same name, but not necessarily of the same artist. Is there a way to get a song of each album, so I can find out what artists it is, and also get the cover of it? I know there is NSPredicate, but this is very slow.

The specific code is not important, I only need the key-value coding part.

Thank you!

Daij-Djan
  • 49,552
  • 17
  • 113
  • 135
IluTov
  • 6,807
  • 6
  • 41
  • 103

2 Answers2

4

This is not a complete answer.

For the benefit of folks like me who didn't originally know where this was documented: You have to be on a Mac with iTunes installed, and then run the command

sdef /Applications/iTunes.app | sdp -fh --basename iTunes

which will magically create iTunes.h in your current working directory. Then you can trawl through iTunes.h to see how the API looks. It doesn't seem to be officially documented in the Mac Developer Library or anything.

Each iTunesTrack has an artist property in addition to the album property, so I would think that all you really want to do is return an array of unique tuples (album, artist).

Okay, so how can we get an array of tuples out of a single KVC query? We'd like to write something like sources.@distinctUnionOfArrays.playlists.@distinctUnionOfArrays.tracks.albumAndArtist and have that return something like an NSArray of NSDictionary, for example @[ @{ @"Help!", @"The Beatles" }, @{ @"No!", @"They Might Be Giants" }, ... ]

EDIT ughoavgfhw suggested the next step in a comment: You could use a category to define albumAndArtist like so:

@interface iTunesTrack (albumAndArtist)
@property (readonly) NSDictionary *albumAndArtist;
@end

@implementation iTunesTrack (albumAndArtist)
-(NSDictionary *) albumAndArtist
{
    return @{ @"album":self.album, @"artist":self.artist };
}
@end

And now you can write the line we were trying to write:

[self.iTunes valueForKeyPath:@"sources.@distinctUnionOfArrays.playlists.@distinctUnionOfArrays.tracks.albumAndArtist"];

This should give you an NSArray of NSDictionary, which is basically what you wanted. Mind you, I haven't tested this code or anything!

Quuxplusone
  • 23,928
  • 8
  • 94
  • 159
  • 2
    You could use a category to define an `albumAndArtist` property which returns the dictionary, then use that property with KVC to get all of the different pairs. – ughoavgfhw Sep 19 '12 at 19:38
  • You guys are geniuses! The only thing I had to change was the category class. iTunesTrack is not a real class. I had to do it with SBObject. But the rest worked like a charm, thank you so much! – IluTov Sep 20 '12 at 10:26
  • Sorry, I still have a question. It works, but unfortunately it's still a lot slower then using a built in property. Can you tell why? – IluTov Sep 20 '12 at 11:08
  • I tried returning for example only the artists-string, which was still very very slow... -(NSString *) albumAndArtist { return [(ITunesFileTrack *)self album]; } – IluTov Sep 20 '12 at 11:09
  • 1
    Re "slower than builtin property": Well, constructing a new NSDictionary is slow, compared to returning an existing NSString. Re "slower than returning just the artist": I would expect `[self album]` to be a lot faster than `@{ ... }`, but it *is* still doing that one extra method dispatch. I'd naively expect `albumAndArtist { return [self album]; }` to be twice as slow as `album`, because there are twice as many method dispatches. If you really need it to be blazing fast, maybe see http://www.mikeash.com/pyblog/performance-comparisons-of-common-operations.html – Quuxplusone Sep 20 '12 at 17:51
  • I still don't get how the property album can be lightning fast, and a simple albumAndArtist { return [self album]; } property can be very very very slow, but I decided to let it go :) – IluTov Oct 11 '12 at 12:43
1

@Ilija: If you post to say you've let it go, then you haven't really let it go yet. ;) Let me see if I can clarify my comment:

-(NSString *) album
{
    return self->album;
}

-(NSDictionary *) albumAndArtist
{
    return @{ @"album":self.album, @"artist":self.artist };
}

The album method above is what the Objective-C compiler will generate for you automatically.* The albumAndArtist method is what I suggested in my answer to your original question. Now, if you ask Clang to lower these two methods to C (clang -rewrite-objc test.m -o test.cc), you get something like this:

static NSDictionary * _I_iTunesTrack_albumAndArtist_albumAndArtist(iTunesTrack * self, SEL _cmd) {
    return ((NSDictionary *(*)(id, SEL, const id *, const id *, NSUInteger))(void *)
    objc_msgSend)(objc_getClass("NSDictionary"), sel_registerName("dictionaryWithObjects:forKeys:count:"),
    (const id *)__NSContainer_literal(2U, ((NSString *(*)(id, SEL))(void *)objc_msgSend)
    ((id)self, sel_registerName("album")), ((NSString *(*)(id, SEL))(void *)objc_msgSend)
    ((id)self, sel_registerName("artist"))).arr, (const id *)__NSContainer_literal(2U,
    (NSString *)&__NSConstantStringImpl_test_m_0, (NSString *)&__NSConstantStringImpl_test_m_1).arr, 2U);
}

or, in human terms,

-(NSDictionary *) albumAndArtist
{
    id album = objc_msgSend(self, sel_registerName("album"));
    id artist = objc_msgSend(self, sel_registerName("artist"));
    id *values = calloc(2, sizeof(id)); values[0] = album; values[1] = artist;
    id *keys = calloc(2, sizeof(id)); keys[0] = @"album"; keys[1] = @"artist";
    Class dict_class = objc_getClass("NSDictionary");
    id result = objc_msgSend(dict_class, sel_registerName("dictionaryWithObjects:forKeys:count:"), values, keys, 2);
    free(values); free(keys);
    return result;
}

Check it out: three sel_registerNames, one objc_getClass, three objc_msgSends, two callocs, and two frees. That's pretty inefficient, compared to the compiler-generated album method.

Technically, the compiler-generated album method looks like this:

-(NSString *) album
{
    return objc_getProperty(self, _cmd,
        __OFFSETOFIVAR__(struct iTunesTrack, album), /*atomic*/ YES);
}

but that's only because it wasn't originally declared as nonatomic. For the meaning of objc_getProperty, see here; but basically, it's faster than an objc_msgSend would have been.)

So clearly albumAndArtist is going to be much slower than album, because of all that extra work it's doing. But — you ask — what if we get rid of all that work and just return self.album? Well, the generated code is still hairier than what the compiler generated for the album getter:

-(NSString *) albumAndArtist_stripped_down
{
    // return self.album;
    return objc_msgSend(self, sel_registerName("album"));
}

When your program calls myTrack.album, it invokes objc_msgSend once to figure out that it should call the album method, and then inside album it invokes objc_getProperty. That's two calls. (Three if you count selector_registerName("album").)

When your program calls myTrack.albumAndArtist_stripped_down, it invokes objc_msgSend once to figure out that it should call the albumAndArtist_stripped_down method, and then that calls objc_msgSend a second time, and then that calls objc_getProperty. That's three calls. (Five if you count selector_registerName.)

So it makes sense to me that albumAndArtist_stripped_down should be about twice as slow (or 5/3 as slow) as album by itself.

And as for the original albumAndArtist, just by counting the function calls, I'd expect it to be about five times as slow as album... but of course it'll be much slower than that, because it's doing at least three memory allocations, whereas album isn't doing any. Memory allocation and cleanup is very expensive, because malloc is a complicated algorithm in its own right.

I hope this clears up the issue for you. :)

Community
  • 1
  • 1
Quuxplusone
  • 23,928
  • 8
  • 94
  • 159
  • Wow, thanks a lot for your explanation! I wouldn't have thought it made that much of a difference. Cleared thing up for me :) – IluTov Oct 11 '12 at 19:15
  • After some programming experience I realize what was really going on at this time. Actually, this really has very little to do with nonatomic methods, but rather with AppleScript. Scripting Bridge automatically makes calls to AppleScript when necessary, which are very expensive. There are methods in `SBElementArray` which wrap up these calls, so if you want every track of the library, it will deliver them all at once. However, if you iterate through the array yourself, it will have to make a call for every single object. Just in case you were wondering. – IluTov Sep 10 '13 at 05:51