Fix performance degredation introduced by 67b1c92

This change effectively reverts 67b1c92 while also making a
small tweak to prevent the reappearance of duplicate results.

-- core configuration --

-- configurable options --

-- limit number of songs to improve efficiency
property resultLimit : 25
-- whether or not to retrieve/display album artwork for each result
property albumArtEnabled : true

-- workflow parameters --

property homeFolder : (path to home folder as text)
property libraryFolder : (path to library folder from user domain as text)
property cacheFolder : (libraryFolder & "Caches:")
property alfredWorkflowDataFolder : (cacheFolder & "com.runningwithcrayons.Alfred-3:Workflow Data:")
property bundleId : "com.calebevans.playsong"
property workflowCacheFolder : (alfredWorkflowDataFolder & bundleId & ":") as text
property artworkCacheFolderName : "Album Artwork"
property artworkCachePath : (workflowCacheFolder & artworkCacheFolderName & ":")
property songArtworkNameSep : " | "
property defaultIconName : "resources/icon-noartwork.png"
-- the name of the playlist used by the workflow for playing songs
property workflowPlaylistName : "Alfred Play Song"
-- the text used to determine if a track is an audio file
property songDescriptor : "audio"
-- list of Alfred results
property resultList : {}

-- replaces substring in string with another substring
on replace(replaceThis, replaceWith, theString)

	set oldDelims to AppleScript's text item delimiters
	set AppleScript's text item delimiters to replaceThis
	set strItems to text items of theString
	set AppleScript's text item delimiters to replaceWith
	set newString to strItems as text
	set AppleScript's text item delimiters to oldDelims
	return newString

end replace

-- retrieve the plural/singular form of a quantity based on the given number
on quantifyNumber(theNumber, quantityName, pluralQuantityName)

	if theNumber is 1 then

		set theString to (theNumber as text) & space & quantityName

	else

		set theString to (theNumber as text) & space & pluralQuantityName

	end if

	return theString

end quantifyNumber

-- encodes JSON reserved characters in the given string
on encodeFeedbackChars(theString)

	set theString to replace("\\", "\\\\", theString)
	set theString to replace("\"", "\\\"", theString)
	return theString

end encodeFeedbackChars

-- decodes JSON reserved characters in the given string
on decodeFeedbackChars(theString)

	set theString to replace("\\\\", "\\", theString)
	set theString to replace("\\\"", "\"", theString)
	return theString

end decodeFeedbackChars

-- adds Alfred result to result list
on addResult(theResult)

	copy theResult to the end of resultList

end addResult

-- adds item for "No Results" message
on addNoResultsItem(query, queryType)

	addResult({uid:"no-results", valid:"no", title:"No Results Found", subtitle:("No " & queryType & "s matching '" & query & "'"), icon:defaultIconName})

end addNoResultsItem

on resultListIsFull()

	return (length of resultList is resultLimit)

end resultListIsFull

on resultListIsEmpty()

	return (length of resultList is 0)

end resultListIsFull

-- builds Alfred result item as JSON
on getResultFeedback(theResult)

	-- encode reserved JSON characters
	set resultUid to encodeFeedbackChars(uid of theResult)
	set resultValid to (valid of theResult) as text
	set resultTitle to encodeFeedbackChars(title of theResult)
	set resultSubtitle to encodeFeedbackChars(subtitle of theResult)

	if (icon of theResult) contains ":" then

		set resultIcon to encodeFeedbackChars(POSIX path of icon of theResult)

	else

		set resultIcon to icon of theResult

	end if

	set json to "{"
	set json to json & "\"uid\":\"" & resultUid & "\","
	set json to json & "\"arg\":\"" & resultUid & "\","
	set json to json & "\"valid\":\"" & resultValid & "\","
	set json to json & "\"title\":\"" & resultTitle & "\","
	set json to json & "\"subtitle\":\"" & resultSubtitle & "\","
	set json to json & "\"icon\":{\"path\":\"" & resultIcon & "\"}"
	set json to json & "}"
	return json

end getResultFeedback

-- retrieves JSON document for Alfred results
on getResultListFeedback()

	set json to "{\"items\": ["

	repeat with theResult in resultList

		set json to json & getResultFeedback(theResult)
		set json to json & ","

	end repeat

	-- remove trailing comma after last item
	set json to text 1 thru (length of json - 1) of json
	set json to json & "]}"
	return json

end getResultListFeedback

-- writes the given content to the given file
on fileWrite(theFile, theContent)

	set fileRef to open for access theFile with write permission
	try
		set eof of fileRef to 0
		write theContent to fileRef starting at eof
		close access fileRef
	on error
		close access fileRef
	end try

end fileWrite

-- builds path to album art for the given song
on getSongArtworkPath(theSong)

	if albumArtEnabled is false then return defaultIconName

	tell application "iTunes"

		set songArtist to artist of theSong
		set songAlbum to album of theSong
		-- generate a unique identifier for that album
		set songArtworkName to (songArtist & songArtworkNameSep & songAlbum) as text
		-- remove forbidden path characters
		set songArtworkName to replace(":", "", songArtworkName) of me
		set songArtworkName to replace("/", "", songArtworkName) of me
		set songArtworkName to replace(".", "", songArtworkName) of me
		set songArtworkPath to (artworkCachePath & songArtworkName & ".jpg")

	end tell

	tell application "Finder"

		-- cache artwork if it's not already cached
		if not (songArtworkPath exists) then

			tell application "iTunes"

				set songArtworks to artworks of theSong
				-- only save artwork if artwork exists for this song
				if (length of songArtworks) is 0 then

					-- use default iTunes icon if song has no artwork
					set songArtworkPath to defaultIconName

				else

					-- save artwork to file
					set songArtwork to data of (item 1 of songArtworks)
					fileWrite(songArtworkPath, songArtwork) of me

				end if

			end tell

		end if

	end tell

	return songArtworkPath

end getSongArtworkPath

-- creates album artwork cache
on createWorkflowPlaylist()

	tell application "iTunes"

		if not (user playlist workflowPlaylistName exists) then

			make new user playlist with properties {name:workflowPlaylistName, shuffle:false}

		end if

	end tell

end createWorkflowPlaylist

on clearQueue()

	tell application "iTunes"

		if user playlist workflowPlaylistName exists then

			delete tracks of user playlist workflowPlaylistName

		end if

	end tell

end clearQueue

on queueSongs(theSongs)

	tell application "iTunes"

		repeat with theSong in theSongs

			duplicate theSong to user playlist workflowPlaylistName

		end repeat

	end tell

end queueSongs

on playQueue()

	tell application "iTunes"

		if number of tracks in user playlist workflowPlaylistName is not 0 then

			play user playlist workflowPlaylistName

		end if

	end tell

end playQueue

-- brings queue into view in iTunes window
on focusQueue()

	tell application "iTunes"

		reveal user playlist workflowPlaylistName

	end tell

end focusQueue

on getPlaylistSongs(playlistId)

	tell application "iTunes"

		set thePlaylist to first user playlist whose id is playlistId
		set theSongs to every track of thePlaylist

	end tell

	return theSongs

end getPlaylistSongs

-- retrieves list of artist names for the given genre
on getGenreArtists(genreName)

	tell application "iTunes"

		set theSongs to every track of playlist 2 whose genre is genreName and kind contains songDescriptor
		set artistNames to {}

		repeat with theSong in theSongs

			if (artist of theSong) is not in artistNames then

				set artistNames to artistNames & (artist of theSong)

			end if

		end repeat

	end tell

	return artistNames

end getGenreArtists

-- retrieves list of songs within the given genre, sorted by artist
on getGenreSongs(genreName)

	set artistNames to getGenreArtists(genreName) of me
	set theSongs to {}

	repeat with artistName in artistNames

		set theSongs to theSongs & getArtistSongs(artistName) of me

	end repeat

	return theSongs

end getGenreSongs

-- retrieves list of album names for the given artist
on getArtistAlbums(artistName)

	tell application "iTunes"

		set theSongs to every track of playlist 2 whose artist is artistName and kind contains songDescriptor
		set albumNames to {}

		repeat with theSong in theSongs

			if (album of theSong) is not in albumNames then

				set albumNames to albumNames & (album of theSong)

			end if

		end repeat

	end tell

	return albumNames

end getArtistAlbums

-- retrieves list of songs by the given artist, sorted by album
on getArtistSongs(artistName)

	tell application "iTunes"

		set albumNames to getArtistAlbums(artistName) of me
		set theSongs to {}

		repeat with albumName in albumNames

			set albumSongs to (every track of playlist 2 whose artist is artistName and album is albumName and kind contains songDescriptor)
			set albumSongs to sortSongsByAlbumOrder(albumSongs) of me
			set theSongs to theSongs & albumSongs

		end repeat

	end tell

	return theSongs

end getArtistSongs

-- retrieves list of songs in the given album
on getAlbumSongs(albumName)

	tell application "iTunes"

		set theSongs to every track of playlist 2 whose album is albumName and kind contains songDescriptor
		set theSongs to sortSongsByAlbumOrder(theSongs) of me

	end tell

	return theSongs

end getAlbumSongs

-- sorts songs from the same album by track number
on sortSongsByAlbumOrder(theSongs)

	tell application "iTunes"

		set theSongsSorted to theSongs

		if length of theSongs is greater than 1 then

			set trackCount to track count of (item 1 of theSongs)
			set discCount to disc count of (item 1 of theSongs)

			if trackCount is not 0 and discCount is not 0 then

				set theSongsSorted to {} as list

				repeat with discIndex from 1 to discCount

					repeat with songIndex from 1 to trackCount

						repeat with theSong in theSongs

							if disc number of theSong is discIndex and track number of theSong is songIndex then

								set nextSong to theSong
								copy nextSong to the end of theSongsSorted

							end if

						end repeat

					end repeat

				end repeat

			end if

		end if

	end tell

	return theSongsSorted

end sortSongsByAlbumOrder

-- retrieves the song with the given ID
on getSong(songId)

	tell application "iTunes"

		set theSong to first track of playlist 2 whose database ID is songId and kind contains songDescriptor

	end tell

	return theSong

end getSong

-- retrieves a list of objects or names matching the given query and type
on getResultsFromQuery(query, queryType)

	set evalScript to run script "
	script

		on findResults(query, queryType, resultLimit, songDescriptor)

			tell application \"iTunes\"

				set theSongs to (get every track in playlist 2 whose " & queryType & " starts with query and kind contains songDescriptor)

				if length of theSongs < resultLimit then

					set theSongs to theSongs & (get every track in playlist 2 whose " & queryType & " contains (space & query) and " & queryType & " does not start with query and kind contains songDescriptor)

				end if

				if length of theSongs < resultLimit then

					set theSongs to theSongs & (get every track in playlist 2 whose " & queryType & " contains query and " & queryType & " does not start with query and " & queryType & " does not contain (space & query) and kind contains songDescriptor)

				end if

				if length of theSongs is 0 then

					if queryType is \"name\" then

						set theSongs to theSongs & (search playlist 2 for query only songs)

					else if queryType is not \"genre\" then

						set theSongs to theSongs & (search playlist 2 for query only " & queryType & "s)

					end if

				end if

				if queryType is \"name\" then

					if length of theSongs > resultLimit then

						set theSongs to items 1 thru resultLimit of theSongs

					end if

					set theResults to theSongs

				else

					set theResults to {}

					repeat with theSong in theSongs

						if length of theResults is resultLimit then exit repeat

						set theResult to " & queryType & " of theSong

						if theResult is not in theResults then

							set theResults to theResults & theResult

						end if

					end repeat

				end if

			end tell

			return theResults

		end findResults

	end script
	"

	evalScript's findResults(query, queryType, resultLimit, songDescriptor)

end getResultsFromQuery

-- returns the given string with leading and trailing whitespace removed
on trimWhitespace(theString)

	-- trim leading whitespace
	repeat while theString begins with space

		if length of theString is 1 then return ""
		set theString to text 2 thru end of theString

	end repeat

	-- trim trailing whitespace
	repeat while theString ends with space

		set theString to text 1 thru ((length of theString) - 1) of theString

	end repeat

	return theString

end trimWhitespace

-- queues the song with the given ID
on queueSong(songId)

	set theSong to getSong(songId)
	queueSongs({theSong})

end queueSong

-- queues all songs belonging to the given album
on queueAlbum(albumName)

	set albumName to decodeFeedbackChars(albumName)
	set theSongs to getAlbumSongs(albumName)
	queueSongs(theSongs)

end queueAlbum

-- queues all songs by the given artist
on queueArtist(artistName)

	set artistName to decodeFeedbackChars(artistName)
	set theSongs to getArtistSongs(artistName)
	queueSongs(theSongs)

end queueArtist

-- queues all songs within the given genre
on queueGenre(genreName)

	set genreName to decodeFeedbackChars(genreName)
	set theSongs to getGenreSongs(genreName)
	queueSongs(theSongs)

end queueGenre

-- queues all songs in the given playlist
on queuePlaylist(playlistId)

	set theSongs to getPlaylistSongs(playlistId)
	queueSongs(theSongs)

end queuePlaylist

-- parses the given result query to retrieve type and id of item to queue
on parseResultQuery(query)

	set pos to offset of "-" in query
	set theType to text 1 thru (pos - 1) of query
	set theId to text (pos + 1) thru end of query
	return {type:theType, id:theId}

end parseResultQuery

on queue(query)

	set typeAndId to parseResultQuery(query)
	set theType to type of typeAndId
	set theId to id of typeAndId

	createWorkflowPlaylist()

	if theType is "song" then
		queueSong(theId)
	else if theType is "album" then
		queueAlbum(theId)
	else if theType is "artist" then
		queueArtist(theId)
	else if theType is "genre" then
		queueGenre(theId)
	else if theType is "playlist" then
		queuePlaylist(theId)
	else
		log "Unknown type: " & theType
	end if

	focusQueue()

end queue

on play(query)

	clearQueue()
	queue(query)
	focusQueue()
	playQueue()

end play

-- core configuration --

-- configurable options --

-- limit number of songs to improve efficiency
property resultLimit : 25
-- whether or not to retrieve/display album artwork for each result
property albumArtEnabled : true

-- workflow parameters --

property homeFolder : (path to home folder as text)
property libraryFolder : (path to library folder from user domain as text)
property cacheFolder : (libraryFolder & "Caches:")
property alfredWorkflowDataFolder : (cacheFolder & "com.runningwithcrayons.Alfred-3:Workflow Data:")
property bundleId : "com.calebevans.playsong"
property workflowCacheFolder : (alfredWorkflowDataFolder & bundleId & ":") as text
property artworkCacheFolderName : "Album Artwork"
property artworkCachePath : (workflowCacheFolder & artworkCacheFolderName & ":")
property songArtworkNameSep : " | "
property defaultIconName : "resources/icon-noartwork.png"
-- the name of the playlist used by the workflow for playing songs
property workflowPlaylistName : "Alfred Play Song"
-- the text used to determine if a track is an audio file
property songDescriptor : "audio"
-- list of Alfred results
property resultList : {}

-- replaces substring in string with another substring
on replace(replaceThis, replaceWith, theString)

	set oldDelims to AppleScript's text item delimiters
	set AppleScript's text item delimiters to replaceThis
	set strItems to text items of theString
	set AppleScript's text item delimiters to replaceWith
	set newString to strItems as text
	set AppleScript's text item delimiters to oldDelims
	return newString

end replace

-- retrieve the plural/singular form of a quantity based on the given number
on quantifyNumber(theNumber, quantityName, pluralQuantityName)

	if theNumber is 1 then

		set theString to (theNumber as text) & space & quantityName

	else

		set theString to (theNumber as text) & space & pluralQuantityName

	end if

	return theString

end quantifyNumber

-- encodes JSON reserved characters in the given string
on encodeFeedbackChars(theString)

	set theString to replace("\\", "\\\\", theString)
	set theString to replace("\"", "\\\"", theString)
	return theString

end encodeFeedbackChars

-- decodes JSON reserved characters in the given string
on decodeFeedbackChars(theString)

	set theString to replace("\\\\", "\\", theString)
	set theString to replace("\\\"", "\"", theString)
	return theString

end decodeFeedbackChars

-- adds Alfred result to result list
on addResult(theResult)

	copy theResult to the end of resultList

end addResult

-- adds item for "No Results" message
on addNoResultsItem(query, queryType)

	addResult({uid:"no-results", valid:"no", title:"No Results Found", subtitle:("No " & queryType & "s matching '" & query & "'"), icon:defaultIconName})

end addNoResultsItem

on resultListIsFull()

	return (length of resultList is resultLimit)

end resultListIsFull

on resultListIsEmpty()

	return (length of resultList is 0)

end resultListIsFull

-- builds Alfred result item as JSON
on getResultFeedback(theResult)

	-- encode reserved JSON characters
	set resultUid to encodeFeedbackChars(uid of theResult)
	set resultValid to (valid of theResult) as text
	set resultTitle to encodeFeedbackChars(title of theResult)
	set resultSubtitle to encodeFeedbackChars(subtitle of theResult)

	if (icon of theResult) contains ":" then

		set resultIcon to encodeFeedbackChars(POSIX path of icon of theResult)

	else

		set resultIcon to icon of theResult

	end if

	set json to "{"
	set json to json & "\"uid\":\"" & resultUid & "\","
	set json to json & "\"arg\":\"" & resultUid & "\","
	set json to json & "\"valid\":\"" & resultValid & "\","
	set json to json & "\"title\":\"" & resultTitle & "\","
	set json to json & "\"subtitle\":\"" & resultSubtitle & "\","
	set json to json & "\"icon\":{\"path\":\"" & resultIcon & "\"}"
	set json to json & "}"
	return json

end getResultFeedback

-- retrieves JSON document for Alfred results
on getResultListFeedback()

	set json to "{\"items\": ["

	repeat with theResult in resultList

		set json to json & getResultFeedback(theResult)
		set json to json & ","

	end repeat

	-- remove trailing comma after last item
	set json to text 1 thru (length of json - 1) of json
	set json to json & "]}"
	return json

end getResultListFeedback

-- writes the given content to the given file
on fileWrite(theFile, theContent)

	set fileRef to open for access theFile with write permission
	try
		set eof of fileRef to 0
		write theContent to fileRef starting at eof
		close access fileRef
	on error
		close access fileRef
	end try

end fileWrite

-- builds path to album art for the given song
on getSongArtworkPath(theSong)

	if albumArtEnabled is false then return defaultIconName

	tell application "iTunes"

		set songArtist to artist of theSong
		set songAlbum to album of theSong
		-- generate a unique identifier for that album
		set songArtworkName to (songArtist & songArtworkNameSep & songAlbum) as text
		-- remove forbidden path characters
		set songArtworkName to replace(":", "", songArtworkName) of me
		set songArtworkName to replace("/", "", songArtworkName) of me
		set songArtworkName to replace(".", "", songArtworkName) of me
		set songArtworkPath to (artworkCachePath & songArtworkName & ".jpg")

	end tell

	tell application "Finder"

		-- cache artwork if it's not already cached
		if not (songArtworkPath exists) then

			tell application "iTunes"

				set songArtworks to artworks of theSong
				-- only save artwork if artwork exists for this song
				if (length of songArtworks) is 0 then

					-- use default iTunes icon if song has no artwork
					set songArtworkPath to defaultIconName

				else

					-- save artwork to file
					set songArtwork to data of (item 1 of songArtworks)
					fileWrite(songArtworkPath, songArtwork) of me

				end if

			end tell

		end if

	end tell

	return songArtworkPath

end getSongArtworkPath

-- creates album artwork cache
on createWorkflowPlaylist()

	tell application "iTunes"

		if not (user playlist workflowPlaylistName exists) then

			make new user playlist with properties {name:workflowPlaylistName, shuffle:false}

		end if

	end tell

end createWorkflowPlaylist

on clearQueue()

	tell application "iTunes"

		if user playlist workflowPlaylistName exists then

			delete tracks of user playlist workflowPlaylistName

		end if

	end tell

end clearQueue

on queueSongs(theSongs)

	tell application "iTunes"

		repeat with theSong in theSongs

			duplicate theSong to user playlist workflowPlaylistName

		end repeat

	end tell

end queueSongs

on playQueue()

	tell application "iTunes"

		if number of tracks in user playlist workflowPlaylistName is not 0 then

			play user playlist workflowPlaylistName

		end if

	end tell

end playQueue

-- brings queue into view in iTunes window
on focusQueue()

	tell application "iTunes"

		reveal user playlist workflowPlaylistName

	end tell

end focusQueue

on getPlaylistSongs(playlistId)

	tell application "iTunes"

		set thePlaylist to first user playlist whose id is playlistId
		set theSongs to every track of thePlaylist

	end tell

	return theSongs

end getPlaylistSongs

-- retrieves list of artist names for the given genre
on getGenreArtists(genreName)

	tell application "iTunes"

		set theSongs to every track of playlist 2 whose genre is genreName and kind contains songDescriptor
		set artistNames to {}

		repeat with theSong in theSongs

			if (artist of theSong) is not in artistNames then

				set artistNames to artistNames & (artist of theSong)

			end if

		end repeat

	end tell

	return artistNames

end getGenreArtists

-- retrieves list of songs within the given genre, sorted by artist
on getGenreSongs(genreName)

	set artistNames to getGenreArtists(genreName) of me
	set theSongs to {}

	repeat with artistName in artistNames

		set theSongs to theSongs & getArtistSongs(artistName) of me

	end repeat

	return theSongs

end getGenreSongs

-- retrieves list of album names for the given artist
on getArtistAlbums(artistName)

	tell application "iTunes"

		set theSongs to every track of playlist 2 whose artist is artistName and kind contains songDescriptor
		set albumNames to {}

		repeat with theSong in theSongs

			if (album of theSong) is not in albumNames then

				set albumNames to albumNames & (album of theSong)

			end if

		end repeat

	end tell

	return albumNames

end getArtistAlbums

-- retrieves list of songs by the given artist, sorted by album
on getArtistSongs(artistName)

	tell application "iTunes"

		set albumNames to getArtistAlbums(artistName) of me
		set theSongs to {}

		repeat with albumName in albumNames

			set albumSongs to (every track of playlist 2 whose artist is artistName and album is albumName and kind contains songDescriptor)
			set albumSongs to sortSongsByAlbumOrder(albumSongs) of me
			set theSongs to theSongs & albumSongs

		end repeat

	end tell

	return theSongs

end getArtistSongs

-- retrieves list of songs in the given album
on getAlbumSongs(albumName)

	tell application "iTunes"

		set theSongs to every track of playlist 2 whose album is albumName and kind contains songDescriptor
		set theSongs to sortSongsByAlbumOrder(theSongs) of me

	end tell

	return theSongs

end getAlbumSongs

-- sorts songs from the same album by track number
on sortSongsByAlbumOrder(theSongs)

	tell application "iTunes"

		set theSongsSorted to theSongs

		if length of theSongs is greater than 1 then

			set trackCount to track count of (item 1 of theSongs)
			set discCount to disc count of (item 1 of theSongs)

			if trackCount is not 0 and discCount is not 0 then

				set theSongsSorted to {} as list

				repeat with discIndex from 1 to discCount

					repeat with songIndex from 1 to trackCount

						repeat with theSong in theSongs

							if disc number of theSong is discIndex and track number of theSong is songIndex then

								set nextSong to theSong
								copy nextSong to the end of theSongsSorted

							end if

						end repeat

					end repeat

				end repeat

			end if

		end if

	end tell

	return theSongsSorted

end sortSongsByAlbumOrder

-- retrieves the song with the given ID
on getSong(songId)

	tell application "iTunes"

		set theSong to first track of playlist 2 whose database ID is songId and kind contains songDescriptor

	end tell

	return theSong

end getSong

-- retrieves a list of objects or names matching the given query and type
on getResultsFromQuery(query, queryType)

	set evalScript to run script "
	script

		on findResults(query, queryType, resultLimit, songDescriptor)

			tell application \"iTunes\"

				set theSongs to (get every track in playlist 2 whose " & queryType & " starts with query and kind contains songDescriptor)

				if length of theSongs < resultLimit then

					set theSongs to theSongs & (get every track in playlist 2 whose " & queryType & " contains (space & query) and " & queryType & " does not start with query and kind contains songDescriptor)

				end if

				if length of theSongs < resultLimit then

					set theSongs to theSongs & (get every track in playlist 2 whose " & queryType & " contains query and " & queryType & " does not start with query and " & queryType & " does not contain (space & query) and kind contains songDescriptor)

				end if

				if length of theSongs < resultLimit then

					set searchSongs to {}

					if queryType is \"name\" then

						set searchSongs to (search playlist 2 for query only songs)

					else if queryType is not \"genre\" then

						set searchSongs to (search playlist 2 for query only " & queryType & "s)

					end if

					repeat with searchSong in searchSongs

						set duplicateSong to false

						repeat with theSong in theSongs

							if (database ID of theSong) is (database ID of searchSong) then

								set duplicateSong to true
								exit repeat

							end if

						end repeat

						if duplicateSong is false then

							copy searchSong to end of theSongs

						end if

					end repeat

				end if

				if queryType is \"name\" then

					if length of theSongs > resultLimit then

						set theSongs to items 1 thru resultLimit of theSongs

					end if

					set theResults to theSongs

				else

					set theResults to {}

					repeat with theSong in theSongs

						if length of theResults is resultLimit then exit repeat

						set theResult to " & queryType & " of theSong

						if theResult is not in theResults then

							set theResults to theResults & theResult

						end if

					end repeat

				end if

			end tell

			return theResults

		end findResults

	end script
	"

	evalScript's findResults(query, queryType, resultLimit, songDescriptor)

end getResultsFromQuery

-- returns the given string with leading and trailing whitespace removed
on trimWhitespace(theString)

	-- trim leading whitespace
	repeat while theString begins with space

		if length of theString is 1 then return ""
		set theString to text 2 thru end of theString

	end repeat

	-- trim trailing whitespace
	repeat while theString ends with space

		set theString to text 1 thru ((length of theString) - 1) of theString

	end repeat

	return theString

end trimWhitespace

-- queues the song with the given ID
on queueSong(songId)

	set theSong to getSong(songId)
	queueSongs({theSong})

end queueSong

-- queues all songs belonging to the given album
on queueAlbum(albumName)

	set albumName to decodeFeedbackChars(albumName)
	set theSongs to getAlbumSongs(albumName)
	queueSongs(theSongs)

end queueAlbum

-- queues all songs by the given artist
on queueArtist(artistName)

	set artistName to decodeFeedbackChars(artistName)
	set theSongs to getArtistSongs(artistName)
	queueSongs(theSongs)

end queueArtist

-- queues all songs within the given genre
on queueGenre(genreName)

	set genreName to decodeFeedbackChars(genreName)
	set theSongs to getGenreSongs(genreName)
	queueSongs(theSongs)

end queueGenre

-- queues all songs in the given playlist
on queuePlaylist(playlistId)

	set theSongs to getPlaylistSongs(playlistId)
	queueSongs(theSongs)

end queuePlaylist

-- parses the given result query to retrieve type and id of item to queue
on parseResultQuery(query)

	set pos to offset of "-" in query
	set theType to text 1 thru (pos - 1) of query
	set theId to text (pos + 1) thru end of query
	return {type:theType, id:theId}

end parseResultQuery

on queue(query)

	set typeAndId to parseResultQuery(query)
	set theType to type of typeAndId
	set theId to id of typeAndId

	createWorkflowPlaylist()

	if theType is "song" then
		queueSong(theId)
	else if theType is "album" then
		queueAlbum(theId)
	else if theType is "artist" then
		queueArtist(theId)
	else if theType is "genre" then
		queueGenre(theId)
	else if theType is "playlist" then
		queuePlaylist(theId)
	else
		log "Unknown type: " & theType
	end if

	focusQueue()

end queue

on play(query)

	clearQueue()
	queue(query)
	focusQueue()
	playQueue()

end play


Remove git pull from closing Vagrant

Remove git pull from closing Vagrant

set close_vagrant to "z puppet; vagrant halt -f;"

-- close vagrant
tell application "iTerm"
	activate
	set myterm to (make new terminal)
	tell myterm
		launch session "Default"
		set mysession to current session
	end tell

	tell mysession
		write text close_vagrant
	end tell

	delay 5
end tell

-- quit iTerm
ignoring application responses
	tell application "iTerm"
		quit
	end tell
end ignoring

delay 2
tell application "System Events" to keystroke return

set close_vagrant to "z puppet; git pull; vagrant halt -f;"

-- close vagrant
tell application "iTerm"
	activate
	set myterm to (make new terminal)
	tell myterm
		launch session "Default"
		set mysession to current session
	end tell

	tell mysession
		write text close_vagrant
	end tell

	delay 5
end tell

-- quit iTerm
ignoring application responses
	tell application "iTerm"
		quit
	end tell
end ignoring

delay 2
tell application "System Events" to keystroke return


Replaced reference to PlistBuddy using standard AppleScript

property kExplicitlyProcessedOptions : {"--human-dir ", "--machine-dir ", "--output-dir ", "--log-command"}

tell application "Xcode"
	if not (exists active project document) then
		error "No active project. Please open an Xcode project and re-run the script."
	try
		my updateProjectXmod(project of active project document)
	on error errMsg
		my logger("Xmod.scpt exception: " & errMsg)
	end try
end tell

on updateProjectXmod(_project)
	tell application "Xcode"
		-- Iterate over every .xcdatamodel in the project.
		set modelList to every file reference of _project whose file kind is "wrapper.xcdatamodel" and comments contains "xmod"
		repeat with modelItr in modelList
			my updateModel(_project, modelItr, comments of modelItr)
		end repeat
		-- Iterate over every .xcdatamodeld (notice the 'd') in the project.
		set modeldList to every group of _project whose name contains ".xcdatamodeld" and comments contains "xmod"
		repeat with modeldIt in modeldList
			-- Find the 'active' model version
			set currentVersionFile to full path of modeldIt & "/.xccurrentversion"
			tell application "System Events"
				set currentVersionPlist to contents of property list file currentVersionFile
				set activeModelVersionFilename to value of property list item "_XCCurrentVersionName" of currentVersionPlist
			end tell
			set activeModelVersion to full path of modeldIt & "/" & activeModelVersionFilename
			set modelItr to item 1 of (every file reference of modeldIt whose name is activeModelVersionFilename)
			-- Then update it
			my updateModel(_project, modelItr, comments of modeldIt)
		end repeat
	end tell
end updateProjectXmod

on updateModel(_project, modelItr, theComments)
	tell application "Xcode"

				set modelInfo to my getModelInfo(full path of modelItr, theComments)
				set humanGroupRef to null
				set machineGroupRef to null

				-- Figure out the model's parent group.
				-- Unversioned models are simple files so their group is the answer.
				-- Version models are bundles, which are represented as pseudo-groups in Xcode. You can't ask a pseudo-group for its parent group,
				-- so we discover it by querying project-wide all groups that contain the pseudo-group and using the first result.
				set modelItemRef to item 1 of (every item reference of _project whose full path is (full path of modelItr))
				set modelGroupRef to group of modelItemRef
				if isBundle of modelInfo then
					set modelGroupRef to item 1 of (every group of _project whose groups contains modelGroupRef)
				end if

				-- look for a group in the project that has the full path of the human manageable files
				set groupRefs to (every group of _project whose full path is (humanDirPath of modelInfo))
				-- if we don't find one, create it in the same group as the model file
				if (count of groupRefs) = 0 then
					tell modelGroupRef
						-- if we have different human and machine paths, then append '_human' to the model name for the group name
						if (humanDirPath of modelInfo) is not equal to (machineDirPath of modelInfo) then
							make new group with properties {full path:(humanDirPath of modelInfo), name:(name of modelInfo & "_human")}
						else
							make new group with properties {full path:(humanDirPath of modelInfo), name:(name of modelInfo)}
						end if
						set groupRefs to (every group of _project whose full path is (humanDirPath of modelInfo))
					end tell
				end if
				set humanGroupRef to item 1 of groupRefs

				-- if we have different human and machine paths, then look for a group for the machine files
				if (humanDirPath of modelInfo) is not equal to (machineDirPath of modelInfo) then
					set groupRefs to (every group of _project whose full path is (machineDirPath of modelInfo))
					-- if we don't find one, create it in the same group as the model file... named with '_machine' appended to the model name
					if (count of groupRefs) = 0 then
						tell modelGroupRef
							make new group with properties {full path:(machineDirPath of modelInfo), name:(name of modelInfo & "_machine")}
						end tell
						set groupRefs to (every group of _project whose full path is (machineDirPath of modelInfo))
					end if
					set machineGroupRef to item 1 of groupRefs
				end if

				-- Create the do shell script string and append any custom per model options to it, skipping the ones we've already parsed out
				set logCommand to false
				set theScript to "/usr/bin/mogenerator --model '" & full path of modelItr & "' --human-dir '" & (humanDirPath of modelInfo) & "' --machine-dir '" & (machineDirPath of modelInfo) & "'"
				set theParagraphs to every paragraph of theComments
				repeat with theParagraph in theParagraphs
					if theParagraph starts with "--" then
						set isExplicitlyProcessedOption to false
						repeat with explicitlyProcessedOption in kExplicitlyProcessedOptions
							if theParagraph starts with explicitlyProcessedOption then set isExplicitlyProcessedOption to true
						end repeat
						if not isExplicitlyProcessedOption then
							set theScript to theScript & " " & the text of theParagraph
						end if
						if theParagraph starts with "--log-command" then
							set logCommand to true
						end if
					end if
				end repeat

				--	Meat.
				if logCommand then
					my logger(theScript)
				end if
				do shell script theScript

				my addFilesFromPathToGroup(_project, modelItr, humanGroupRef)
				if machineGroupRef is not null then
					my addFilesFromPathToGroup(_project, modelItr, machineGroupRef)
				end if
	end tell
end updateModel

on everyTargetWithBuildFilePath(_project, _buildFilePath)
	set theResult to {}
	tell application "Xcode"
		repeat with targetItr in (every target of _project)
			repeat with buildFileItr in build files of targetItr
				if full path of file reference of buildFileItr is _buildFilePath then set theResult to theResult & {(targetItr as anything)}
			end repeat
		end repeat
	end tell
	return theResult
end everyTargetWithBuildFilePath

on getModelInfo(modelFileUnixPath, theComments)
	set modelFilePosixRef to POSIX file modelFileUnixPath
	set modelFileAlias to modelFilePosixRef as alias
	set theOutputDir to ""
	set theHumanDir to ""
	set theMachineDir to ""

	-- check for any directory path options specified in the commments
	set theParagraphs to every paragraph of theComments
	repeat with theParagraph in theParagraphs
		set paragraphLength to (length of theParagraph)
		if (offset of "--human-dir " in theParagraph) = 1 then
			set remainderOffset to (length of "--human-dir ") + 1
			if remainderOffset > paragraphLength then
				set theHumanDir to ""
			else
				set theHumanDir to text remainderOffset thru -1 of theParagraph
			end if
		else if (offset of "--machine-dir " in theParagraph) = 1 then
			set remainderOffset to (length of "--machine-dir ") + 1
			if remainderOffset > paragraphLength then
				set theMachineDir to ""
			else
				set theMachineDir to text remainderOffset thru -1 of theParagraph
			end if
		else if (offset of "--output-dir " in theParagraph) = 1 then
			set remainderOffset to (length of "--output-dir ") + 1
			if remainderOffset > paragraphLength then
				set theOutputDir to ""
			else
				set theOutputDir to text remainderOffset thru -1 of theParagraph
			end if
		end if
	end repeat

	-- remove any leading/trailing spaces or tabs from the paths
	if (length of theHumanDir) > 0 then
		repeat while character 1 of theHumanDir = tab or character 1 of theHumanDir = space
			set theHumanDir to text 2 thru -1 of theHumanDir
		end repeat
		repeat while last character of theHumanDir = tab or last character of theHumanDir = space
			set theHumanDir to text 1 thru -2 of theHumanDir
		end repeat
	end if

	if (length of theMachineDir) > 0 then
		repeat while character 1 of theMachineDir = tab or character 1 of theMachineDir = space
			set theMachineDir to text 2 thru -1 of theMachineDir
		end repeat
		repeat while last character of theMachineDir = tab or last character of theMachineDir = space
			set theMachineDir to text 1 thru -2 of theMachineDir
		end repeat
	end if

	if (length of theOutputDir) > 0 then
		repeat while character 1 of theOutputDir = tab or character 1 of theOutputDir = space
			set theOutputDir to text 2 thru -1 of theOutputDir
		end repeat
		repeat while last character of theOutputDir = tab or last character of theOutputDir = space
			set theOutputDir to text 1 thru -2 of theOutputDir
		end repeat
	end if

	-- get actual the model info and modify value for theOutputDir if necessary
	tell application "Finder"
		set modelFileFolder to folder of modelFileAlias

		set isModelBundle to name of modelFileFolder ends with ".xcdatamodeld"
		if isModelBundle then
			set modelFileAlias to (folder of modelFileAlias) as alias -- it's a bundle, go up one folder
			set modelFileFolder to folder of modelFileAlias
		end if

		set modelFileName to name of modelFileAlias
		if isModelBundle then
			set extensionLength to length of ".xcdatamodeld"
		else
			set extensionLength to length of ".xcdatamodel"
		end if

		set modelName to text 1 thru -(extensionLength + 1) of modelFileName -- pull off the extension

		-- if we didn't find any directory specifiers in the comments, then make theOutputDir the folder containg the model file
		if ((length of theOutputDir) = 0) and ((length of theHumanDir) = 0) and ((length of theMachineDir) = 0) then
			if not (exists folder modelName of modelFileFolder) then
				make folder at modelFileFolder with properties {name:modelName}
			set modelSrcFolder to folder modelName of modelFileFolder
			set theOutputDir to text 1 thru -2 of (POSIX path of (modelSrcFolder as alias))
			-- otherwise, if theOutputDir isn't a full path itself already, set theOutputDir to a full path relative to the model file
		else if theOutputDir does not start with "/" then
			set modelFolderPath to text 1 thru -2 of (POSIX path of (modelFileFolder as alias))
			if (length of theOutputDir) = 0 then
				set theOutputDir to modelFolderPath
			else
				copy modelFolderPath & "/" & theOutputDir to theOutputDir
			end if
		end if

	end tell

	-- if theHumanDir is empty, use theOutputDir value instead, if theHumanDir not empty and it isn't a full path, then treat it as a relative path to theOutputDir
	if (length of theHumanDir) = 0 then
		set theHumanDir to theOutputDir
	else if theHumanDir does not start with "/" then
		copy theOutputDir & "/" & theHumanDir to theHumanDir
	end if

	-- if theMachineDir is empty, use theOutputDir value instead, if theMachineDir not empty and it isn't a full path, then treat it as a relative path to theOutputDir
	if (length of theMachineDir) = 0 then
		set theMachineDir to theOutputDir
	else if theMachineDir does not start with "/" then
		copy theOutputDir & "/" & theMachineDir to theMachineDir
	end if

	-- ensure the directories exist
	do shell script "mkdir -p '" & theHumanDir & "'"
	do shell script "mkdir -p '" & theMachineDir & "'"

	-- the following should resolve any symlinks, '..', or '~' in the paths
	tell application "System Events"
		set dirAlias to POSIX file theHumanDir as alias
		set theHumanDir to (POSIX path of (dirAlias as alias))
		set dirAlias to POSIX file theMachineDir as alias
		set theMachineDir to (POSIX path of (dirAlias as alias))
	end tell

	return {name:modelName, isBundle:isModelBundle, machineDirPath:theMachineDir, humanDirPath:theHumanDir}

end getModelInfo

on logger(msg)
	do shell script "logger '" & msg & "'"
end logger

on addFilesFromPathToGroup(_project, modelItr, groupRef)
	tell application "Xcode"
		-- get the full path of the directory that the groupRef represents
		set groupPath to full path of groupRef

		--	Build a list of source files in the group's directory
		tell application "System Events"
			set modelSrcDirAlias to POSIX file groupPath as alias
			set fileList to (every file of modelSrcDirAlias whose (name ends with ".m" or name ends with ".mm" or name ends with ".h"))
			set pathList to {}
			repeat with fileItem in fileList
				set pathList to pathList & POSIX path of fileItem
			end repeat
		end tell

		--	Add the source files to the group and the model's targets, but only if they don't exist already
		set targetList to null
		repeat with pathItr in pathList
			tell groupRef
				set fileRefs to (every file reference of _project whose full path is pathItr)
				if (count of fileRefs) = 0 then
					set modelSrcFileRef to make new file reference with properties {full path:pathItr, name:name of (info for POSIX file pathItr)}
					if targetList is null then
						set targetList to my everyTargetWithBuildFilePath(_project, full path of modelItr)
					end if
					repeat with targetIndex from 1 to (count of targetList)
						set targetItr to item targetIndex of targetList
						add modelSrcFileRef to targetItr
					end repeat
				end if
			end tell
		end repeat
	end tell
end addFilesFromPathToGroup

property kExplicitlyProcessedOptions : {"--human-dir ", "--machine-dir ", "--output-dir ", "--log-command"}

tell application "Xcode"
	if not (exists active project document) then
		error "No active project. Please open an Xcode project and re-run the script."
	try
		my updateProjectXmod(project of active project document)
	on error errMsg
		my logger("Xmod.scpt exception: " & errMsg)
	end try
end tell

on updateProjectXmod(_project)
	tell application "Xcode"
		-- Iterate over every .xcdatamodel in the project.
		set modelList to every file reference of _project whose file kind is "wrapper.xcdatamodel" and comments contains "xmod"
		repeat with modelItr in modelList
			my updateModel(_project, modelItr, comments of modelItr)
		end repeat
		-- Iterate over every .xcdatamodeld (notice the 'd') in the project.
		set modeldList to every group of _project whose name contains ".xcdatamodeld" and comments contains "xmod"
		repeat with modeldIt in modeldList
			-- Find the 'active' model version
			set currentVersionFile to full path of modeldIt & "/.xccurrentversion"
			set activeModelVersionFilename to do shell script "/usr/libexec/PlistBuddy -c 'print _XCCurrentVersionName' " & currentVersionFile
			set activeModelVersion to full path of modeldIt & "/" & activeModelVersionFilename
			set modelItr to item 1 of (every file reference of modeldIt whose name is activeModelVersionFilename)
			-- Then update it
			my updateModel(_project, modelItr, comments of modeldIt)
		end repeat
	end tell
end updateProjectXmod

on updateModel(_project, modelItr, theComments)
	tell application "Xcode"

				set modelInfo to my getModelInfo(full path of modelItr, theComments)
				set humanGroupRef to null
				set machineGroupRef to null

				-- Figure out the model's parent group.
				-- Unversioned models are simple files so their group is the answer.
				-- Version models are bundles, which are represented as pseudo-groups in Xcode. You can't ask a pseudo-group for its parent group,
				-- so we discover it by querying project-wide all groups that contain the pseudo-group and using the first result.
				set modelItemRef to item 1 of (every item reference of _project whose full path is (full path of modelItr))
				set modelGroupRef to group of modelItemRef
				if isBundle of modelInfo then
					set modelGroupRef to item 1 of (every group of _project whose groups contains modelGroupRef)
				end if

				-- look for a group in the project that has the full path of the human manageable files
				set groupRefs to (every group of _project whose full path is (humanDirPath of modelInfo))
				-- if we don't find one, create it in the same group as the model file
				if (count of groupRefs) = 0 then
					tell modelGroupRef
						-- if we have different human and machine paths, then append '_human' to the model name for the group name
						if (humanDirPath of modelInfo) is not equal to (machineDirPath of modelInfo) then
							make new group with properties {full path:(humanDirPath of modelInfo), name:(name of modelInfo & "_human")}
						else
							make new group with properties {full path:(humanDirPath of modelInfo), name:(name of modelInfo)}
						end if
						set groupRefs to (every group of _project whose full path is (humanDirPath of modelInfo))
					end tell
				end if
				set humanGroupRef to item 1 of groupRefs

				-- if we have different human and machine paths, then look for a group for the machine files
				if (humanDirPath of modelInfo) is not equal to (machineDirPath of modelInfo) then
					set groupRefs to (every group of _project whose full path is (machineDirPath of modelInfo))
					-- if we don't find one, create it in the same group as the model file... named with '_machine' appended to the model name
					if (count of groupRefs) = 0 then
						tell modelGroupRef
							make new group with properties {full path:(machineDirPath of modelInfo), name:(name of modelInfo & "_machine")}
						end tell
						set groupRefs to (every group of _project whose full path is (machineDirPath of modelInfo))
					end if
					set machineGroupRef to item 1 of groupRefs
				end if

				-- Create the do shell script string and append any custom per model options to it, skipping the ones we've already parsed out
				set logCommand to false
				set theScript to "/usr/bin/mogenerator --model '" & full path of modelItr & "' --human-dir '" & (humanDirPath of modelInfo) & "' --machine-dir '" & (machineDirPath of modelInfo) & "'"
				set theParagraphs to every paragraph of theComments
				repeat with theParagraph in theParagraphs
					if theParagraph starts with "--" then
						set isExplicitlyProcessedOption to false
						repeat with explicitlyProcessedOption in kExplicitlyProcessedOptions
							if theParagraph starts with explicitlyProcessedOption then set isExplicitlyProcessedOption to true
						end repeat
						if not isExplicitlyProcessedOption then
							set theScript to theScript & " " & the text of theParagraph
						end if
						if theParagraph starts with "--log-command" then
							set logCommand to true
						end if
					end if
				end repeat

				--	Meat.
				if logCommand then
					my logger(theScript)
				end if
				do shell script theScript

				my addFilesFromPathToGroup(_project, modelItr, humanGroupRef)
				if machineGroupRef is not null then
					my addFilesFromPathToGroup(_project, modelItr, machineGroupRef)
				end if
	end tell
end updateModel

on everyTargetWithBuildFilePath(_project, _buildFilePath)
	set theResult to {}
	tell application "Xcode"
		repeat with targetItr in (every target of _project)
			repeat with buildFileItr in build files of targetItr
				if full path of file reference of buildFileItr is _buildFilePath then set theResult to theResult & {(targetItr as anything)}
			end repeat
		end repeat
	end tell
	return theResult
end everyTargetWithBuildFilePath

on getModelInfo(modelFileUnixPath, theComments)
	set modelFilePosixRef to POSIX file modelFileUnixPath
	set modelFileAlias to modelFilePosixRef as alias
	set theOutputDir to ""
	set theHumanDir to ""
	set theMachineDir to ""

	-- check for any directory path options specified in the commments
	set theParagraphs to every paragraph of theComments
	repeat with theParagraph in theParagraphs
		set paragraphLength to (length of theParagraph)
		if (offset of "--human-dir " in theParagraph) = 1 then
			set remainderOffset to (length of "--human-dir ") + 1
			if remainderOffset > paragraphLength then
				set theHumanDir to ""
			else
				set theHumanDir to text remainderOffset thru -1 of theParagraph
			end if
		else if (offset of "--machine-dir " in theParagraph) = 1 then
			set remainderOffset to (length of "--machine-dir ") + 1
			if remainderOffset > paragraphLength then
				set theMachineDir to ""
			else
				set theMachineDir to text remainderOffset thru -1 of theParagraph
			end if
		else if (offset of "--output-dir " in theParagraph) = 1 then
			set remainderOffset to (length of "--output-dir ") + 1
			if remainderOffset > paragraphLength then
				set theOutputDir to ""
			else
				set theOutputDir to text remainderOffset thru -1 of theParagraph
			end if
		end if
	end repeat

	-- remove any leading/trailing spaces or tabs from the paths
	if (length of theHumanDir) > 0 then
		repeat while character 1 of theHumanDir = tab or character 1 of theHumanDir = space
			set theHumanDir to text 2 thru -1 of theHumanDir
		end repeat
		repeat while last character of theHumanDir = tab or last character of theHumanDir = space
			set theHumanDir to text 1 thru -2 of theHumanDir
		end repeat
	end if

	if (length of theMachineDir) > 0 then
		repeat while character 1 of theMachineDir = tab or character 1 of theMachineDir = space
			set theMachineDir to text 2 thru -1 of theMachineDir
		end repeat
		repeat while last character of theMachineDir = tab or last character of theMachineDir = space
			set theMachineDir to text 1 thru -2 of theMachineDir
		end repeat
	end if

	if (length of theOutputDir) > 0 then
		repeat while character 1 of theOutputDir = tab or character 1 of theOutputDir = space
			set theOutputDir to text 2 thru -1 of theOutputDir
		end repeat
		repeat while last character of theOutputDir = tab or last character of theOutputDir = space
			set theOutputDir to text 1 thru -2 of theOutputDir
		end repeat
	end if

	-- get actual the model info and modify value for theOutputDir if necessary
	tell application "Finder"
		set modelFileFolder to folder of modelFileAlias

		set isModelBundle to name of modelFileFolder ends with ".xcdatamodeld"
		if isModelBundle then
			set modelFileAlias to (folder of modelFileAlias) as alias -- it's a bundle, go up one folder
			set modelFileFolder to folder of modelFileAlias
		end if

		set modelFileName to name of modelFileAlias
		if isModelBundle then
			set extensionLength to length of ".xcdatamodeld"
		else
			set extensionLength to length of ".xcdatamodel"
		end if

		set modelName to text 1 thru -(extensionLength + 1) of modelFileName -- pull off the extension

		-- if we didn't find any directory specifiers in the comments, then make theOutputDir the folder containg the model file
		if ((length of theOutputDir) = 0) and ((length of theHumanDir) = 0) and ((length of theMachineDir) = 0) then
			if not (exists folder modelName of modelFileFolder) then
				make folder at modelFileFolder with properties {name:modelName}
			set modelSrcFolder to folder modelName of modelFileFolder
			set theOutputDir to text 1 thru -2 of (POSIX path of (modelSrcFolder as alias))
			-- otherwise, if theOutputDir isn't a full path itself already, set theOutputDir to a full path relative to the model file
		else if theOutputDir does not start with "/" then
			set modelFolderPath to text 1 thru -2 of (POSIX path of (modelFileFolder as alias))
			if (length of theOutputDir) = 0 then
				set theOutputDir to modelFolderPath
			else
				copy modelFolderPath & "/" & theOutputDir to theOutputDir
			end if
		end if

	end tell

	-- if theHumanDir is empty, use theOutputDir value instead, if theHumanDir not empty and it isn't a full path, then treat it as a relative path to theOutputDir
	if (length of theHumanDir) = 0 then
		set theHumanDir to theOutputDir
	else if theHumanDir does not start with "/" then
		copy theOutputDir & "/" & theHumanDir to theHumanDir
	end if

	-- if theMachineDir is empty, use theOutputDir value instead, if theMachineDir not empty and it isn't a full path, then treat it as a relative path to theOutputDir
	if (length of theMachineDir) = 0 then
		set theMachineDir to theOutputDir
	else if theMachineDir does not start with "/" then
		copy theOutputDir & "/" & theMachineDir to theMachineDir
	end if

	-- ensure the directories exist
	do shell script "mkdir -p '" & theHumanDir & "'"
	do shell script "mkdir -p '" & theMachineDir & "'"

	-- the following should resolve any symlinks, '..', or '~' in the paths
	tell application "System Events"
		set dirAlias to POSIX file theHumanDir as alias
		set theHumanDir to (POSIX path of (dirAlias as alias))
		set dirAlias to POSIX file theMachineDir as alias
		set theMachineDir to (POSIX path of (dirAlias as alias))
	end tell

	return {name:modelName, isBundle:isModelBundle, machineDirPath:theMachineDir, humanDirPath:theHumanDir}

end getModelInfo

on logger(msg)
	do shell script "logger '" & msg & "'"
end logger

on addFilesFromPathToGroup(_project, modelItr, groupRef)
	tell application "Xcode"
		-- get the full path of the directory that the groupRef represents
		set groupPath to full path of groupRef

		--	Build a list of source files in the group's directory
		tell application "System Events"
			set modelSrcDirAlias to POSIX file groupPath as alias
			set fileList to (every file of modelSrcDirAlias whose (name ends with ".m" or name ends with ".mm" or name ends with ".h"))
			set pathList to {}
			repeat with fileItem in fileList
				set pathList to pathList & POSIX path of fileItem
			end repeat
		end tell

		--	Add the source files to the group and the model's targets, but only if they don't exist already
		set targetList to null
		repeat with pathItr in pathList
			tell groupRef
				set fileRefs to (every file reference of _project whose full path is pathItr)
				if (count of fileRefs) = 0 then
					set modelSrcFileRef to make new file reference with properties {full path:pathItr, name:name of (info for POSIX file pathItr)}
					if targetList is null then
						set targetList to my everyTargetWithBuildFilePath(_project, full path of modelItr)
					end if
					repeat with targetIndex from 1 to (count of targetList)
						set targetItr to item targetIndex of targetList
						add modelSrcFileRef to targetItr
					end repeat
				end if
			end tell
		end repeat
	end tell
end addFilesFromPathToGroup