Search code examples
databaseapplescriptmetadatamanipulate

AppleScript to edit an XML file?


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!


Solution

  • 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