create iTunes playlist with scripting bridge - cocoa

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/

Related

Drag and drop files into NSOutlineView

I'm trying to implement simple drag and drop operation into NSOutlineView Based on Apple's example - https://developer.apple.com/library/mac/samplecode/SourceView/Introduction/Intro.html
All seems to be ok, but finally when I drop some files from Finder I get error:
[<ChildNode 0x60800005a280> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key description.') was raised during a dragging session
Here is my test project: https://www.dropbox.com/s/1mgcg2dysvs292u/SimpleDrag.zip?dl=0
What I exactly need in my app: allow user to drag and drop multiple files and folder into some tree list and then display them to user. Also save all this this into some file, so it can be loaded again with all user dragged files and folders.
A final result I want to have like this:
The description property of NSObject is read-only, and is generally set by providing a getter in the implementation file:
- (NSString *)description {
return [self urlString]; // Using urlString solely for demo purposes.
}
You can't set it, either via key-value coding or by direct assignment:
self.description = [self urlString]; // Xcode error: 'Assignment to readonly property'
[self setValue:[self urlString] forKey:#"description"];
In -[ChildNode copyWithZone:] an attempt is made to do the latter of the two, and that's what causes the warning to be logged to the console.
// -------------------------------------------------------------------------------
// copyWithZone:zone
// -------------------------------------------------------------------------------
- (id)copyWithZone:(NSZone *)zone
{
id newNode = [[[self class] allocWithZone:zone] init];
// One of the keys in mutableKeys is 'description'...
// ...but it's readonly! (it's defined in the NSObject protocol)
for (NSString *key in [self mutableKeys])
{
[newNode setValue:[self valueForKey:key] forKey:key];
}
return newNode;
}
This begs the question why do you get the warning in your app, and not in the sample app? From what I can tell no ChildNode instance is ever sent a copyWithZone: message in the sample app, whereas this does happen in your app, immediately after the drop. Of course there's a second question here as well: why do Apple explicitly include the description key-path when it can't be set this way? - unfortunately I can't help you with that.
A really handy way of trying to trap errors that don't actually cause exceptions is to add an All Exceptions breakpoint. If you do this in your sample app you'll see that the app freezes at the line that's causing the problem, giving you a better chance of figuring out the issue.

Testing for existence of a property (i.e. numberOfValidItemsForDrop)

My general question is if and how it is possible to test to see if an arbitrary object supports a given property. For methods, this is simple. I would send a respondsToSelector: message to the object and check the result. I cannot find a similar mechanism for properties.
My specific problem at hand is that I'm writing a custom NSView that supports dropping of image files (plural) onto it. I am building on 10.9 and deploying on 10.6. For machines running 10.7 and later, I would like to take advantage of the user feedback provided in the drag image (number and acceptable files) by enumerateDraggingItemsWithOptions:... method and the numberOfValidItemsForDrop property.
My initial thought is as follows. Test the sender (declared id < NSDraggingInfo >) provided to the draggingEntered: method. If so, use the mechanisms provided in 10.7. If not, go back to the earlier mechanisms.
if( [sender respondsToSelector:#selector(enumerateDraggingItemsWithOptions:forView:classes:searchOptions:usingBlock:) ] )
{
__block NSInteger n=0;
[sender enumerateDraggingItemsWithOptions:NSDraggingItemEnumerationClearNonenumeratedImages
forView:self
classes:types
searchOptions:options
usingBlock:^(NSDraggingItem *draggingItem, NSInteger idx, BOOL *stop) { ++n; } ];
self.numberOfValidItemsForDrop = n;
rval = (n>0 ? NSDragOperationCopy : NSDragOperationNone);
}
else
{
NSArray *itemsInDrag = [[sender draggingPasteboard] readObjectsForClasses:types options:options];
rval = [itemsInDrag count]>0 ? NSDragOperationCopy : NSDragOperationNone);
}
My concern is whether or not the assignment to the numberOfValidItemsForDrop will cause problems in the 10.6 environment. I threw together a quick test in which I assigned to a bogus property inside an if(0) condition and the compiler choked.
if(0)
{
sender.bogus=1;
}
This is not really the same thing as the property is not valid in the development environment. But, it has me concerned that the assignment to numberOfValidItemsForDrop might also have a problem at runtime on 10.6 even though it should never actually be executed.
I have thrown together a small dummy application which I have sent to my tester, but have not yet heard back and would like to keep moving on this project.
Thanks for any/all info.
You can use the Objective-C runtime function class_getProperty to test for the existence of a named property:
if(class_getProperty([self class], "numberOfValidItemsForDrop")) {
// property exists
}
See https://developer.apple.com/library/mac/documentation/Cocoa/Reference/ObjCRuntimeRef/Reference/reference.html
To check if a property exists or not, you can use as : the property is assumed not to be readonly.
if ([yourClassObject respondsToSelector:#selector(set<yourPropertyName>:)]) {
NSLog(#"yes it exists");
}
else{
NSLog(#"no it doesn't exists");
}

How to avoid copy and pasting?

I'd like to improve this method if possible: this is a small section whereby all of the textfield (eyepiece, objectivelenses etc) texts are saved. Unfortunately, having to do this lots of times for each part of my app is prone to error so I would like to improve it. I'm thinking some sort of fast enumeration with arguments for the method being the textfields etc. and I can have all the keys in a dictionary (which is already set up). Just a pointer to the right docs or, perhaps, some sort of process that has worked for you would be fantastic!
-(IBAction)saveUserEntries {
if (eyepiece.text != nil) {
eyepieceString = [[NSString alloc] initWithFormat:eyepiece.text];
[eyepiece setText:eyepieceString];
NSUserDefaults *eyepieceDefault = [NSUserDefaults standardUserDefaults];
[eyepieceDefault setObject:eyepieceString forKey:#"eyepieceKey"];
}
else {
[[NSUserDefaults standardUserDefaults] removeObjectForKey:#"eyepieceKey"];
}
if (objectiveLenses.text != nil) {
objectiveLensString = [[NSString alloc] initWithFormat:objectiveLenses.text];
[objectiveLenses setText:objectiveLensString];
NSUserDefaults *objectiveDefault = [NSUserDefaults standardUserDefaults];
[objectiveDefault setObject:objectiveLensString forKey:#"objectiveKey"];
}
else {
[[NSUserDefaults standardUserDefaults] removeObjectForKey:#"objectiveKey"];
}
Thank you for taking the time to read this!
I will attempt to answer this question based on a OOP solution.
Create a method that accepts whatever type object these textboxes are as an argument, send the reference of said object to the method, and save the entry in a similar method you do know. This will avoid the "copy and paste" errors you are worried about.
You should be able to loop through every instance of said object that exists, if a cocoa application, works like similar to Java and .NET ( I really don't know ). I just know there must be a way to loop through every instance of a single object within the application domain.
If this was .NET I simply would suggest TextBox.Name and TextBox.String to make this a generic method that could be used to save the properties of any TextBox sent to it. If this doesn't anwer your question ( was a little long for a comment ) then I aplogize.

some criteria peg CPU using Scripting Bridge and NSPredicate

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.)

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];

Resources