Trying to use https://github.com/japlscript/obstmusic to talk to Apple Music app on macOS with Java, I used to write native AppleScript and then java applescript library but that was removed from Java.
In this method it looks for a existing folder playlist called songkong, it it finds it then returns it. If none exists then it creates such a folder and then returns it.
private FolderPlaylist getPlayListFolder() { Application app = Application.getInstance(); com.tagtraum.macos.music.Playlist[] songKongPlaylists = app.getPlaylists(); for(com.tagtraum.macos.music.Playlist next:songKongPlaylists) { if(next.getName().equals("songkong")) { return (com.tagtraum.macos.music.FolderPlaylist)next; } } Object songkongPlaylist = app.make(FolderPlaylist.class); if(songkongPlaylist instanceof FolderPlaylist) { ((FolderPlaylist)songkongPlaylist).setName("songkong"); return ((FolderPlaylist)songkongPlaylist); } return null; }
First time I run it when I have to create a folder playlist, because non exists it works, but if I run again so it finds an existing folder playlist it then fails complaining as follows
4/04/2022 14.53.25:BST:OSXUpdateItunesWithChanges:updateItunes:SEVERE: *** Unable to run itunes update:class jdk.proxy2.$Proxy62 cannot be cast to class com.tagtraum.macos.music.FolderPlaylist (jdk.proxy2.$Proxy62 is in module jdk.proxy2 of loader ‘app’; com.tagtraum.macos.music.FolderPlaylist is in unnamed module of loader ‘app’) java.lang.ClassCastException: class jdk.proxy2.$Proxy62 cannot be cast to class com.tagtraum.macos.music.FolderPlaylist (jdk.proxy2.$Proxy62 is in module jdk.proxy2 of loader ‘app’; com.tagtraum.macos.music.FolderPlaylist is in unnamed module of loader ‘app’) at com.jthink.songkong.ituneshelper.OSXUpdateMusicWithChanges.getPlayListFolder(OSXUpdateMusicWithChanges.java:41) at com.jthink.songkong.ituneshelper.OSXUpdateMusicWithChanges.createPlaylist(OSXUpdateMusicWithChanges.java:56) at com.jthink.songkong.ituneshelper.OSXUpdateItunesWithChanges.analyseFiles(OSXUpdateItunesWithChanges.java:246) at com.jthink.songkong.ituneshelper.OSXUpdateItunesWithChanges.updateItunes(OSXUpdateItunesWithChanges.java:126) at com.jthink.songkong.ituneshelper.UpdateItunesWithChanges.call(UpdateItunesWithChanges.java:184) at com.jthink.songkong.ituneshelper.UpdateItunesWithChanges.call(UpdateItunesWithChanges.java:33) at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264) at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136) at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)
Im not using modules so I think references to modules is probably misleading. More likely the issue I have to do something more than just cast from Playlist
to FolderPlaylist
but I cannot find an alternative.
Advertisement
Answer
ObstMusic uses JaplScript to talk to Apple’s Music app via AppleScript (in an imperfect way). It does this by creating dynamic proxies for Java interfaces that have been generated for Music’s AppleScript classes.
Now, what happens in your code?
com.tagtraum.macos.music.Playlist[] songKongPlaylists = app.getPlaylists();
Here, in essence, ObstMusic generates an AppleScript snippet that asks Music for all playlists. The signature of the getPlaylist()
method is as follows:
Playlist[] getPlaylists​();
Now, when JaplScript generates dynamic proxies for the returned AppleScript references, it has to figure out what types it has to use. Ideally, it would look at the AppleScript references (and it could) to figure out what type to use. But it would imply another AppleScript roundtrip (or not… see update below). So for large collections this could take a while. For performance reasons, JaplScript simply uses the type declared in the method you have called. In this case Playlist
, which is a superclass of FolderPlaylist
. But since the FolderPlaylist
is not specified during dynamic proxy generation, you cannot simply cast to it. That’s why you see the ClassCastException
.
The described behavior is obviously not the most convenient, since it does not adhere to the usual Java behavior (or that of many other OO languages for that matter).
If you want to work around this and are willing to take the performance hit, you can ask a JaplScript instance for its real AppleScript runtime type by calling TypeClass typeClass = someJaplScriptProxy.getTypeClass()
. You can also get the TypeClass
of each Music app interface by calling e.g. TypeClass tc = Playlist.CLASS
(note the casing). Finally, you can get all Music app interfaces by calling Set<java.lang.Class<?>> classes = Application.APPLICATION_CLASSES
, which returns a set of all Java interfaces declared for Music app.
Putting this all together, you can create a map from real TypeClass
to most specific Java interface and use this in your cast()
call, roughly like this:
Set<java.lang.Class<?>> classes = Application.APPLICATION_CLASSES; Map<TypeClass, java.lang.Class<?>> typeClassToJava = new HashMap<>(); for (final Class<?> c : classes) { typeClassToJava.put(TypeClass.fromClass(c), c); }
Using this map you can iterate over the returned playlist array and cast all playlist objects to their actual (most specific) types and work around the issue you experienced.
Update 4/21/2022:
Starting with version 3.4.11 (Obstmusic 0.9.6), JaplScript is much better at creating dynamic proxies with the most specific Java interface that is suitable for the AppleScript object specifier. This means, you may not have to manually cast at all anymore.