Use iTunes Library framework in Node.js/Electron app?

Viewed 290

I am developing an Electron (Node.js) app that accesses the iTunes database. Before Catalina one could use an exported XML version of the database iTunes Music Library.xml. This was removed in Catalina where the iTunes library framework should be used instead https://developer.apple.com/documentation/ituneslibrary.

Is it possible to include this framework in my Electron or generally in a Node.js project and interact with it and if yes — how?

I scanned GitHub for modules or possible solutions but all of them rely on the old XML file.

1 Answers

The only way I see is using a node native module. Apple uses Objectiv-C and every other language (javascript, python, etc) uses C or C++ bindings. Luckly, clang supports Objective-C++, which allows to mix C++ and Objective-C. A basic snippet that works looks like this (name it readMusic.mm. mm is the extension für Objectiv-C++)

#import <Foundation/Foundation.h>
#import <iTunesLibrary/ITLibrary.h>
#import <iTunesLibrary/ITLibMediaItem.h>
#import <iTunesLibrary/ITLibArtist.h>
#include <node.h>
 

namespace demo {

using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::Array;
using v8::Object;
using v8::String;
using v8::Value;
using v8::Integer;

char const *emptyString = "";

void Method(const FunctionCallbackInfo<Value>& args) {
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; // Set this up so Cocoa works

    Isolate* isolate = args.GetIsolate(); // Setup for Javascript Connection
    Local<v8::Context> context = isolate->GetCurrentContext();
    Local<String> keyTitle = String::NewFromUtf8(isolate, "title").ToLocalChecked();
    Local<String> keyArtist = String::NewFromUtf8(isolate, "artist").ToLocalChecked();
    Local<String> keyFilePath = String::NewFromUtf8(isolate, "filePath").ToLocalChecked();
    Local<String> keyBpm = String::NewFromUtf8(isolate, "bpm").ToLocalChecked();


    NSError *error = nil;
    ITLibrary *library = [ITLibrary libraryWithAPIVersion:@"1.0" error:&error]; // Connect to iTunes / Music Library
    if (library)
    {
        NSArray *tracks = library.allMediaItems; // Load all Songs with Cocoa / Objective-C
        int size = [tracks count];

        Local<Array> jsSongsArr = Array::New(isolate, size); // Create Array for Javascript v8 engine

        for (int i = 0; i < size; i++) { // Copy elements
            // Reading Data from Cocoa
            ITLibMediaItem *song = tracks[i];
            NSString *title = [song title];
            ITLibArtist *artist = [song artist];
            NSURL *location = [song location];

            // Convert it to c
            const char *titleInC = emptyString;
            const char *artistInC = emptyString;
            const char *filePathInC = emptyString;
            const long bpmInC = [song beatsPerMinute];

            if (title) {
                titleInC = [title UTF8String];
            }
            if (artist) {
                NSString *artistNSString = [artist name];
                if (artistNSString) {
                    artistInC = [artistNSString UTF8String];
                }
            }
            if (location) {
                NSString *locationNSString = [location absoluteString];
                if (locationNSString) {
                    filePathInC = [locationNSString UTF8String];
                }
            }

            Local<Object> jsSong = Object::New(isolate); // Create Javascript Object
            jsSong->Set(context, keyTitle, String::NewFromUtf8(isolate, titleInC).ToLocalChecked()).FromJust(); // Copy data in Javascript Object
            jsSong->Set(context, keyArtist, String::NewFromUtf8(isolate, artistInC).ToLocalChecked()).FromJust();
            jsSong->Set(context, keyFilePath, String::NewFromUtf8(isolate, filePathInC).ToLocalChecked()).FromJust();
            jsSong->Set(context, keyBpm, Integer::New(isolate, bpmInC)).FromJust();
            
            jsSongsArr->Set(context, i, jsSong).FromJust(); // Add the Object to Javascript Array
        }
        args.GetReturnValue().Set(jsSongsArr); // Set the return value of the function
    } else { // If error occurs
        args.GetReturnValue().Set(String::NewFromUtf8(isolate, [[error localizedDescription] UTF8String]).ToLocalChecked());
    }
}

void Initialize(Local<Object> exports) {
  NODE_SET_METHOD(exports, "readMusic", Method); // Tells node which function to use
}

NODE_MODULE(NODE_GYP_MODULE_NAME, Initialize) // Inits a native node module

}

Don't forget the NSAutoReleasePool when doing stuff like that. For the binding you need to set the -ObjC++ flags in your binding.gyp.

{
  "targets": [
    {
      "target_name": "addon",
      "sources": [ "readMusic.mm" ],
      "cflags!": [ "-ObjC++" ],
      "cflags_cc!": [ "-ObjC++" ],
      "libraries": [
          "/System/Library/Frameworks/iTunesLibrary.framework/Versions/Current/iTunesLibrary"
      ]
    }
  ]
}

Then compile and codesign it (very important for distribution) via

HOME=~/.electron-gyp node-gyp rebuild --target=10.1.0 --arch=x64 --dist-url=https://electronjs.org/headers
codesign -s "YOUR DEVELOPER ID" build/Release/addon.node

And use it via

require("./build/Release/addon.node").readMusic();

Because of the dependence on native modules this is only compilable on Mac (https://www.electron.build/multi-platform-build). Make sure to exclude that code on windows platforms.

Related