Last active
June 10, 2020 19:20
-
-
Save hendriks73/6d97016245cd93ca45c57f682a280522 to your computer and use it in GitHub Desktop.
JXA-based JavaScript that creates an XML file for Music.app like iTunes used to create before macOS 10.15.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/*********************************************************************** | |
* JXA-based JavaScript that creates an XML file for Music.app like * | |
* iTunes used to create before macOS 10.15. * | |
* * | |
* NOTE: The XML is not identical, but a best effort is made. * | |
* Missing are for example all B64-encoded smart data and the * | |
* protected flag as well as certain distinguished playlists * | |
* kinds. For a real replacement, use the ITLibrary API . * | |
* * | |
* USAGE: * | |
* * | |
* To write XML to standard out call: * * | |
* osascript -l JavaScript musicxml.js * * | |
* * | |
* To write XML to a file: * * | |
* osascript -l JavaScript musicxml.js YOUR_FILE * * | |
* * | |
* AUTHOR: Hendrik Schreiber * | |
* * | |
* LICENSE: MPL 2.0 * | |
* * | |
* This Source Code Form is subject to the terms of the Mozilla Public * | |
* License, v. 2.0. If a copy of the MPL was not distributed with this * | |
* file, You can obtain one at http://mozilla.org/MPL/2.0/. * | |
***********************************************************************/ | |
/** | |
* Create ISO string without milliseconds. | |
*/ | |
Date.prototype.toiTunesISOString = function() { | |
const regularISO = this.toISOString() | |
// cut off milliseconds, as iTunes does not export those | |
return regularISO.substring(0, 19) + "Z" | |
} | |
/** | |
* Make sure we escape certain characters for XML. | |
*/ | |
function escapeXML(unsafe) { | |
return unsafe.toString().replace(/[<>&'"]/g, function (c) { | |
switch (c) { | |
case '<': return '<' | |
case '>': return '>' | |
case '&': return '&' | |
case '\'': return ''' | |
case '"': return '"' | |
} | |
}) | |
} | |
/** | |
* Get command line arguments. | |
*/ | |
function argv() { | |
const args = $.NSProcessInfo.processInfo.arguments | |
const argc = args.count | |
// Build the normal argv/argc | |
let argv = [] | |
for (let i = 0; i < argc; i++) { | |
argv.push(ObjC.unwrap(args.objectAtIndex(i))) | |
} | |
delete args | |
return argv | |
} | |
/** | |
* Convert a playlist object to an XML string. | |
*/ | |
function playlistToXML(playlist) { | |
const properties = playlist.properties() | |
let xmlPlaylist = `<dict>\n` | |
const klass = properties["class"] | |
if (klass == 'libraryPlaylist') { | |
xmlPlaylist = xmlPlaylist.concat(`<key>Master</key><true/>\n`) | |
} | |
const visible = properties["visible"] | |
if (!visible) { | |
xmlPlaylist = xmlPlaylist.concat(`<key>Visible</key><false/>\n`) | |
} | |
const playlistID = properties["id"] | |
xmlPlaylist = xmlPlaylist.concat(`<key>Playlist ID</key><integer>${playlistID}</integer>\n`) | |
try { | |
const parent = playlist.parent() | |
if (parent != null) { | |
xmlPlaylist = xmlPlaylist.concat(`<key>Parent Persistent ID</key><string>${parent.persistentID()}</string>\n`) | |
} | |
} catch (error) { | |
// ignore - happens when the playlist does not have a parent | |
} | |
const persistentID = properties["persistentID"] | |
xmlPlaylist = xmlPlaylist.concat(`<key>Playlist Persistent ID</key><string>${persistentID}</string>\n`) | |
const specialKind = properties["specialKind"] | |
if (specialKind == "folder") { | |
xmlPlaylist = xmlPlaylist.concat(`<key>Folder</key><true/>\n`) | |
} | |
if (specialKind == "Movies") { | |
xmlPlaylist = xmlPlaylist.concat(`<key>Distinguished Kind</key><integer>2</integer>\n<key>Movies</key><true/>\n`) | |
} else if (specialKind == "TV Shows") { | |
xmlPlaylist = xmlPlaylist.concat(`<key>Distinguished Kind</key><integer>3</integer>\n<key>TV Shows</key><true/>\n`) | |
} else if (specialKind == "Music") { | |
xmlPlaylist = xmlPlaylist.concat(`<key>Distinguished Kind</key><integer>4</integer>\n<key>Music</key><true/>\n`) | |
} else if (specialKind == "Books") { | |
xmlPlaylist = xmlPlaylist.concat(`<key>Distinguished Kind</key><integer>5</integer>\n<key>Audiobooks</key><true/>\n`) | |
} else if (specialKind == "Podcasts") { | |
xmlPlaylist = xmlPlaylist.concat(`<key>Distinguished Kind</key><integer>10</integer>\n<key>Podcasts</key><true/>\n`) | |
} else if (specialKind == "Purchased Music") { | |
xmlPlaylist = xmlPlaylist.concat(`<key>Distinguished Kind</key><integer>19</integer>\n<key>Purchased Music</key><true/>\n`) | |
} else if (specialKind == "Genius") { | |
xmlPlaylist = xmlPlaylist.concat(`<key>Distinguished Kind</key><integer>26</integer>\n<key>Genius</key><true/>\n`) | |
} else if (specialKind == "iTunes U") { | |
xmlPlaylist = xmlPlaylist.concat(`<key>Distinguished Kind</key><integer>31</integer>\n<key>iTunes U</key><true/>\n`) | |
} | |
// other distinguished kinds: downloaded? voice memos? | |
const name = properties["name"] | |
if (name != null && name.length > 0) { | |
xmlPlaylist = xmlPlaylist.concat(`<key>Name</key><string>${escapeXML(name)}</string>\n`) | |
} | |
const description = properties["description"] | |
if (description != null && description.length > 0) { | |
xmlPlaylist = xmlPlaylist.concat(`<key>Description</key><string>${escapeXML(description)}</string>\n`) | |
} | |
if (klass == 'userPlaylist') { | |
if (properties["smart"] || properties["genius"]) { | |
xmlPlaylist = xmlPlaylist.concat(`<key>Smart Info</key><data/>\n`) | |
xmlPlaylist = xmlPlaylist.concat(`<key>Smart Criteria</key><data/>\n`) | |
} | |
} | |
const loved = properties["loved"] | |
if (loved) { | |
xmlPlaylist = xmlPlaylist.concat(`<key>Loved</key><true/>\n`) | |
} | |
const disliked = properties["disliked"] | |
if (disliked) { | |
xmlPlaylist = xmlPlaylist.concat(`<key>Disliked</key><true/>\n`) | |
} | |
// list tracks | |
const tracks = playlist.tracks | |
const trackCount = tracks.length | |
xmlPlaylist = xmlPlaylist.concat(`<key>Playlist Items</key>\n<array>\n`) | |
for (let j=0; j<trackCount; j++) { | |
xmlPlaylist = xmlPlaylist.concat(`<dict><key>Track ID</key><integer>${tracks[j].databaseID()}</integer></dict>\n`) | |
} | |
xmlPlaylist = xmlPlaylist.concat(`</array>\n`) | |
if (klass == 'userPlaylist' && properties["genius"] && tracks.length > 0) { | |
// we are guessing that the genius list is always based on the first track | |
xmlPlaylist = xmlPlaylist.concat(`<key>Genius Track ID</key><integer>${tracks[0].databaseID()}</integer>\n`) | |
} | |
xmlPlaylist = xmlPlaylist.concat(`</dict>\n`) | |
return xmlPlaylist | |
} | |
/** | |
* Convert a track object to an XML string. | |
*/ | |
function trackToXML(track) { | |
const properties = track.properties() | |
console.log(Object.entries(properties).toString()) | |
const trackID = properties["databaseID"] | |
console.log(trackID) | |
console.log(track.persistentID()) | |
let xmlTrack = `<key>${trackID}</key>\n<dict>\n<key>Track ID</key><integer>${trackID}</integer>\n` | |
const size = properties["size"] | |
if (size > 0) { | |
xmlTrack = xmlTrack.concat(`<key>Size</key><integer>${size}</integer>\n`) | |
} | |
const totalTime = properties["duration"] * 1000 | |
if (totalTime > 0) { | |
xmlTrack = xmlTrack.concat(`<key>Total Time</key><integer>${Math.round(totalTime)}</integer>\n`) | |
} | |
const startTime = properties["start"] * 1000 | |
if (startTime > 0) { | |
xmlTrack = xmlTrack.concat(`<key>Start Time</key><integer>${Math.round(startTime)}</integer>\n`) | |
} | |
const stopTime = properties["finish"] * 1000 | |
if (stopTime > 0 && stopTime != totalTime) { | |
xmlTrack = xmlTrack.concat(`<key>Stop Time</key><integer>${Math.round(stopTime)}</integer>\n`) | |
} | |
const discNumber = properties["discNumber"] | |
if (discNumber > 0) { | |
xmlTrack = xmlTrack.concat(`<key>Disc Number</key><integer>${discNumber}</integer>\n`) | |
} | |
const discCount = properties["discCount"] | |
if (discCount > 0) { | |
xmlTrack = xmlTrack.concat(`<key>Disc Count</key><integer>${discCount}</integer>\n`) | |
} | |
const trackNumber = properties["trackNumber"] | |
if (trackNumber > 0) { | |
xmlTrack = xmlTrack.concat(`<key>Track Number</key><integer>${trackNumber}</integer>\n`) | |
} | |
const trackCount = properties["trackCount"] | |
if (trackCount > 0) { | |
xmlTrack = xmlTrack.concat(`<key>Track Count</key><integer>${trackCount}</integer>\n`) | |
} | |
const year = properties["year"] | |
if (year > 0) { | |
xmlTrack = xmlTrack.concat(`<key>Year</key><integer>${year}</integer>\n`) | |
} | |
const bpm = properties["bpm"] | |
if (bpm > 0) { | |
xmlTrack = xmlTrack.concat(`<key>BPM</key><integer>${bpm}</integer>\n`) | |
} | |
const dateModified = properties["modificationDate"] | |
if (dateModified != null) { | |
xmlTrack = xmlTrack.concat(`<key>Date Modified</key><date>${dateModified.toiTunesISOString()}</date>\n`) | |
} | |
const dateAdded = properties["dateAdded"] | |
if (dateAdded != null) { | |
xmlTrack = xmlTrack.concat(`<key>Date Added</key><date>${dateAdded.toiTunesISOString()}</date>\n`) | |
} | |
const bitRate = properties["bitRate"] | |
if (bitRate > 0) { | |
xmlTrack = xmlTrack.concat(`<key>Bit Rate</key><integer>${bitRate}</integer>\n`) | |
} | |
const sampleRate = properties["sampleRate"] | |
if (sampleRate > 0) { | |
xmlTrack = xmlTrack.concat(`<key>Sample Rate</key><integer>${sampleRate}</integer>\n`) | |
} | |
const gapless = properties["gapless"] | |
if (gapless) { | |
xmlTrack = xmlTrack.concat(`<key>Part Of Gapless Album</key><true/>\n`) | |
} | |
const playCount = properties["playedCount"] | |
if (playCount > 0) { | |
xmlTrack = xmlTrack.concat(`<key>Play Count</key><integer>${playCount}</integer>\n`) | |
} | |
let playDateUTC = "" | |
let playDate = 0 | |
const playedDate = properties["playedDate"] | |
if (playedDate != null) { | |
xmlTrack = xmlTrack.concat(`<key>Play Date UTC</key><date>${playedDate.toiTunesISOString()}</date>\n`) | |
playDate = (playedDate.getTime() - epoch) / 1000 | |
xmlTrack = xmlTrack.concat(`<key>Play Date</key><integer>${playDate}</integer>\n`) | |
} | |
const skipCount = properties["skippedCount"] | |
if (skipCount > 0) { | |
xmlTrack = xmlTrack.concat(`<key>Skip Count</key><integer>${skipCount}</integer>\n`) | |
} | |
const skipDate = properties["skippedDate"] | |
if (skipDate != null) { | |
xmlTrack = xmlTrack.concat(`<key>Skip Date</key><date>${skipDate.toiTunesISOString()}</date>\n`) | |
} | |
const rating = properties["rating"] | |
if (rating > 0) { | |
xmlTrack = xmlTrack.concat(`<key>Rating</key><integer>${rating}</integer>\n`) | |
} | |
const ratingComputed = properties["ratingKind"] == "computed" | |
if (ratingComputed) { | |
xmlTrack = xmlTrack.concat(`<key>Rating Computed</key><true/>\n`) | |
} | |
const albumRating = properties["albumRating"] | |
if (albumRating > 0) { | |
xmlTrack = xmlTrack.concat(`<key>Album Rating</key><integer>${albumRating}</integer>\n`) | |
} | |
const albumRatingComputed = properties["albumRatingKind"] == "computed" | |
if (albumRatingComputed) { | |
xmlTrack = xmlTrack.concat(`<key>Album Rating Computed</key><true/>\n`) | |
} | |
const persistentID = properties["persistentID"] | |
xmlTrack = xmlTrack.concat(`<key>Persistent ID</key><string>${persistentID}</string>\n`) | |
let klass = properties["class"] | |
let trackType = klass | |
if (klass == "urlTrack") { | |
trackType = "URL" | |
} else if (klass == "fileTrack") { | |
trackType = "File" | |
} else if (klass == "sharedTrack") { | |
trackType = "Remote" | |
} | |
xmlTrack = xmlTrack.concat(`<key>Track Type</key><string>${trackType}</string>\n`) | |
const movementNumber = properties["movementNumber"] | |
if (movementNumber > 0) { | |
xmlTrack = xmlTrack.concat(`<key>Movement Number</key><integer>${movementNumber}</integer>\n`) | |
} | |
const movementCount = properties["movementCount"] | |
if (movementCount > 0) { | |
xmlTrack = xmlTrack.concat(`<key>Movement Count</key><integer>${movementCount}</integer>\n`) | |
} | |
const movementName = properties["movement"] | |
if (movementName != null && movementName.length > 0) { | |
xmlTrack = xmlTrack.concat(`<key>Movement Name</key><string>${escapeXML(movementName)}</string>\n`) | |
} | |
const work = properties["work"] | |
if (work != null && work.length > 0) { | |
xmlTrack = xmlTrack.concat(`<key>Work</key><string>${escapeXML(work)}</string>\n`) | |
} | |
const grouping = properties["grouping"] | |
if (grouping != null && grouping.length > 0) { | |
xmlTrack = xmlTrack.concat(`<key>Grouping</key><string>${escapeXML(grouping)}</string>\n`) | |
} | |
const comments = properties["comment"] | |
if (comments != null && comments.length > 0) { | |
xmlTrack = xmlTrack.concat(`<key>Comments</key><string>${escapeXML(comments)}</string>\n`) | |
} | |
const name = properties["name"] | |
if (name != null && name.length > 0) { | |
xmlTrack = xmlTrack.concat(`<key>Name</key><string>${escapeXML(name)}</string>\n`) | |
} | |
const artist = properties["artist"] | |
if (artist != null && artist.length > 0) { | |
xmlTrack = xmlTrack.concat(`<key>Artist</key><string>${escapeXML(artist)}</string>\n`) | |
} | |
const album = properties["album"] | |
if (album != null && album.length > 0) { | |
xmlTrack = xmlTrack.concat(`<key>Album</key><string>${escapeXML(album)}</string>\n`) | |
} | |
const albumArtist = properties["albumArtist"] | |
if (albumArtist != null && albumArtist.length > 0) { | |
xmlTrack = xmlTrack.concat(`<key>Album Artist</key><string>${escapeXML(albumArtist)}</string>\n`) | |
} | |
const composer = properties["composer"] | |
if (composer != null && composer.length > 0) { | |
xmlTrack = xmlTrack.concat(`<key>Composer</key><string>${escapeXML(composer)}</string>\n`) | |
} | |
const genre = properties["genre"] | |
if (genre != null && genre.length > 0) { | |
xmlTrack = xmlTrack.concat(`<key>Genre</key><string>${escapeXML(genre)}</string>\n`) | |
} | |
const kind = properties["kind"] | |
if (kind != null && kind.length > 0) { | |
xmlTrack = xmlTrack.concat(`<key>Kind</key><string>${escapeXML(kind)}</string>\n`) | |
} | |
const sortName = properties["sortName"] | |
if (sortName != null && sortName.length > 0 && sortName != name) { | |
xmlTrack = xmlTrack.concat(`<key>Sort Name</key><string>${escapeXML(sortName)}</string>\n`) | |
} | |
const sortComposer = properties["sortComposer"] | |
if (sortComposer != null && sortComposer.length > 0 && sortComposer != composer) { | |
xmlTrack = xmlTrack.concat(`<key>Sort Composer</key><string>${escapeXML(sortComposer)}</string>\n`) | |
} | |
const sortArtist = properties["sortArtist"] | |
if (sortArtist != null && sortArtist.length > 0 && sortArtist != artist) { | |
xmlTrack = xmlTrack.concat(`<key>Sort Artist</key><string>${escapeXML(sortArtist)}</string>\n`) | |
} | |
const sortAlbumArtist = properties["sortAlbumArtist"] | |
if (sortAlbumArtist != null && sortAlbumArtist.length > 0 && sortAlbumArtist != albumArtist) { | |
xmlTrack = xmlTrack.concat(`<key>Sort Album Artist</key><string>${escapeXML(sortAlbumArtist)}</string>\n`) | |
} | |
const sortAlbum = properties["sortAlbum"] | |
if (sortAlbum != null && sortAlbum.length > 0 && sortAlbum != album) { | |
xmlTrack = xmlTrack.concat(`<key>Album</key><string>${escapeXML(sortAlbum)}</string>\n`) | |
} | |
const disabled = !properties["enabled"] | |
if (disabled) { | |
xmlTrack = xmlTrack.concat(`<key>Disabled</key><true/>\n`) | |
} | |
const disliked = properties["disliked"] | |
if (disliked) { | |
xmlTrack = xmlTrack.concat(`<key>Disliked</key><true/>\n`) | |
} | |
const albumDisliked = properties["albumDisliked"] | |
if (albumDisliked) { | |
xmlTrack = xmlTrack.concat(`<key>Album Disliked</key><true/>\n`) | |
} | |
const loved = properties["loved"] | |
if (loved) { | |
xmlTrack = xmlTrack.concat(`<key>Loved</key><true/>\n`) | |
} | |
const albumLoved = properties["albumLoved"] | |
if (albumLoved) { | |
xmlTrack = xmlTrack.concat(`<key>Album Loved</key><true/>\n`) | |
} | |
const artworkCount = track.artworks.length | |
if (artworkCount > 0) { | |
xmlTrack = xmlTrack.concat(`<key>Artwork Count</key><integer>${artworkCount}</integer>\n`) | |
} | |
const unplayed = properties["unplayed"] | |
if (unplayed) { | |
xmlTrack = xmlTrack.concat(`<key>Unplayed</key><true/>\n`) | |
} | |
const mediaKind = properties["mediaKind"] | |
const podcast = mediaKind === "podcast" | |
if (podcast) { | |
xmlTrack = xmlTrack.concat(`<key>Podcast</key><true/>\n`) | |
} | |
const movie = mediaKind === "movie" | |
if (movie) { | |
xmlTrack = xmlTrack.concat(`<key>Movie</key><true/>\n`) | |
} | |
const homeVideo = mediaKind === "home video" | |
if (homeVideo) { | |
xmlTrack = xmlTrack.concat(`<key>Home Video</key><true/>\n`) | |
} | |
const tvShow = mediaKind === "TV show" | |
if (tvShow) { | |
xmlTrack = xmlTrack.concat(`<key>TV Show</key><true/>\n`) | |
} | |
const releaseDate = properties["releaseDate"] | |
if (releaseDate != null) { | |
xmlTrack = xmlTrack.concat(`<key>Release Date</key><date>${releaseDate.toiTunesISOString()}</date>\n`) | |
} | |
const compilation = properties["compilation"] | |
if (compilation) { | |
xmlTrack = xmlTrack.concat(`<key>Compilation</key><true/>\n`) | |
} | |
const hasVideo = properties["mediaKind"] != "Music Video" | |
if (hasVideo) { | |
xmlTrack = xmlTrack.concat(`<key>Has Video</key><true/>\n`) | |
} | |
const purchaserName = properties["downloaderName"] | |
if (purchaserName != null && purchaserName.length > 0) { | |
xmlTrack = xmlTrack.concat(`<key>Purchased</key><true/>\n`) | |
} | |
const season = properties["seasonNumber"] | |
if (season > 0) { | |
xmlTrack = xmlTrack.concat(`<key>Season</key><integer>${season}</integer>\n`) | |
} | |
const episodeOrder = properties["episodeNumber"] | |
if (episodeOrder > 0) { | |
xmlTrack = xmlTrack.concat(`<key>Episode Order</key><integer>${episodeOrder}</integer>\n`) | |
} | |
const series = properties["show"] | |
if (series != null && series.length > 0) { | |
xmlTrack = xmlTrack.concat(`<key>Series</key><string>${escapeXML(series)}</string>\n`) | |
} | |
const sortSeries = properties["sortShow"] | |
if (sortSeries != null && sortSeries.length > 0 && sortSeries != series) { | |
xmlTrack = xmlTrack.concat(`<key>Sort Series</key><string>${escapeXML(sortSeries)}</string>\n`) | |
} | |
const episode = properties["episodeID"] | |
if (episode != null && episode.length > 0) { | |
xmlTrack = xmlTrack.concat(`<key>Episode</key><string>${escapeXML(episode)}</string>\n`) | |
} | |
if (trackType === "URL") { | |
const location = properties["address"] | |
xmlTrack = xmlTrack.concat(`<key>Location</key><string>${escapeXML(encodeURI(location))}</string>\n`) | |
} | |
else if (trackType === "File") { | |
const location = properties["location"] | |
xmlTrack = xmlTrack.concat(`<key>Location</key><string>file://${escapeXML(encodeURI(location))}</string>\n`) | |
} | |
xmlTrack = xmlTrack.concat(`</dict>\n`) | |
// Protected? | |
// Category? (used for podcasts) | |
return xmlTrack | |
} | |
// very simple-minded parameter validation | |
const args = argv() | |
if (args.length < 4 || args.length > 5) { | |
console.log(`Invalid parameter: ${args.toString()}`) | |
ObjC.import('stdlib') | |
$.exit(128) | |
} | |
const music = Application("Music") | |
music.fixedIndexing(true) | |
const date = new Date().toiTunesISOString() | |
const version = music.version() | |
const source = music.sources[0] | |
const libraryId = source.persistentID() | |
const libraryPlaylist = source.libraryPlaylists[0] | |
const musicFolder = 'unknown' | |
const epoch = new Date("1904-01-01T00:00:00Z").getTime() | |
const header = `<?xml version="1.0" encoding="UTF-8"?> | |
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | |
<plist version="1.0"> | |
<dict> | |
<key>Major Version</key><integer>1</integer> | |
<key>Minor Version</key><integer>1</integer> | |
<key>Application Version</key><string>${version}</string> | |
<key>Date</key><date>${date}</date> | |
<key>Features</key><integer>5</integer> | |
<key>Show Content Ratings</key><true/> | |
<key>Library Persistent ID</key><string>${libraryId}</string> | |
<key>Creator</key><string>musicxml.js</string> | |
` | |
const footer = `<key>Music Folder</key><string>${escapeXML(musicFolder)}</string> | |
</dict> | |
</plist> | |
` | |
let out = '' | |
out = out.concat(header) | |
// iterate over tracks | |
out = out.concat(`<key>Tracks</key>\n<dict>\n`) | |
const allTracks = libraryPlaylist.tracks | |
const trackCount = allTracks.length | |
for (let i=0; i<trackCount; i++) { | |
out = out.concat(trackToXML(allTracks[i])) | |
} | |
out = out.concat(`</dict>\n`) | |
// iterate over playlists | |
out = out.concat(`<key>Playlists</key>\n<array>\n`) | |
const allPlaylists = source.playlists | |
const playlistCount = allPlaylists.length | |
for (let i=0; i<playlistCount; i++) { | |
out = out.concat(playlistToXML(allPlaylists[i])) | |
} | |
out = out.concat(`</array>\n`) | |
out = out.concat(footer) | |
// output | |
if (args.length == 5) { | |
// if file name is passed as argument, write to that file | |
const file = args[4].toString() | |
const nsPath = $(file).stringByStandardizingPath | |
fileStr = $.NSString.alloc.initWithUTF8String(out) | |
fileStr.writeToFileAtomicallyEncodingError(nsPath, true, $.NSUTF8StringEncoding, $()) | |
const message = `Exported library to: ${file}` | |
message | |
} else { | |
// just dump to standard out, if no file name is given | |
// how will this be char encoded? UTF-8? | |
out | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment