Skip to content

Instantly share code, notes, and snippets.

@iccir
Last active January 7, 2020 08:49
Show Gist options
  • Save iccir/2363d00cadac3eb6c26aa8762e718c09 to your computer and use it in GitHub Desktop.
Save iccir/2363d00cadac3eb6c26aa8762e718c09 to your computer and use it in GitHub Desktop.
Embrace's code for handling drops from Catalina's Music app, due to Apple not following the API contract for file promises. You will need to register for the kPasteboardTypeFileURLPromise drag type.
- (NSArray<NSURL *> *) parsePasteboard:(NSPasteboard *)pasteboard
{
NSMutableDictionary *trackIDToMetadataMap = [NSMutableDictionary dictionary];
NSMutableArray *orderedTrackIDs = [NSMutableArray array];
NSMutableArray *metadataFileURLs = [NSMutableArray array];
NSMutableArray *otherFileURLs = [NSMutableArray array];
auto parsePlaylist = ^(NSDictionary *playlist) {
if (![playlist isKindOfClass:[NSDictionary class]]) {
return;
}
NSArray *items = [playlist objectForKey:@"Playlist Items"];
if (![items isKindOfClass:[NSArray class]]) {
return;
}
for (NSDictionary *item in items) {
if (![item isKindOfClass:[NSDictionary class]]) {
continue;
}
id trackIDObject = [item objectForKey:@"Track ID"];
if (![trackIDObject respondsToSelector:@selector(integerValue)]) {
trackIDObject = nil;
}
NSInteger trackID = [trackIDObject integerValue];
if (trackID) [orderedTrackIDs addObject:@(trackID)];
}
};
auto parsePlaylists = ^(NSArray *playlists) {
if (![playlists isKindOfClass:[NSArray class]]) {
return;
}
for (NSDictionary *playlist in playlists) {
parsePlaylist(playlist);
}
};
auto parseTrack = ^(NSString *key, NSDictionary *track) {
if (![key isKindOfClass:[NSString class]]) {
return;
}
if (![track isKindOfClass:[NSDictionary class]]) {
return;
}
NSString *artist = [track objectForKey:@"Artist"];
if (![artist isKindOfClass:[NSString class]]) artist = nil;
NSString *name = [track objectForKey:@"Name"];
if (![name isKindOfClass:[NSString class]]) name = nil;
NSString *location = [track objectForKey:@"Location"];
if (![location isKindOfClass:[NSString class]]) location = nil;
if ([location hasPrefix:@"file:"]) {
location = [[NSURL URLWithString:location] path];
}
id totalTimeObject = [track objectForKey:@"Total Time"];
if (![totalTimeObject respondsToSelector:@selector(doubleValue)]) {
totalTimeObject = nil;
}
id trackIDObject = [track objectForKey:@"Track ID"];
if (![trackIDObject respondsToSelector:@selector(integerValue)]) {
trackIDObject = nil;
}
NSTimeInterval totalTime = [totalTimeObject doubleValue] / 1000.0;
NSInteger trackID = [trackIDObject integerValue];
if (!trackID) return;
MusicAppPasteboardMetadata *metadata = [[MusicAppPasteboardMetadata alloc] init];
[metadata setDuration:totalTime];
[metadata setTitle:name];
[metadata setArtist:artist];
[metadata setLocation:sGetExpandedPath(location)];
[metadata setTrackID:trackID];
[metadata setDatabaseID:[key integerValue]];
[trackIDToMetadataMap setObject:metadata forKey:@(trackID)];
};
auto parseTracks = ^(NSDictionary *tracks) {
if (![tracks isKindOfClass:[NSDictionary class]]) {
return;
}
for (NSString *key in tracks) {
NSDictionary *track = [tracks objectForKey:key];
parseTrack(key, track);
}
};
auto parseMetadataRoot = ^(NSDictionary *root) {
if (![root isKindOfClass:[NSDictionary class]]) {
return;
}
parsePlaylists( [root objectForKey:@"Playlists"] );
parseTracks( [root objectForKey:@"Tracks"] );
};
auto parsePasteboardItem = ^(NSPasteboardItem *item) {
for (NSString *type in [item types]) {
if ([type hasPrefix:@"com.apple."] && [type hasSuffix:@".metadata"]) {
parseMetadataRoot([item propertyListForType:type]);
}
}
};
// Step 1, Fill metadataFileURLs from com.apple.*.metadata
{
for (NSPasteboardItem *item in [pasteboard pasteboardItems]) {
parsePasteboardItem(item);
}
for (NSNumber *trackID in orderedTrackIDs) {
MusicAppPasteboardMetadata *metadata = [trackIDToMetadataMap objectForKey:trackID];
NSString *location = [metadata location];
NSURL *fileURL = location ? [NSURL fileURLWithPath:location] : nil;
if (fileURL) [metadataFileURLs addObject:fileURL];
}
}
// Step 2, Fill otherFileURLs with NSPasteboardTypeFileURL/kPasteboardTypeFileURLPromise
{
for (NSPasteboardItem *item in [pasteboard pasteboardItems]) {
NSArray *types = [item types];
BOOL hasPromise = [types containsObject:(id)kPasteboardTypeFileURLPromise];
BOOL hasPromiseContent = [types containsObject:(id)kPasteboardTypeFilePromiseContent];
NSString *fileURLString = [item propertyListForType:NSPasteboardTypeFileURL];
// In macOS Catalina 10.15.0, the new Music app likes to write a real URL
// as a kPasteboardTypeFileURLPromise without kPasteboardTypeFilePromiseContent
//
if (!fileURLString && hasPromise && !hasPromiseContent) {
fileURLString = [item propertyListForType:NSPasteboardTypeFileURL];
}
NSURL *fileURL = fileURLString ? [NSURL URLWithString:fileURLString] : nil;
fileURL = [fileURL URLByStandardizingPath];
fileURL = [fileURL URLByResolvingSymlinksInPath];
if (fileURL) [otherFileURLs addObject:fileURL];
}
}
// Step 3, Fill otherFileURLs with legacy NSFilenamesPboardType
if ([otherFileURLs count] == 0) {
NSArray *filenames = [pasteboard propertyListForType:NSFilenamesPboardType];
if (filenames) {
for (NSString *filename in filenames) {
NSURL *url = [NSURL fileURLWithPath:sGetExpandedPath(filename)];
if (url) [otherFileURLs addObject:url];
}
}
}
NSArray *fileURLs = [metadataFileURLs count] > [otherFileURLs count] ? metadataFileURLs : otherFileURLs;
return fileURLs;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment