Here’s a script that’s really slow. It’s doing a bit more than your asking… and currently it’s just creating a “Master Tree”… From that you could then work thru it and create Folders in Finder / or Export M3U playlist files as needed. I’d add functions to add / calculate a finder path based on the “Root Export Path”. The M3U Playlist Create function can easily be made: of course change any iTunes references to Music
Notice the properties:
property trimTotal : true
property trimCount : 40
property includeSmarts : false
My iTunes has over 7000 playlists so I’ve included the ability to trim that down for testing to the trimCount. When trimTotal is true:
Also includeSmarts to include exclude smartPlaylists
use AppleScript version "2.4" -- Yosemite (10.10) or later
use framework "Foundation"
use scripting additions
-- classes, constants, and enums used
property NSDictionary : a reference to current application's NSDictionary
property NSMutableDictionary : a reference to current application's NSMutableDictionary
property NSMutableArray : a reference to current application's NSMutableArray
property NSPredicate : a reference to current application's NSPredicate
property NSArray : a reference to current application's NSArray
property everyUserPlaylist : {}
property finalTree : missing value
property allUserPlaylists : missing value
property rootPlaylists : missing value
property rootPlaylistsNames : missing value
property aPlaylist : missing value
property aPlaylistProps : missing value
property aPlaylistDict : missing value
property aTrackDict : missing value
property currentIndex : 0
property totalCount : 0
property progressTotalCount : 0
property progressDivider : 25
property progressUpdateInterval : 0
property progressCompleted : 0
property progressMainDesc : ""
property progressSubDesc : ""
property progressAtRoot : true
property progressMainUpdate : false
property progressTracksUpdate : false
property progressTracksCount : 0
property progressTracksCompleted : 0
property progressTracksDesc : ""
property progressTracksSubDesc : ""
property trimTotal : true
property trimCount : 40
property includeSmarts : false
property saveFilePath : missing value
property exportFolderPath : missing value
property logProgress : true
property logExtras : false
my setUpDefaults()
my selectExportFolder()
my selectSaveFile()
my loadUserPlaylists()
my parseRootPlaylists()
my createTreeFromRootPlaylists()
my saveMasterTree()
on selectExportFolder()
set exportFolderPath to choose folder with prompt ¬
"Select or Create Root Export Folder" default location path to music folder ¬
log {"exportFolderPath is:", exportFolderPath}
log {"class of exportFolderPath is:", class of exportFolderPath}
end selectExportFolder
on selectSaveFile()
set saveFilePath to choose file name with prompt ¬
"Select Name for Master Tree File" default name ¬
"ITunes Tree Master.XML" default location exportFolderPath
log {"saveFilePath is:", saveFilePath}
log {"class of saveFilePath is:", class of saveFilePath}
end selectSaveFile
on saveMasterTree()
if (logProgress) then log {"START saveMasterTree"}
set savedMaster to (finalTree's writeToURL:saveFilePath atomically:true)
log {"savedMaster is:", savedMaster}
end saveMasterTree
on setUpDefaults()
set allUserPlaylists to NSMutableArray's new()
set finalTree to NSMutableArray's new()
end setUpDefaults
on loadUserPlaylists()
if (logProgress) then log {"START loadUserPlaylists"}
tell application id "com.apple.iTunes"
--set everyUserPlaylist to get (every playlist whose special kind is none or special kind is folder)
if (includeSmarts) then
set everyUserPlaylist to (every playlist whose special kind is none or special kind is folder)
else
set everyUserPlaylist to (every playlist whose (special kind is none and smart is false) or special kind is folder)
end if
end tell
set totalCount to count of everyUserPlaylist
if (trimTotal) then
set totalCount to trimCount
log {"setting to trimCount is:", trimCount}
end if
my setUpProgressWithCount:totalCount progressDescription:"Creating Playlist Dictionaries for All User Playlists" subDescription:"Parsing Playlist"
repeat with currentIndex from 1 to totalCount
set aPlaylist to item currentIndex of everyUserPlaylist
if (aPlaylist's name ≠ "Home Videos") then
set aPlaylistDict to (my getBasePropertiesForPlaylist:aPlaylist)
(allUserPlaylists's addObject:aPlaylistDict)
end if
(my updateProgressWithCompletedSteps:currentIndex)
end repeat
my updateProgressCompletedMessage:"Completed Creating All Playlist Base Dictionaries" completedSubMessage:"Next Filter And Parse Root Playlists"
end loadUserPlaylists
on parseRootPlaylists()
if (logProgress) then log {"START parseRootPlaylists"}
set aPred to NSPredicate's predicateWithFormat:"parentName = 'ROOT'"
set rootPlaylists to allUserPlaylists's filteredArrayUsingPredicate:aPred
set rootPlaylistsNames to rootPlaylists's valueForKeyPath:"playlistName"
my resetProgress()
end parseRootPlaylists
on createTreeFromRootPlaylists()
if (logProgress) then log {"START createTreeFromRootPlaylists rootPlaylists count is:", count of rootPlaylists}
set finalTree to NSMutableArray's new()
repeat with aRootDict in rootPlaylists
set progressAtRoot to true
set isFolder to (aRootDict's valueForKey:"isFolder") as boolean
if (isFolder) then
set currentChildren to (my getPlaylistFolderChildren:aRootDict)
set currentChildrenCount to count of currentChildren
(aRootDict's setValue:(currentChildrenCount) forKey:"playlistCount")
(aRootDict's setValue:currentChildren forKey:"children")
else
set aPlaylistTracks to (my getPlaylistTracks:aRootDict)
set aPlaylistTracksCount to count of aPlaylistTracks
(aRootDict's setValue:(aPlaylistTracksCount) forKey:"playlistCount")
(aRootDict's setValue:aPlaylistTracks forKey:"playlistTracks")
end if
(finalTree's addObject:aRootDict)
end repeat
end createTreeFromRootPlaylists
on getPlaylistFolderChildren:aFolderDict
if (logProgress) then log {"START getPlaylistFolderChildren"}
set aFoldersChildren to NSMutableArray's new()
set aFolderID to (aFolderDict's valueForKey:"playlistPersistentID")
set aFolderName to (aFolderDict's valueForKey:"playlistName")
-- GET ALL THE PLAYLISTS who's parentPersistentID matches the currentFolder
--set aPred to NSPredicate's predicateWithFormat_(formatString, aValue, ...)
set parentPred to NSPredicate's predicateWithFormat_("parentPersistentID = %@", aFolderID)
set folderChildDicts to allUserPlaylists's filteredArrayUsingPredicate:parentPred
set folderChildCount to count of folderChildDicts
set aDesc to "Processing Folder:" & aFolderName & "'s Children"
set aSubDesc to "Processing ChildDict"
if (progressAtRoot) then
my setUpProgressWithCount:folderChildCount progressDescription:aDesc subDescription:aSubDesc
else
set aDesc to "Processing Sub Folder:" & aFolderName & "'s Children"
my addToProgressSubCount:folderChildCount progressAdditionalDescription:aDesc
end if
repeat with currentIndex from 1 to folderChildCount
set aChildDict to item currentIndex of folderChildDicts
set isFolder to (aChildDict's valueForKey:"isFolder") as boolean
if (isFolder) then
set progressAtRoot to false
set currentChildren to (my getPlaylistFolderChildren:aChildDict)
set currentChildrenCount to count of currentChildren
(aChildDict's setValue:(currentChildrenCount) forKey:"playlistCount")
(aChildDict's setValue:currentChildren forKey:"children")
else
set aPlaylistTracks to (my getPlaylistTracks:aChildDict)
set aPlaylistTracksCount to count of aPlaylistTracks
(aChildDict's setValue:(aPlaylistTracksCount) forKey:"playlistCount")
(aChildDict's setValue:aPlaylistTracks forKey:"playlistTracks")
end if
(aFoldersChildren's addObject:aChildDict)
(my updateProgressWithCompletedSteps:currentIndex)
end repeat
set aCompletedDescription to "FINISHED " & aDesc
set aSubMessage to "Parsed " & folderChildCount & " Total Children"
my updateProgressCompletedMessage:aCompletedDescription completedSubMessage:aSubMessage
return aFoldersChildren
end getPlaylistFolderChildren:
on getPlaylistTracks:aPlaylistDict
set playlistTracksDicts to NSMutableArray's new()
set aPlaylistID to (aPlaylistDict's valueForKey:"playlistID")
set aPlaylistName to (aPlaylistDict's valueForKey:"playlistName")
set aPlaylist to my findPlaylistWithID:aPlaylistID
if (aPlaylist ≠ missing value) then
tell application id "com.apple.iTunes"
set aPlaylistTracks to aPlaylist's tracks
set aTracksCount to count of aPlaylistTracks
set aDescription to "Fetching Tracks for Playlist:" & aPlaylistName
my setUpProgressTracksWithCount:aTracksCount progressTracksDescription:aDescription tracksSubDescription:"Parsing Playlist Track"
repeat with aTrackIndex from 1 to aTracksCount
set aTrackDict to NSMutableDictionary's new()
set aTrack to item aTrackIndex of aPlaylistTracks
set aPersistentID to aTrack's persistent ID
set aName to aTrack's name
set aArtist to aTrack's artist
set aAlbum to aTrack's album
set aLocation to aTrack's location
set aLocationPath to POSIX path of aLocation
(aTrackDict's setValue:(aTrackIndex) forKey:"playlistIndex")
(aTrackDict's setValue:aPersistentID forKey:"trackPersistentID")
(aTrackDict's setValue:aName forKey:"trackName")
(aTrackDict's setValue:aArtist forKey:"trackArtist")
(aTrackDict's setValue:aAlbum forKey:"trackAlbum")
-- SAVING NSArray or NSDictionary with URLs fails
-- (aTrackDict's setValue:aLocation forKey:"trackLocation")
(aTrackDict's setValue:aLocationPath forKey:"trackLocationPath")
(playlistTracksDicts's addObject:aTrackDict)
(my updateProgressTracksWithCompletedSteps:aTrackIndex)
end repeat
set aCompletedDescription to "FINISHED " & aDescription
set aSubMessage to "Parsed " & aTracksCount & " Total Tracks"
my updateProgressTracksCompletedMessage:aCompletedDescription completedSubMessage:aSubMessage
end tell
end if
return playlistTracksDicts
end getPlaylistTracks:
on getBasePropertiesForPlaylist:aPlaylist
set aPlaylistProps to NSMutableDictionary's new()
tell application id "com.apple.iTunes"
set aName to aPlaylist's name
set aPersistentID to aPlaylist's persistent ID
set aPlaylistID to aPlaylist's id
set isSmart to aPlaylist's smart
set aSpecialKind to aPlaylist's special kind
set aNodeType to "playlist"
set isFolder to false
if (aSpecialKind is folder) then
set aNodeType to "folder"
set isFolder to true
set isSmart to false
end if
set aParent to missing value
set aParentName to ""
set aParentPersistentID to ""
try
set aParent to aPlaylist's parent
end try
if (aParent is missing value) then
set aParentName to "ROOT"
else
set aParentName to aParent's name
set aParentPersistentID to aParent's persistent ID
end if
end tell
(aPlaylistProps's setValue:aName forKey:"playlistName")
(aPlaylistProps's setValue:aPersistentID forKey:"playlistPersistentID")
(aPlaylistProps's setValue:(aPlaylistID) forKey:"playlistID")
(aPlaylistProps's setValue:(isSmart) forKey:"isSmart")
(aPlaylistProps's setValue:aNodeType forKey:"nodeType")
(aPlaylistProps's setValue:(isFolder) forKey:"isFolder")
(aPlaylistProps's setValue:aParentName forKey:"parentName")
(aPlaylistProps's setValue:aParentPersistentID forKey:"parentPersistentID")
--log {"aPlaylistProps is", aPlaylistProps}
return aPlaylistProps
end getBasePropertiesForPlaylist:
on findPlaylistWithID:aID
set aPlaylistID to aID as integer
set aPlaylist to missing value
tell application id "com.apple.iTunes"
set aFoundPlaylist to missing value
set playlistExsists to exists (a reference to playlist id aPlaylistID)
if playlistExsists then
try
set aFoundPlaylist to playlist id aPlaylistID
on error theErr number theNum
log {"Can't find aFoundPlaylist " & theErr & " " & theNum & " " & aPlaylistID}
end try
end if
if (aFoundPlaylist ≠ missing value) then
set aPlaylist to get aFoundPlaylist
end if
end tell
return aPlaylist
end findPlaylistWithID:
on findOrCreateFolderWithName:aFolderName
tell application id "com.apple.iTunes"
set aFolder to missing value
set aFolderName to aFolderName as text
set folderExists to exists (a reference to folder playlist aFolderName)
if logCreate then
log {"folderExists is:", folderExists}
end if
if folderExists then
try
set aFolder to folder playlist aFolderName
on error theErr number theNum
log {"Can't find aFolder " & theErr & " " & theNum & " " & aFolderName}
end try
else
set aFolder to (make new folder playlist with properties {name:aFolderName})
end if
end tell
return aFolder
end findOrCreateFolderWithName:
on movePlaylistNamed:aPlaylistName toFolderNamed:aFolderName
set aPlaylist to (my findOrCreatePlaylistWithName:aPlaylistName)
set aFolder to (my findOrCreateFolderWithName:aFolderName)
set moveOK to (my moveITunesItem:aPlaylist toFolder:aFolder)
return moveOK
end movePlaylistNamed:toFolderNamed:
on moveFolderNamed:aFolderName toParentNamed:aParentName
set aFolder to (my findOrCreateFolderWithName:aFolderName)
set aParentFolder to (my findOrCreateFolderWithName:aParentName)
set moveOK to (my moveITunesItem:aFolder toFolder:aParentFolder)
return moveOK
end moveFolderNamed:toParentNamed:
on movePlaylist:aPlaylist toFolderNamed:aFolderName
set aFolder to (my findOrCreateFolderWithName:aFolderName)
set moveOK to (my moveITunesItem:aPlaylist toFolder:aFolder)
return moveOK
end movePlaylist:toFolderNamed:
on moveITunesItem:aItem toFolder:aFolder
set moveOK to true
set shouldMove to (my shouldMoveITunesItem:aItem toParent:aFolder)
if shouldMove then
tell application id "com.apple.iTunes"
try
move aItem to aFolder
on error theErr number theNum
log {"Can't move aItem to aFolder " & theErr & " " & theNum}
set moveOK to false
end try
end tell
end if
set moveOK to moveOK as boolean
return moveOK
end moveITunesItem:toFolder:
on shouldMoveITunesItem:aItem toParent:aFolder
set moveIt to true
tell application id "com.apple.iTunes"
if exists (aItem's parent) then
try
if (aItem's parent is aFolder) then
set moveIt to false
end if
end try
end if
end tell
return moveIt
end shouldMoveITunesItem:toParent:
on setUpProgressWithCount:aCount progressDescription:aDescription subDescription:subDescription
if (logProgress) then log {"START setUpProgress"}
-- Update the initial progress information
set progressMainUpdate to true
set progressTotalCount to aCount
set progressUpdateInterval to (progressTotalCount div progressDivider)
if (progressUpdateInterval = 0) then
set progressUpdateInterval to 1
end if
if (logProgress) then log {"progressUpdateInterval is:", progressUpdateInterval, " for progressTotalCount:", progressTotalCount}
set progressCompleted to 0
set progressMainDesc to aDescription
set progressSubDesc to subDescription
set progress total steps to progressTotalCount
set progress completed steps to progressCompleted
set progress description to progressMainDesc
set progress additional description to "Preparing to process."
end setUpProgressWithCount:progressDescription:subDescription:
on addToProgressSubCount:aAddCount progressAdditionalDescription:addSubDescription
if (logProgress) then log {"START addToProgressSubCount aAddCount is:", aAddCount}
-- Update the initial progress information
set progressTotalCount to progressTotalCount + aAddCount
set progressUpdateInterval to (progressTotalCount div progressDivider)
if (progressUpdateInterval = 0) then
set progressUpdateInterval to 1
end if
if (logProgress) then log {"progressUpdateInterval is:", progressUpdateInterval, " for progressTotalCount:", progressTotalCount}
set progressSubDesc to addSubDescription
set progress total steps to progressTotalCount
--set progress completed steps to 0
--set progress description to aDescription
set progress additional description to "Adding " & aAddCount & " sub tasks"
end addToProgressSubCount:progressAdditionalDescription:
on updateProgressWithCompletedSteps:completedSteps
-- Update the progress completed steps
set progressCompleted to completedSteps
set progress completed steps to progressCompleted
-- Update the progress detail if past progressUpdateInterval
if (progressCompleted mod progressUpdateInterval) = 0 then
set progress additional description to progressSubDesc & " " & progressCompleted & " of " & progressTotalCount
end if
end updateProgressWithCompletedSteps:
on updateProgressCompletedMessage:completedDescription completedSubMessage:subMessage
if (logProgress) then
log {"START updateProgressCompletedMessage"}
--my debugLogProgressMainInfo()
end if
-- Update the progress description completed
set progress description to completedDescription
set progress additional description to subMessage
if (progressCompleted ≥ progressTotalCount) then
my resetProgress()
end if
end updateProgressCompletedMessage:completedSubMessage:
on resetProgress()
-- Reset the progress information
set progress total steps to 0
set progress completed steps to 0
set progress description to ""
set progress additional description to ""
(*
set progressTotalCount to 0
set progressCompleted to 0
set progressMainDesc to ""
set progressSubDesc to ""
*)
end resetProgress
on prepareSwitchProgressMainMode()
set shouldBackUp to (progressTracksCount > 0 and progressTracksCompleted < progressTracksCount) as boolean
if (logProgress) then log {"shouldBackUp is:", shouldBackUp}
if (progressMainUpdate or shouldBackUp) then
my backUpProgressTracksInfo()
end if
my restoreProgressMainInfo()
end prepareSwitchProgressMainMode
on backUpProgressMainInfo()
set progressTotalCount to progress total steps
set progressCompleted to progress completed steps
set progressMainDesc to progress description
--set progressSubDesc to progress additional description
if (logProgress) then my debugLogProgressMainInfo()
end backUpProgressMainInfo
on restoreProgressMainInfo()
set progressMainUpdate to true
set progressTracksUpdate to false
set progress total steps to progressTotalCount
set progress completed steps to progressCompleted
set progress description to progressMainDesc
set progress additional description to progressSubDesc
end restoreProgressMainInfo
on debugLogProgressMainInfo()
log {"progressTracksUpdate is:", progressTracksUpdate}
log {"progressAtRoot is:", progressAtRoot}
log {"progressMainUpdate is:", progressMainUpdate}
log {"progressMainDesc is:", progressMainDesc}
log {"progressSubDesc is:", progressSubDesc}
log {"progressTotalCount is:", progressTotalCount}
log {"progressCompleted is:", progressCompleted}
log {"progressUpdateInterval is:", progressUpdateInterval}
end debugLogProgressMainInfo
on prepareSwitchProgressTracksMode()
if (logProgress) then log {"START prepareSwitchProgressTracksMode"}
if (progressMainUpdate or not progressTracksUpdate) then
my backUpProgressMainInfo()
end if
set progressMainUpdate to false
set progressTracksUpdate to true
end prepareSwitchProgressTracksMode
on setUpProgressTracksWithCount:aCount progressTracksDescription:aDescription tracksSubDescription:subDescription
if (logProgress) then log {"START setUpProgressTracksWithCount"}
my prepareSwitchProgressTracksMode()
-- Update the initial progress information
set progressTracksCount to aCount
set progressTracksCompleted to 0
set progressTracksDesc to aDescription
set progressTracksSubDesc to subDescription
set progress total steps to progressTracksCount
set progress completed steps to progressTracksCompleted
set progress description to progressTracksDesc
set progress additional description to "Preparing to process Tracks"
end setUpProgressTracksWithCount:progressTracksDescription:tracksSubDescription:
on updateProgressTracksWithCompletedSteps:completedSteps
-- Update the progress completed steps
set progressTracksCompleted to completedSteps
set progress completed steps to progressTracksCompleted
-- Update the progress detail if past progressUpdateInterval
set progress additional description to progressTracksSubDesc & " " & progressTracksCompleted & " of " & progressTracksCount
end updateProgressTracksWithCompletedSteps:
on updateProgressTracksCompletedMessage:completedDescription completedSubMessage:subMessage
-- Update the progress description completed
set progress description to completedDescription
set progress additional description to subMessage
my resetProgressTracks()
my prepareSwitchProgressMainMode()
end updateProgressTracksCompletedMessage:completedSubMessage:
on resetProgressTracks()
-- Reset the progress information
set progressTracksCount to 0
set progressTracksCompleted to 0
set progressTracksDesc to ""
set progressTracksSubDesc to ""
(*
set progress total steps to 0
set progress completed steps to 0
set progress description to ""
set progress additional description to ""
*)
end resetProgressTracks
on backUpProgressTracksInfo()
set progressTracksCount to progress total steps
set progressTracksCompleted to progress completed steps
set progressTracksDesc to progress description
set progressTracksSubDesc to progress additional description
if (logProgress) then my debugLogProgressTracksInfo()
end backUpProgressTracksInfo
on restoreProgressTracksInfo()
set progressTracksUpdate to true
set progressMainUpdate to false
set progress total steps to progressTracksCount
set progress completed steps to progressTracksCompleted
set progress description to progressTracksDesc
set progress additional description to progressTracksSubDesc
end restoreProgressTracksInfo
on debugLogProgressTracksInfo()
log {"progressAtRoot is:", progressAtRoot}
log {"progressMainUpdate is:", progressMainUpdate}
log {"progressTracksUpdate is:", progressTracksUpdate}
log {"progressTracksDesc is:", progressTracksDesc}
log {"progressTracksSubDesc is:", progressTracksSubDesc}
log {"progressTracksCount is:", progressTracksCount}
log {"progressTracksCompleted is:", progressTracksCompleted}
end debugLogProgressTracksInfo