AppleScript to edit an XML file?

190 Views Asked by At

I have an Apple Music Library output file that looks like this:

    <key>6871</key>
    <dict>
        <key>Track ID</key><integer>6871</integer>
        <key>Name</key><string>12 Wake Up Call</string>
        <key>Artist</key><string>Rebelution</string>
        <key>Album Artist</key><string>Rebelution</string>
        <key>Grouping</key><string>AllMusic</string>
        <key>Kind</key><string>Apple Music AAC audio file</string>
        <key>Size</key><integer>6178208</integer>
        <key>Total Time</key><integer>257332</integer>
        <key>Year</key><integer>2009</integer>
        <key>Date Modified</key><date>2011-11-22T23:32:45Z</date>
        <key>Date Added</key><date>2011-12-14T23:30:26Z</date>
        <key>Bit Rate</key><integer>256</integer>
        <key>Sample Rate</key><integer>44100</integer>
        <key>Play Count</key><integer>101</integer>
        <key>Play Date</key><integer>3717804040</integer>
        <key>Play Date UTC</key><date>2021-10-23T07:20:40Z</date>
        <key>Skip Count</key><integer>10</integer>
        <key>Skip Date</key><date>2020-09-16T14:39:31Z</date>
        <key>Rating</key><integer>60</integer>
        <key>Album Rating</key><integer>60</integer>
        <key>Album Rating Computed</key><true/>
        <key>Normalization</key><integer>1699</integer>
        <key>Artwork Count</key><integer>1</integer>
        <key>Persistent ID</key><string>56B43C03AFF476E5</string>
        <key>Track Type</key><string>Remote</string>
        <key>Apple Music</key><true/>
    </dict>

I am trying to make this easier to store in a database (I don't understand SQL, but that's the end goal). For now I am adding and looking up "entries" in an excel sheet. I am able to manipulate the XML file manually by pasting it into a workbook, then I have to use ablebits and vlookups and a bunch of other time consuming operations which I paste into a new text file. End goal of this question is to get my "XML" file to look like this:

<key>5056</key> 
<dict>  
    <TrackID>5056</TrackID>
    <Name>Heart Like a Lion</Name>
    <Artist>Rebelution</Artist>
    <AlbumArtist>Rebelution</AlbumArtist>
    <Composer>Eric Ariel Rachmany, Marley D. Williams, Rourke Carey &#38; Wesley Dallas Finley</Composer>
    <Album>Courage to Grow</Album>
    <Grouping>LIBRARY</Grouping>
    <Genre>Reggae</Genre>z
    <Kind>Apple Music AAC audio file</Kind>
    <Size>11679958</Size>
    <TotalTime>338413</TotalTime>
    <DiscNumber>1</DiscNumber>
    <DiscCount>1</DiscCount>
    <TrackNumber>2</TrackNumber>
    <TrackCount>12</TrackCount>
    <Year>2007</Year>
    <DateModified>2021-11-10T08:29:23Z</DateModified>
    <DateAdded>2021-11-10T08:29:23Z</DateAdded>
    <BitRate>256</BitRate>
    <SampleRate>44100</SampleRate>
    <PlayCount>8</PlayCount>
    <PlayDate>3747937611</PlayDate>
    <PlayDateUTC>2022-10-07T01:46:51Z</PlayDateUTC>
    <ReleaseDate>2007-06-08T12:00:00Z</ReleaseDate>
    <Rating>100</Rating>
    <AlbumRating>60</AlbumRating>
    <AlbumRatingComputed></AlbumRatingComputed>
    <ArtworkCount>1</ArtworkCount>
    <SortAlbum>Courage to Grow</SortAlbum>
    <SortArtist>Rebelution</SortArtist>
    <SortName>Heart Like a Lion</SortName>
    <PersistentID>AD1A6E4E78F9C79D</PersistentID>
    <TrackType>Remote</TrackType>
    <AppleMusic></AppleMusic>
</dict> 

Anything will help, this has become more time consuming and difficult than I thought.

Im also open to alternative routes... I just want to backup my metadata because I lost it once (recovered it manually as mentioned above), but I also have some good ideas for making playlists based on timestamps of metadata values.

Oh side note... Im also open to using another language if that's easier. I have minimal background in code and have been teaching myself AppleScript since my scrips are mostly interacting with Apple stuff.

Thanks!

1

There are 1 best solutions below

0
red_menace On BEST ANSWER

AppleScriptObjC can be used to access the various Cocoa frameworks, for example to read a plist/xml file into an NSDictionary (similar to a record), where the various keys can be accessed programmatically, and for utilities such as date formatting, list sorting, etc.

There is an NSXMLNode class that can be used to create the elements, but in this case manually converting the dictionary keys isn't quite as wordy.

The following script creates a plain XML file from an Apple Music Library export. It extracts the specified key items into a track element and uses the track ID as an element attribute:

use framework "Foundation" -- for the AppleScriptObjC bits
use scripting additions

# the dictionary keys to extract (use an empty list {} for everything):
property keyNames : {"Name", "Kind", "Size", "Total Time", "Date Added", "Track Type", "Location"}

property keepSet : missing value -- this will be an NSSet of the keys
property indent : "  " -- formatting

on run -- create an XML file for track data from an exported Music Library plist/XML file
   if keyNames is not in {"", {}, missing value} then set keepSet to current application's NSMutableSet's setWithArray:(keyNames as list)
   set fileURL to current application's NSURL's fileURLWithPath:(POSIX path of (choose file of type {"com.apple.property-list", "public.xml"} with prompt "Choose the Music Library export file to process:"))
   set fileData to current application's NSData's dataWithContentsOfURL:fileURL
   try -- read file data (source XML file needs to be in Apple's property list format)
      set plist to (current application's NSPropertyListSerialization's propertyListWithData:fileData options:(current application's NSPropertyListMutableContainersAndLeaves) format:(missing value) |error|:(missing value))
      if plist is missing value then error "The chosen file is not an Apple plist/XML file."
      set trackDict to (plist's valueForKey:"Tracks") -- dictionary of tracks
      if trackDict is missing value then error "The chosen file does not have a 'Tracks' key in the root directory."
   on error errmess
      display alert "Script Error" message errmess
      error number -128 -- cancel
   end try
   set theResult to ""
   repeat with trackItem in trackDict's allKeys()
      set trackKeyPath to "Tracks." & (trackItem as text) -- dictionary for individual track key
      set theResult to theResult & addWrapper(trackItem as text, (XMLtext from (plist's valueForKeyPath:trackKeyPath)))
   end repeat
   writeToFile((choose file name default name "Converted Library.xml"), addWrapper(missing value, theResult))
end run

# return XML text from simple key/value pairs of a dictionary
on XMLtext from dictionary
   set XMLElements to {}
   set candidate to current application's NSMutableSet's setWithArray:(dictionary's allKeys())
   if keepSet is not missing value then candidate's intersectSet:keepSet -- remove other keys
   repeat with keyItem in candidate's allObjects()
      try
         set theItem to (dictionary's valueForKey:keyItem)
         set theValue to theItem as text -- test
      on error errmess -- can't coerce object to text
         set theClass to current application's NSStringFromClass(theItem's class) as text
         if theClass contains "Date" then -- format NSDate
            set theValue to (current application's NSISO8601DateFormatter's alloc's init()'s stringFromDate:theItem) as text
         else -- something needing additional formatting or processing such as a collection, etc
            log theClass & ":  " & errmess
            set theValue to "*ERROR*" -- or add formatting for the object
         end if
      end try
      set keyName to (keyItem's lowercaseString's stringByReplacingOccurrencesOfString:" " withString:"_") -- no spaces in key names
      set end of XMLElements to indent & indent & "<" & keyName & ">" & theValue & "</" & keyName & ">" & linefeed -- can also use NSXMLNode
   end repeat
   set elementArray to current application's NSArray's arrayWithArray:XMLElements
   return (elementArray's sortedArrayUsingSelector:"compare:") as list as text -- sort
end XMLtext

# add wrappers for individual track entries or the document
to addWrapper(theKey, theText)
   if theKey is not missing value then -- wrap individual track elements - the key is used as an attribute
      return linefeed & indent & "<track id=\"" & theKey & "\">" & linefeed & theText & indent & "</track>" & linefeed
   else -- wrapper and root element for a standard XML document
      return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>
<!-- track data extracted " & ((current date) as «class isot» as string) & " from exported Apple Music Library -->
<music_tracks>" & theText & "</music_tracks>" & linefeed
   end if
end addWrapper

on writeToFile(filePath, whatever)
   try
      set fileRef to (open for access filePath with write permission)
      set eof of fileRef to 0 -- overwrite existing
      write whatever to fileRef starting at eof
      close access fileRef
   on error
      try
         close access fileRef
      end try
   end try
end writeToFile