some criteria peg CPU using Scripting Bridge and NSPredicate - cocoa

I'm trying to get a list of tracks out of iTunes via Scripting Bridge. I'm using NSPredicate because that's the recommended way. This works very well in some cases, and is unusably slow in others. For instance, this will execute very quickly:
NSString *formatString = #"artist == ABC AND album == XYZ";
NSPredicate *trackFilter = [NSPredicate predicateWithFormat:formatString];
NSArray *tracksToPlay = [[libraryPlaylist fileTracks] filteredArrayUsingPredicate:trackFilter];
(libraryPlaylist is an iTunesLibraryPlaylist object that was created elsewhere.)
But if I add either kind or videoKind to the mix, iTunes hits 100% CPU for a minute or more.
NSString *formatString = #"artist == ABC AND album == XYZ AND kind != 'PDF document' AND videoKind == %#", ;
NSPredicate *trackFilter = [NSPredicate predicateWithFormat:formatString, [NSAppleEventDescriptor descriptorWithTypeCode:iTunesEVdKNone]];
NSArray *tracksToPlay = [[libraryPlaylist fileTracks] filteredArrayUsingPredicate:trackFilter];
But that will eventually work. The real failure is albumArtist. If I try
NSString *formatString = #"albumArtist == ABC AND album == XYZ";
NSPredicate *trackFilter = [NSPredicate predicateWithFormat:formatString];
NSArray *tracksToPlay = [[libraryPlaylist fileTracks] filteredArrayUsingPredicate:trackFilter];
iTunes will go to 100% CPU and sit there for I don't know how long. (I gave up after 3 or 4 minutes.) Am I missing something or is this a bug in iTunes?
Additional info
My code takes the resulting tracks and calls another method to add them to a playlist (also using Scripting Bridge). I noticed when trying to filter by kind, the tracks would slowly pop onto the list one by one while iTunes hammered the CPU. This can only mean that filteredArrayUsingPredicate has already returned its results, so what is iTunes working so hard on?

Another post indirectly helped me find the answer.
Using the “Library” playlist causes a number of unusual problems. Using the “Music” playlist instead seems to fix them. In the example above, setting libraryPlaylist this way is what caused the problem:
iTunesApplication *iTunes = [SBApplication applicationWithBundleIdentifier:#"com.apple.iTunes"];
iTunesSource *library = [[[[iTunes sources] get] filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:#"kind == %i", iTunesESrcLibrary]] objectAtIndex:0];
iTunesLibraryPlaylist *libraryPlaylist = [[[library libraryPlaylists] objectAtIndex:0];
Getting the "Music" playlist instead of the "Library" playlist is the answer:
iTunesApplication *iTunes = [SBApplication applicationWithBundleIdentifier:#"com.apple.iTunes"];
iTunesSource *library = [[[[iTunes sources] get] filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:#"kind == %i", iTunesESrcLibrary]] objectAtIndex:0];
iTunesLibraryPlaylist *libraryPlaylist = [[[[library playlists] get] filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:#"specialKind == %i", iTunesESpKMusic]] objectAtIndex:0];
Other things to be aware of
The "albumArtist == ABC AND album == XYZ" filter in the original question was actually running pretty quickly. What's slow is anything you do with the result afterward. Calling get right away is a partial solution. (get runs as slow as anything else, but by doing it up front, you limit the slowness to a single operation. Also note that get only works on an SBElementArray.)
I've also found that calling fileTracks re-introduces some slowness. Using tracks instead fixes that. So the filter should read:
NSArray *tracksToPlay = [(SBElementArray *)[[libraryPlaylist tracks] filteredArrayUsingPredicate:trackFilter] get];
(When using "Library", only fileTracks would return objects with a location property, which you need to add them to a playlist. After switching to "Music", tracks seems to return objects with a location as well.)

Related

NSTreeController - Retrieving selected node

I added Book object in bookController (NSCreeController). Now i want to get stored Book object when i select the row.
- (IBAction)addClicked:(id)sender {
NSTimeInterval timeStamp = [[NSDate date] timeIntervalSince1970];
// NSTimeInterval is defined as double
NSUInteger indexArr[] = {0,0};
Book *obj = [[Book alloc] init];
NSString *dateString = [NSDateFormatter localizedStringFromDate:[NSDate date] dateStyle:NSDateFormatterNoStyle timeStyle:NSDateFormatterLongStyle];
obj.title = [NSString stringWithFormat:#"New %#",dateString];
obj.filename = [NSString stringWithFormat:#"%d",arc4random()%100000];
[self.booksController insertObject:obj atArrangedObjectIndexPath:[NSIndexPath indexPathWithIndexes:indexArr length:2]];
}
I concede there perhaps could be a better solution--
I am unfamiliar with how NSTreeController works, but I looked a the class reference and noticed that it has a content property, similar to an NSArrayController (Which I am familiar with grabbing specific objects from).
I believe that if the content property is actually of type of some kind of tree data structure, my answer here probably won't work. The class reference says this about content:
The value of this property can be an array of objects, or a
single root object. The default value is nil. This property is
observable using key-value observing.
So this is what I historically have done with the expected results:
NSString *predicateString = [NSString stringWithFormat:NEVER_TRANSLATE(#"(filename == %#) AND (title == %#)"), #"FILENAME_ARGUMENT_HERE", #"TITLE_ARGUMENT_HERE"];
NSArray *matchingObjects = [[self content] filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:predicateString]];
Then simply calling -objectAtIndex: will grab you your object. Note that the NSArray will be empty if the object doesn't exist, and if you have duplicate objects, there will be multiple objects in the array.
I also searched for an answer to your question, and found this SO thread:
Given model object, how to find index path in NSTreeController?
It looks pretty promising if my solution doesn't work, the author just steps through the tree and does an isEqual comparison.
If you could (if it's not too much trouble), leave a comment here to let me know what works for you, I'm actually curious :)

better way to find max date inside big pool of core data objects

i have big pool of core date objects (around 10000) and there is too long time doing code according profile:
NSDate *maxExternalChangedDate = [codes valueForKeyPath:#"#max.externalChangedDate"];
is community know better way to found it?
NSString *rateSheetID = [rateSheetAndPrefix valueForKey:#"rateSheetID"];
NSFetchRequest *requestCodesForSale = [[NSFetchRequest alloc] init];
[requestCodesForSale setEntity:[NSEntityDescription entityForName:#"CodesvsDestinationsList"
inManagedObjectContext:self.moc]];
[requestCodesForSale setPredicate:[NSPredicate predicateWithFormat:#"(%K.carrier.GUID == %#)",relationshipName,carrierGUID]];
NSError *error = nil;
NSArray *codes = [self.moc executeFetchRequest:requestCodesForSale error:&error];
if (error) NSLog(#"Failed to executeFetchRequest to data store: %# in function:%#", [error localizedDescription],NSStringFromSelector(_cmd));
NSNumber *count = [codes valueForKeyPath:#"#count.externalChangedDate"];
if (count == 0) {
[requestCodesForSale release];
[pool drain], pool = nil;
return YES;
}
NSDate *maxExternalChangedDate = [codes valueForKeyPath:#"#max.externalChangedDate"];
By using NSFetchRequest and returning NSDictionaryResultType You can use NSExpressionDescription to yeild the results for functions like max() and min().
Sample Code from Apple
NSExpression *keyPathExpression = [NSExpression expressionForKeyPath:#"salary"];
NSExpression *maxSalaryExpression = [NSExpression expressionForFunction:#"max:"
arguments:[NSArray arrayWithObject:keyPathExpression]];
NSExpressionDescription *expressionDescription = [[NSExpressionDescription alloc] init];
[expressionDescription setName:#"maxSalary"];
[expressionDescription setExpression:maxSalaryExpression];
[expressionDescription setExpressionResultType:NSDecimalAttributeType];
[request setPropertiesToFetch:[NSArray arrayWithObject:expressionDescription]];
Check out this doc for more information.
Core Data Programming Guide
I have the same issue.
In theory this should work, but for me it did not.
For some reasons the query crashes with the error that the database is corrupt.
In the end I perfomed a query, where I ordered on my field DESCENDING, and using setFetchLim it:1. Its not perfect, but at least it worked.
Also I made sure the field I use has an index.
Since I have few records, this works fine.
On 30000 records, it might be a problem though.
I followed the IOS documentation, fot fatch a "max:" query, but only got "database corrupted" errors. That is the sample code from Apple fails BADLY.
Googling the internet, it seem the call to setPropertiesToFetch fails in IOS 5+ ??!
I have not found any way around that.
Using a normal query, it worked without any issue.
So I must conclude Apple code is no longer corerct.

Core Data - get/create NSManagedObject performance

I'm creating an iphone/ipad app that basically reads XML documents and creates tableviews from objects created based on the xml. The xml represents a 'level' in a filesystem. Its basically a browser.
Each time i parse the xml documents i update the filesystem which is mirrored in a core-data sqllite database. For each "File" encountered in the xml i attempt to get the NSManagedObject associated with it.
The problem is this function which i use to get/create either a new blank entity or get the existing one from database.
+(File*)getOrCreateFile:(NSString*)remotePath
context:(NSManagedObjectContext*)context
{
struct timeval start,end,res;
gettimeofday(&start,NULL);
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"File" inManagedObjectContext:context];
[fetchRequest setEntity:entity];
[fetchRequest setFetchLimit:1];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"remotePath == %#",remotePath];
[fetchRequest setPredicate:predicate];
NSError *error;
NSArray *items = [context executeFetchRequest:fetchRequest error:&error];
[fetchRequest release];
File *f;
if ([items count] == 0)
f = (File*)[NSEntityDescription insertNewObjectForEntityForName:#"File" inManagedObjectContext:context];
else
f = (File*)[items objectAtIndex:0];
gettimeofday(&end, NULL);
[JFS timeval_subtract:&res x:&end y:&start];
count++;
elapsed += res.tv_usec;
return f;
}
For eksample, if i'm parsing a document with 200ish files the total time on a iPhone 3G is about 4 seconds. 3 of those seconds are spent in this function getting the objets from core data.
RemotePath is a unique string of variable length and indexed in the sqllite database.
What am i doing wrong here? or.. what could i do better/different to improve performance.
Executing fetches is somewhat expensive in Core Data, though the Core Data engineers have done some amazing work to keep this hit minimal. Thus, you may be able to improve things slightly by running a fetch to return multiple items at once. For example, batch the remotePaths and fetch with a predicate such as
[NSPredicate predicateWithFormat:#"remotePath IN %#", paths];
where paths is a collection of possible paths.
From the results, you can do the searches in-memory to determine if a particular path is present.
Fundamentally, however, doing fetches against strings (even if indexed) is an expensive operation. There may not be much you can do. Consider fetching against non-string attributes, perhaps by hasing the path and saving the hash in the entity as well. You'll get back a (potentially) larger result set which you could then search in memory for string equality.
Finally, do not make any changes without some performance data. Profile, profile, profile.

iTunes Scripting Bridge reveal does not work

The following code should show a certain track in iTunes:
NSString* iTunesPath = [[NSWorkspace sharedWorkspace] absolutePathForAppBundleWithIdentifier:#"com.apple.iTunes"];
iTunesApplication *iTunes = nil;
if ( iTunesPath ) {
iTunes = [[SBApplication alloc] initWithURL:[NSURL fileURLWithPath:iTunesPath]];
[iTunes setDelegate:self];
}
iTunesSource *librarySource = nil;
NSArray *sources = [iTunes sources];
for (iTunesSource *source in sources) {
if ([source kind] == iTunesESrcLibrary) {
librarySource = source;
break;
}
}
SBElementArray *libraryPlaylist = [librarySource libraryPlaylists];
iTunesLibraryPlaylist *iTLibPlaylist = nil;
if ([libraryPlaylist count] > 0) {
iTLibPlaylist = [libraryPlaylist objectAtIndex:0];
}
SBElementArray *fileTracks = [iTLibPlaylist fileTracks];
iTunesFileTrack *track = [fileTracks objectAtIndex:4];
NSLog(#"Try to reveal track: %# at path :%#",[track description],[[track location] path]);
[track reveal];
Output:
Try to reveal track: <ITunesFileTrack #0x1364ed20: ITunesFileTrack 4 of ITunesLibraryPlaylist 0 of ITunesSource 0 of application "iTunes" (2474)> at path :/Users/...
But absolutely noting happens. What am I doing wrong?
(iTunes Version: 9.0.3)
The Library playlist doesn't exist anymore in the UI; it's there in the model, so it shows up in AppleScript, but trying to reveal it or anything in it won't do anything in the UI, as you saw. You can reproduce this in AppleScript as well (reveal track 5 of library playlist 1 of source 1).
The solution is to talk to the Music playlist, not the Library playlist. “Music” is the second playlist—playlist 2 in AppleScript, [[librarySource playlists] objectAtIndex:1] in Cocoa.
If you want to reveal a playing item in whatever playlist it's playing in, use reveal current track (which should be [[iTunes currentTrack] reveal], although I haven't tested that).
This helped me solve a related issue. Thanks.
I would recommend against using [[librarySource playlists] objectAtIndex:1] to get the playlist. It feels too much like guessing. You should also avoid iterating through the arrays from Scripting Bridge with a for loop.
This example solves both problems:
iTunesApplication *iTunes = [SBApplication applicationWithBundleIdentifier:#"com.apple.iTunes"];
iTunesSource *library = [[[[iTunes sources] get] filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:#"kind == %i", iTunesESrcLibrary]] objectAtIndex:0];
iTunesLibraryPlaylist *lp = [[[[library playlists] get] filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:#"specialKind == %i", iTunesESpKMusic]] objectAtIndex:0];
[[library playlists] objectWithName:#"Music"] also works, but I’m not sure if that’s locale dependent (and the name could change in a future update).
Just to add to the answer of Rob McBroom, it would be even better to use firstObject instead of objectAtIndex:0. It will prevent an exception in case your query fails and returns an empty array. This will happen when you search for the Internet radio source (kind == iTunesESrcRadioTuner) and the Internet Radio library is disabled in the preferences.
iTunesApplication* iTunesApp = [SBApplication applicationWithBundleIdentifier:#"com.apple.iTunes"];
iTunesSource* radioTunerSource = [[[[iTunesApp sources] get] filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:#"kind == %i", iTunesESrcRadioTuner]] firstObject];

create iTunes playlist with scripting bridge

I am trying to create a new user playlist using the cocoa scripting bridge, but cannot seem to get it to work. I have so far:
iTunesApplication *iTunes = [SBApplication
applicationWithBundleIdentifier:#"com.apple.iTunes"];
SBElementArray *iSources = [iTunes sources];
iTunesSource *library = nil;
for (iTunesSource *source in iSources) {
if ([[source name] isEqualToString:#"Library"]) {
library = source;
break;
}
}
// could not find the itunes library
if (!library) {
NSLog(#"Could not connect to the iTunes library");
return;
}
// now look for our playlist
NSString *playlistName = #"new playlist";
SBElementArray *playlists = [library userPlaylists];
iTunesUserPlaylist *playlist = nil;
for (iTunesUserPlaylist *thisList in playlists) {
if ([[thisList name] isEqualToString:playlistName]) {
playlist = thisList;
break;
}
}
// if the playlist was not found, create it
if (!playlist) {
playlist = [[[iTunes classForScriptingClass:#"playlist"] alloc] init];
[playlist setName:playlistName];
[[library userPlaylists] insertObject:playlist atIndex:0];
}
When I try and add a name for the playlist, I get the error message:
iTunesBridge[630:80f] *** -[SBProxyByClass setName:]: object has not been added to a container yet; selector not recognized
Can anyone point me in the correct direction?
The error message is telling you that Scripting Bridge objects like your playlist can't receive messages until they've been added to the relevant SBElementArray, so your attempt to set a property on the playlist before adding it to the array fails.
The simplest solution is just to rearrange the last two lines of code, like this:
// if the playlist was not found, create it
if (!playlist) {
playlist = [[[iTunes classForScriptingClass:#"playlist"] alloc] init];
[[library userPlaylists] insertObject:playlist atIndex:0];
[playlist setName:playlistName];
}
The other option is to use initWithProperties: which according to your comment on another answer is what you ended up doing.
Making new application objects is dreadfully obfuscated in SB. The pseudo-Cocoa-ish alloc-init-insert procedure bears no resemblance to what's actually going on underneath. While the alloc-init appears to create a regular object that you can manipulate with subsequent method calls, the result is actually a shim whose only function is to be 'inserted' into an 'array', at which point SB sends an actual make event to the target process. (See also here and here for SB criticisms.)
IIRC, the only point you can actually specify initial properties is in -initWithProperties:. You can set them after the object has been 'inserted', but that is a completely different operation (manipulating an object that already exists rather than specifying initial state for an object being created) so can easily have unintended consequences if you aren't careful.
At any rate, here's how you'd normally create a new playlist if one doesn't already exist:
set playlistName to "new playlist"
tell application "iTunes"
if not (exists playlist playlistName) then
make new playlist with properties {name:playlistName}
end if
end tell
And, FWIW, here's how I'd do it in ObjC, using objc-appscript (which I wrote so I wouldn't have to use SB, natch):
#import "ITGlue/ITGlue.h"
NSString *playlistName = #"new playlist";
ITApplication *itunes = [ITApplication applicationWithName: #"iTunes"];
ITReference *playlist = [[itunes playlists] byName: playlistName];
if ([[[playlist exists] send] boolValue])
playlist = [playlist getItem];
else
playlist = [[[[itunes make] new_: [ITConstant playlist]]
withProperties: [NSDictionary dictionaryWithObject: playlistName
forKey: [ITConstant name]]] send];
(The downside of objc-appscript is that you have to build and embed a copy of the framework in your application bundle. The benefits are that it's more capable, less prone to application compatibility issues, and much less obfuscated. Plus you can use appscript's ASTranslate tool to convert the Apple events sent by the above AppleScript into ObjC syntax - very handy when figuring out how to construct your references and commands.)
Just a quick note that [[source name] isEqualToString:#"Library"] definitely does not work on non-english systems. It might be better to simply use iTunesSource *library = [[_iTunes sources] objectAtIndex: 0]; since the first source item is the one at the top, e.g. the main library.
This is what I've done to reliably identify the library. I could be doing it wrong.
- (iTunesSource *)iTunesLibrary
{
NSArray *librarySource = [[[self iTunes] sources] filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:#"kind == %#", [NSAppleEventDescriptor descriptorWithTypeCode:iTunesESrcLibrary]]];
if ([[librarySource lastObject] exists]) {
return [librarySource lastObject];
}
return nil;
}
You should look into EyeTunes. It's an open-source framework for interacting with iTunes using Objective-C. You code would look much more simple if you did it through EyeTunes.
http://www.liquidx.net/eyetunes/

Resources