--
--  slideshow -- Observe and Control Slideshow Applications
--  Copyright (c) 2014-2015 Ralf S. Engelschall <http://engelschall.com>
--
--  This Source Code Form is subject to the terms of the Mozilla Public
--  License (MPL), version 2.0. If a copy of the MPL was not distributed
--  with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
--
--  File:     connector-osx-kn6.js
--  Purpose:  connector engine for Apple Keynote 6 under Mac OS X
--  Language: AppleScript
--

--  utility function
on filterText(this_text, allowed_chars)
   set new_text to ""
   repeat with this_char in this_text
       set x to the offset of this_char in allowed_chars
       if x is not 0 then
           set new_text to (new_text & this_char) as string
       end if
   end repeat
   return new_text
end filterText

--  utility function
on replaceText(this_text, search_string, replacement_string)
   set AppleScript's text item delimiters to the search_string
   set the item_list to every text item of this_text
   set AppleScript's text item delimiters to the replacement_string
   set this_text to the item_list as string
   set AppleScript's text item delimiters to ""
   return this_text
end replace_chars

--  utility function
on asciiCharset()
    set charset to " \"'!#$%()*+,-./:;<=>?@[\\]^_{|}~"
    set charset to (charset & "0123456789")
    set charset to (charset & "ABCDEFGHIJKLMNOPQRSTUVWXYZ")
    set charset to (charset & "abcdefghijklmnopqrstuvwxyz")
    return charset
end asciiCharset

--  get application state
on knGetState()
    set state to "closed"
    tell application "System Events"
        set is_running to (exists (some process whose name is "Keynote"))
    end tell
    if is_running then
       try
            set state to "running"
            tell application "Keynote"
                if false then -- FIXME: how to detect play mode?
                    set state to "viewing"
                else if (get count of slides of front document) > 0 then
                    set state to "editing"
                end if
            end tell
        on error errMsg
        end try
    end if
    return state
end knGetState

--  get Apple Keynote current slide
on knGetCurSlide()
    try
        tell application "Keynote"
            if false then -- FIXME: how to detect play mode?
                return (get slide number of current slide of front document) -- FIXME: wrong!
            else
                return (get slide number of current slide of front document)
            end if
        end tell
    on error errMsg
        return 0
    end try
end knGetCurSlide

--  get Apple Keynote maximum slide
on knGetMaxSlide()
    try
        tell application "Keynote"
            return (get count of slides of front document)
        end tell
    on error errMsg
        return 0
    end try
end knGetMaxSlide

--  the STATE command
on cmdSTATE()
    set state to knGetState()
    if state is "closed" then
        set position to 0
        set slides to 0
    else
        set position to knGetCurSlide()
        set slides to knGetMaxSlide()
    end if
    return ("{ \"response\": { " & ¬
        "\"state\": \"" & state & "\", " & ¬
        "\"position\": " & position & ", " & ¬
        "\"slides\": " & slides & " " & ¬
    "} }")
end cmdSTATE

--  the INFO command
on cmdINFO()
    set output to ""
    if knGetMaxSlide() is 0 then
        error "still no active presentation"
    end if
    tell application "Keynote"
        set thePresentation to front document
        set theTitles to ""
        set theNotes to ""
        set slideCount to (get count of slides of thePresentation)
        repeat with slideNum from 1 to slideCount
            set theSlide to slide slideNum of thePresentation

            set theTitle to (get object text of default title item of theSlide) as string
            if theTitles is not "" then
                set theTitles to (theTitles & ", ")
            end if
            set theTitles to (theTitles & "\"" & (my replaceText(theTitle, "\"", "\\\"")) & "\"")

            set theNote to (presenter notes of theSlide) as string
            set theNote to (my filterText(theNote, my asciiCharset()))
            if theNotes is not "" then
                set theNotes to (theNotes & ", ")
            end if
            set theNotes to (theNotes & "\"" & (my replaceText(theNote, "\"", "\\\"")) & "\"")

        end repeat
        set theTitles to ("[ " & theTitles & " ]")
        set theNotes to ("[ " & theNotes & " ]")
        set output to ("{ \"response\": { \"titles\": " & theTitles & ", \"notes\": " & theNotes & " } }")
    end tell
    return output
end cmdINFO

--  the control commands
on cmdCTRL(command, arg)
    set state to knGetState()
    if command is "BOOT" then
        if state is not "closed" then
            error "application already running"
        end if
        tell application "Keynote"
            activate
        end tell
    else if command is "QUIT" then
        if state is "closed" then
            error "application already closed"
        end if
        tell application "Keynote"
            quit
        end tell
    else if command is "OPEN" then
        if state is "editing" or state is "viewing" then
            error "active presentation already existing"
        end if
        tell application "Keynote"
            tell application "Finder" to set thePath to ¬
                POSIX file (POSIX path of (container of (path to me) as string) & (arg)) as alias
            open thePath
        end tell
    else if command is "CLOSE" then
        if state is "closed" or state is "running" then
            error "still no active presentation"
        end if
        tell application "Keynote"
            close front document
        end tell
    else if command is "START" then
        if state is "closed" or state is "running" then
            error "still no active presentation"
        end if
        if state is "viewing" then
            error "active presentation already viewing"
        end if
        tell application "Keynote"
            start front document from (slide 1 of front document)
        end tell
    else if command is "STOP" then
        if state is not "viewing" then
            error "no active slideshow"
        end if
        tell application "Keynote"
            stop front document
        end tell
    else if command is "PAUSE" then
        if state is not "viewing" then
            error "no active slideshow"
        end if
        tell application "Keynote"
            activate
            tell application "System Events" to keystroke "b"
        end tell
    else if command is "RESUME" then
        if state is not "viewing" then
            error "no active slideshow"
        end if
        tell application "Keynote"
            activate
            tell application "System Events" to keystroke "b"
        end tell
    else if command is "FIRST" then
        if state is not "viewing" then
            error "no active slideshow"
        end if
        tell application "Keynote"
            repeat with i from 1 to (my knGetMaxSlide())
                try
                    show previous
                on error errMsg
                    exit repeat
                end try
                delay 0.02
            end repeat
        end tell
    else if command is "LAST" then
        if state is not "viewing" then
            error "no active slideshow"
        end if
        tell application "Keynote"
            repeat with i from 1 to ((my knGetMaxSlide()) - 1)
                try
                    show next
                on error errMsg
                    exit repeat
                end try
                delay 0.02
            end repeat
        end tell
    else if command is "GOTO" then
        if state is not "viewing" then
            error "no active slideshow"
        end if
        tell application "Keynote"
            repeat with i from 1 to (my knGetMaxSlide())
                try
                    show previous
                on error errMsg
                    exit repeat
                end try
                delay 0.02
            end repeat
            repeat with i from 1 to ((arg as integer) - 1)
                try
                    show next
                end try
                delay 0.02
            end repeat
        end tell
    else if command is "PREV" then
        if state is not "viewing" then
            error "no active slideshow"
        end if
        tell application "Keynote"
            show previous
        end tell
    else if command is "NEXT" then
        if state is not "viewing" then
            error "no active slideshow"
        end if
        tell application "Keynote"
            show next
        end tell
    end if
    return "{ \"response\": \"OK\" }"
end cmdCTRL

--  main procedure
on run argv
    set cmd to item 1 of argv
    set arg to ""
    if count of argv is 2 then
        set arg to item 2 of argv
    end if
    try
        if cmd is "STAT" then
            set output to cmdSTATE()
        else if cmd is "INFO" then
            set output to cmdINFO()
        else if cmd is "BOOT" ¬
            or cmd is "QUIT" ¬
            or cmd is "OPEN" ¬
            or cmd is "CLOSE" ¬
            or cmd is "START" ¬
            or cmd is "STOP" ¬
            or cmd is "PAUSE" ¬
            or cmd is "RESUME" ¬
            or cmd is "FIRST" ¬
            or cmd is "LAST" ¬
            or cmd is "GOTO" ¬
            or cmd is "PREV" ¬
            or cmd is "NEXT" then
            set output to cmdCTRL(cmd, arg)
        else
            set output to "{ \"error\": \"invalid command\" }"
        end if
    on error errMsg
        set output to ("{ \"error\": \"" & errMsg & "\" }")
    end try
    copy output to stdout
end run

--
--  slideshow -- Observe and Control Slideshow Applications
--  Copyright (c) 2014-2015 Ralf S. Engelschall <http://engelschall.com>
--
--  This Source Code Form is subject to the terms of the Mozilla Public
--  License (MPL), version 2.0. If a copy of the MPL was not distributed
--  with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
--
--  File:     connector-osx-kn6.scpt
--  Purpose:  connector engine for Apple Keynote 6 under Mac OS X
--  Language: AppleScript
--

--  utility function
on filterText(this_text, allowed_chars)
   set new_text to ""
   repeat with this_char in this_text
       set x to the offset of this_char in allowed_chars
       if x is not 0 then
           set new_text to (new_text & this_char) as string
       end if
   end repeat
   return new_text
end filterText

--  utility function
on replaceText(this_text, search_string, replacement_string)
   set AppleScript's text item delimiters to the search_string
   set the item_list to every text item of this_text
   set AppleScript's text item delimiters to the replacement_string
   set this_text to the item_list as string
   set AppleScript's text item delimiters to ""
   return this_text
end replace_chars

--  utility function
on asciiCharset()
    set charset to " \"'!#$%()*+,-./:;<=>?@[\\]^_{|}~"
    set charset to (charset & "0123456789")
    set charset to (charset & "ABCDEFGHIJKLMNOPQRSTUVWXYZ")
    set charset to (charset & "abcdefghijklmnopqrstuvwxyz")
    return charset
end asciiCharset

--  get application state
on knGetState()
    set state to "closed"
    tell application "System Events"
        set is_running to (exists (some process whose name is "Keynote"))
    end tell
    if is_running then
       try
            set state to "running"
            tell application "Keynote"
                if false then -- FIXME: how to detect play mode?
                    set state to "viewing"
                else if (get count of slides of front document) > 0 then
                    set state to "editing"
                end if
            end tell
        on error errMsg
        end try
    end if
    return state
end knGetState

--  get Apple Keynote current slide
on knGetCurSlide()
    try
        tell application "Keynote"
            if false then -- FIXME: how to detect play mode?
                return (get slide number of current slide of front document) -- FIXME: wrong!
            else
                return (get slide number of current slide of front document)
            end if
        end tell
    on error errMsg
        return 0
    end try
end knGetCurSlide

--  get Apple Keynote maximum slide
on knGetMaxSlide()
    try
        tell application "Keynote"
            return (get count of slides of front document)
        end tell
    on error errMsg
        return 0
    end try
end knGetMaxSlide

--  the STATE command
on cmdSTATE()
    set state to knGetState()
    if state is "closed" then
        set position to 0
        set slides to 0
    else
        set position to knGetCurSlide()
        set slides to knGetMaxSlide()
    end if
    return ("{ \"response\": { " & ¬
        "\"state\": \"" & state & "\", " & ¬
        "\"position\": " & position & ", " & ¬
        "\"slides\": " & slides & " " & ¬
    "} }")
end cmdSTATE

--  the INFO command
on cmdINFO()
    set output to ""
    if knGetMaxSlide() is 0 then
        error "still no active presentation"
    end if
    tell application "Keynote"
        set thePresentation to front document
        set theTitles to ""
        set theNotes to ""
        set slideCount to (get count of slides of thePresentation)
        repeat with slideNum from 1 to slideCount
            set theSlide to slide slideNum of thePresentation

            set theTitle to (get object text of default title item of theSlide) as string
            if theTitles is not "" then
                set theTitles to (theTitles & ", ")
            end if
            set theTitles to (theTitles & "\"" & (my replaceText(theTitle, "\"", "\\\"")) & "\"")

            set theNote to (presenter notes of theSlide) as string
            set theNote to (my filterText(theNote, my asciiCharset()))
            if theNotes is not "" then
                set theNotes to (theNotes & ", ")
            end if
            set theNotes to (theNotes & "\"" & (my replaceText(theNote, "\"", "\\\"")) & "\"")

        end repeat
        set theTitles to ("[ " & theTitles & " ]")
        set theNotes to ("[ " & theNotes & " ]")
        set output to ("{ \"response\": { \"titles\": " & theTitles & ", \"notes\": " & theNotes & " } }")
    end tell
    return output
end cmdINFO

--  the control commands
on cmdCTRL(command, arg)
    set state to knGetState()
    if command is "BOOT" then
        if state is not "closed" then
            error "application already running"
        end if
        tell application "Keynote"
            activate
        end tell
    else if command is "QUIT" then
        if state is "closed" then
            error "application already closed"
        end if
        tell application "Keynote"
            quit
        end tell
    else if command is "OPEN" then
        if state is "editing" or state is "viewing" then
            error "active presentation already existing"
        end if
        tell application "Keynote"
            tell application "Finder" to set thePath to ¬
                POSIX file (POSIX path of (container of (path to me) as string) & (arg)) as alias
            open thePath
        end tell
    else if command is "CLOSE" then
        if state is "closed" or state is "running" then
            error "still no active presentation"
        end if
        tell application "Keynote"
            close front document
        end tell
    else if command is "START" then
        if state is "closed" or state is "running" then
            error "still no active presentation"
        end if
        if state is "viewing" then
            error "active presentation already viewing"
        end if
        tell application "Keynote"
            start front document from (slide 1 of front document)
        end tell
    else if command is "STOP" then
        if state is not "viewing" then
            error "no active slideshow"
        end if
        tell application "Keynote"
            stop front document
        end tell
    else if command is "PAUSE" then
        if state is not "viewing" then
            error "no active slideshow"
        end if
        tell application "Keynote"
            activate
            tell application "System Events" to keystroke "b"
        end tell
    else if command is "RESUME" then
        if state is not "viewing" then
            error "no active slideshow"
        end if
        tell application "Keynote"
            activate
            tell application "System Events" to keystroke "b"
        end tell
    else if command is "FIRST" then
        if state is not "viewing" then
            error "no active slideshow"
        end if
        tell application "Keynote"
            repeat with i from 1 to (my knGetMaxSlide())
                try
                    show previous
                on error errMsg
                    exit repeat
                end try
                delay 0.02
            end repeat
        end tell
    else if command is "LAST" then
        if state is not "viewing" then
            error "no active slideshow"
        end if
        tell application "Keynote"
            repeat with i from 1 to ((my knGetMaxSlide()) - 1)
                try
                    show next
                on error errMsg
                    exit repeat
                end try
                delay 0.02
            end repeat
        end tell
    else if command is "GOTO" then
        if state is not "viewing" then
            error "no active slideshow"
        end if
        tell application "Keynote"
            repeat with i from 1 to (my knGetMaxSlide())
                try
                    show previous
                on error errMsg
                    exit repeat
                end try
                delay 0.02
            end repeat
            repeat with i from 1 to ((arg as integer) - 1)
                try
                    show next
                end try
                delay 0.02
            end repeat
        end tell
    else if command is "PREV" then
        if state is not "viewing" then
            error "no active slideshow"
        end if
        tell application "Keynote"
            show previous
        end tell
    else if command is "NEXT" then
        if state is not "viewing" then
            error "no active slideshow"
        end if
        tell application "Keynote"
            show next
        end tell
    end if
    return "{ \"response\": \"OK\" }"
end cmdCTRL

--  main procedure
on run argv
    set cmd to item 1 of argv
    set arg to ""
    if count of argv is 2 then
        set arg to item 2 of argv
    end if
    try
        if cmd is "STAT" then
            set output to cmdSTATE()
        else if cmd is "INFO" then
            set output to cmdINFO()
        else if cmd is "BOOT" ¬
            or cmd is "QUIT" ¬
            or cmd is "OPEN" ¬
            or cmd is "CLOSE" ¬
            or cmd is "START" ¬
            or cmd is "STOP" ¬
            or cmd is "PAUSE" ¬
            or cmd is "RESUME" ¬
            or cmd is "FIRST" ¬
            or cmd is "LAST" ¬
            or cmd is "GOTO" ¬
            or cmd is "PREV" ¬
            or cmd is "NEXT" then
            set output to cmdCTRL(cmd, arg)
        else
            set output to "{ \"error\": \"invalid command\" }"
        end if
    on error errMsg
        set output to ("{ \"error\": \"" & errMsg & "\" }")
    end try
    copy output to stdout
end run

fix filename in comment

fix filename in comment


on newPresentation(themeName)
	tell application "Keynote"
		activate
		-- FIXME: Make this selectable?
		set targetWidth to 1024 -- 1440 <-- higher resolution
		set targetHeight to 768 -- 1080 <-- higher resolution

		set props to {document theme:theme (themeName as string), width:targetWidth, height:targetHeight}

		set thisDocument to make new document with properties props
		return id of thisDocument
	end tell
end newPresentation

on openPresentation(posixPath)
	tell application "Keynote"
		open posixPath
	end tell
end openPresentation

on savePresentation(docId, posixPath)
	tell application "Keynote"
		activate
        set theDocument to document id docId
		save theDocument in POSIX file posixPath
        delete theDocument
        open posixPath
	end tell
end savePresentation

on closePresentation(docId)
	tell application "Keynote"
		activate
        set theDocument to document id docId
		close theDocument
	end tell
end savePresentation

on createSlide(docId, masterSlideName)
	tell application "Keynote"
		tell document id docId
			set thisSlide to make new slide with properties {base slide:master slide masterSlideName}
		end tell
	end tell
end createSlide

on deleteAllSlides(docId)
	tell application "Keynote"
		tell document id docId to delete every slide
	end tell
end deleteAllSlides

on finalize(docId)
	tell application "Keynote"
		tell document id docId to delete slide 1
	end tell
end finalize

on themeMasters(docId)
	--	  tell application "Keynote" to get the name of every master slide of thisDocument
	tell application "Keynote"
		set names to the name of every master slide of document id docId
		return names
	end tell
end themeMasters

on addImage(docId, slideIndex, n, filepath)
  (*
	FIXME: Use slide ID to adress slide instead of "current slide"
  *)
  tell application "Keynote"
	tell slide slideIndex of document id docId
		-- TO REPLACE A PLACEHOLDER OR EXISTING IMAGE:
		set thisPlaceholderImageItem to image n
		-- change the value of the “file name” property of the image to be an HFS file reference to the replacement image file
	    set macPath to POSIX file filepath as Unicode text
		set file name of thisPlaceholderImageItem to ¬
			alias macPath
	end tell
  end tell
end addImage

on addTitle(docId, slideIndex, thisSlideTitle)
  (*
	FIXME: Use slide ID to adress slide instead of "current slide"
  *)
	tell application "Keynote"
    	tell slide slideIndex of document id docId
			set the object text of the default title item to thisSlideTitle
		end tell
	end tell
end addTitle

on addBody(docId, slideIndex, thisSlideBody)
  (*
	FIXME: Use slide ID to adress slide instead of "current slide"
  *)
	tell application "Keynote"
    	tell slide slideIndex of document id docId
            set the object text of the default body item to thisSlideBody
		end tell
	end tell
end addTitle

    (*
  	FIXME: Use slide ID to adress slide instead of "current slide"
    *)
on addPresenterNotes(docId, slideIndex, theNotes)
	tell application "Keynote"
    	tell slide slideIndex of document id docId
			set presenter notes to theNotes
		end tell
	end tell
end addPresenterNotes

on addText(docId, slideIndex, n, theText)
  (*
	FIXME: Use slide ID to adress slide instead of "current slide"
  *)
  tell application "Keynote"
  	tell slide slideIndex of document id docId
		set thisPlaceholderItem to text item n
		set object text of thisPlaceholderItem to theText
	end tell
  end tell
end addText

on newPresentation(themeName)
	tell application "Keynote"
		activate
		-- FIXME: Make this selectable?
		set targetWidth to 1024 -- 1440 <-- higher resolution
		set targetHeight to 768 -- 1080 <-- higher resolution

		set props to {document theme:theme (themeName as string), width:targetWidth, height:targetHeight}

		set thisDocument to make new document with properties props
		return id of thisDocument
	end tell
end newPresentation

on openPresentation(posixPath)
	tell application "Keynote"
		open posixPath
	end tell
end openPresentation

on savePresentation(docId, posixPath)
	tell application "Keynote"
		activate
        set theDocument to document id docId
		save theDocument in POSIX file posixPath
        delete theDocument
        open posixPath
	end tell
end savePresentation

on closePresentation(docId)
	tell application "Keynote"
		activate
        set theDocument to document id docId
		close theDocument
	end tell
end savePresentation

on createSlide(docId, masterSlideName)
	tell application "Keynote"
		tell document id docId
			set thisSlide to make new slide with properties {base slide:master slide masterSlideName}
		end tell
	end tell
end createSlide

on deleteAllSlides(docId)
	tell application "Keynote"
		tell document id docId to delete every slide
	end tell
end deleteAllSlides

on finalize(docId)
	tell application "Keynote"
		tell document id docId to delete slide 1
	end tell
end finalize

on themeMasters(docId)
	--	  tell application "Keynote" to get the name of every master slide of thisDocument
	tell application "Keynote"
		set names to the name of every master slide of document id docId
		return names
	end tell
end themeMasters

on addImage(docId, slideIndex, n, filepath)
  tell application "Keynote"
	tell slide slideIndex of document id docId
		-- TO REPLACE A PLACEHOLDER OR EXISTING IMAGE:
		set thisPlaceholderImageItem to image n
		-- change the value of the “file name” property of the image to be an HFS file reference to the replacement image file
	    set macPath to POSIX file filepath as Unicode text
		set file name of thisPlaceholderImageItem to ¬
			alias macPath
	end tell
  end tell
end addImage

on addTitle(docId, slideIndex, thisSlideTitle)
	tell application "Keynote"
    	tell slide slideIndex of document id docId
			set the object text of the default title item to thisSlideTitle
		end tell
	end tell
end addTitle

on addBody(docId, slideIndex, thisSlideBody)
	tell application "Keynote"
    	tell slide slideIndex of document id docId
            set the object text of the default body item to thisSlideBody
		end tell
	end tell
end addTitle

on addPresenterNotes(docId, slideIndex, theNotes)
	tell application "Keynote"
    	tell slide slideIndex of document id docId
			set presenter notes to theNotes
		end tell
	end tell
end addPresenterNotes

on addText(docId, slideIndex, n, theText)
  tell application "Keynote"
  	tell slide slideIndex of document id docId
		set thisPlaceholderItem to text item n
		set object text of thisPlaceholderItem to theText
	end tell
  end tell
end addText

Remove FIXME: comments

Remove FIXME: comments


on alfred_script(q)

    set AppleScript's text item delimiters to {" "}

    tell application "Google Chrome"

        if q is "" or q is "p" or q is "pp" or q is "play" or q is "pause" then
            set buttonClass to "play"
            set execCode to "$('." & buttonClass & "').first().click()"
        else if q is "next" or q is "skip" or q is "n" then
            set buttonClass to "next"
            set execCode to "$('." & buttonClass & "').first().click()"
        else
            set argList to every text item of q
            if item 1 of argList is "genre" then
                set execCode to "$('li.genre a')[" & item 2 of argList & "].click();"
            end if
        end if

        set seenPlayerTab to 0

        set thisWindowIndex to 1
        repeat with thisWindow in windows

            set thisTabIndex to 1
            repeat with thisTab in tabs of thisWindow

                if URL of thisTab starts with "https://www.focusatwill.com/" then
                    set seenPlayerTab to 1
                    exit repeat
                end if

                set thisTabIndex to thisTabIndex + 1

            end repeat

            if seenPlayerTab is greater than 0 then
                tell tab thisTabIndex of window thisWindowIndex
                    execute javascript execCode
                end tell
                exit repeat
            end if

            set thisWindowIndex to thisWindowIndex + 1

        end repeat

        if seenPlayerTab is 0 then
            set newTab to make new tab at end of tabs of window 1
            set URL of newTab to "https://www.focusatwill.com/music/#player"
            delay 5
            tell newTab
                execute javascript execCode
            end tell
        end if

    end tell
end alfred_script

on alfred_script(q)

    set AppleScript's text item delimiters to {" "}

    tell application "Google Chrome"

        if q is "" or q is "p" or q is "pp" or q is "play" or q is "pause" then
            set execCode to "document.getElementsByClassName('play')[0].click()"
        else if q is "next" or q is "skip" or q is "n" then
            set execCode to "document.getElementsByClassName('next')[0].click()"
        else
            set argList to every text item of q
            if item 1 of argList is "genre" then
                set execCode to "document.querySelectorAll('li.genre a')[" & item 2 of argList & "].click();"
            end if
        end if

        set seenPlayerTab to 0

        set thisWindowIndex to 1
        repeat with thisWindow in windows

            set thisTabIndex to 1
            repeat with thisTab in tabs of thisWindow

                if URL of thisTab starts with "https://www.focusatwill.com/" then
                    set seenPlayerTab to 1
                    exit repeat
                end if

                set thisTabIndex to thisTabIndex + 1

            end repeat

            if seenPlayerTab is greater than 0 then
                tell tab thisTabIndex of window thisWindowIndex
                    execute javascript execCode
                end tell
                exit repeat
            end if

            set thisWindowIndex to thisWindowIndex + 1

        end repeat

        if seenPlayerTab is 0 then
            set newTab to make new tab at end of tabs of window 1
            set URL of newTab to "https://www.focusatwill.com/music/#player"
            delay 5
            tell newTab
                execute javascript execCode
            end tell
        end if

    end tell
end alfred_script

Fix broken javascript calls

[#2]: Fix broken javascript calls

At some point, using $() seems to have broken in Chrome. Replaced those
calls with equivalent DOM calls in vanilla Javascript.


tell application "System Events" to tell process "iOS Simulator"
	activate
	set frontmost to true
	click menu item "Reset Content and Settings" of menu 0 of menu bar item "iOS Simulator" of menu bar 1
	click button "Reset" of window ""
  return
end tell

tell application "System Events" to tell process "iOS Simulator"
	activate
	set frontmost to true
	click menu item "Reset Content and Settings" of menu 0 of menu bar item "iOS Simulator" of menu bar 1
  delay 0.1
  tell application "System Events" to key code 36
	-- click button "Reset" of window ""
  return
end tell

Update reset sim script

Update reset sim script


property stdout : ""
on run
	set newline to ASCII character 10

	set stdout to stdout & "  shutdown Terminal ..." & newline
	try
		tell application "Terminal" to quit
	on error
		set stdout to stdout & "    Terminal was not running prior uninstallation" & newline
	end try

	set stdout to stdout & "  remove TotalTerminal.app ..." & newline
	try
		do shell script "sudo rm -rf \"/Applications/TotalTerminal.app\"" with administrator privileges
	on error
		set stdout to stdout & "    unable to remove /Applications/TotalTerminal.app" & newline
	end try

	set stdout to stdout & "  remove TotalTerminal.osax ..." & newline
	try
		do shell script "sudo rm -rf \"/Library/ScriptingAdditions/TotalTerminal.osax\"" with administrator privileges
	on error
		set stdout to stdout & "    unable to remove /Library/ScriptingAdditions/TotalTerminal.osax" & newline
	end try

	set stdout to stdout & "  relaunch Terminal ..." & newline
	try
		tell application "Terminal" to launch
	on error
		set stdout to stdout & "    failed to relaunch Terminal" & newline
	end try

	set stdout to stdout & "TotalTerminal uninstallation finished" & newline

	-- at this point Terminal should start cleanly and with no signs of TotalTerminal
	-- you may check Events/Replies tab to see if there were no issues with uninstallation

	stdout -- this is needed for platypus to display output in details window
end run

property stdout : ""
on run
	set newline to ASCII character 10

	set stdout to stdout & "  shutdown Terminal ..." & newline
	try
		tell application "Terminal" to quit
	on error
		set stdout to stdout & "    Terminal was not running prior uninstallation" & newline
	end try

	set stdout to stdout & "  shutdown TotalTerminalCrashWatcher ..." & newline
	try
		do shell script "killall -SIGINT TotalTerminalCrashWatcher" with administrator privileges
	on error
		set stdout to stdout & "    TotalTerminalCrashWatcher was not running" & newline
	end try

	set stdout to stdout & "  remove TotalTerminal.app ..." & newline
	try
		do shell script "sudo rm -rf \"/Applications/TotalTerminal.app\"" with administrator privileges
	on error
		set stdout to stdout & "    unable to remove /Applications/TotalTerminal.app" & newline
	end try

	set stdout to stdout & "  remove TotalTerminal.osax ..." & newline
	try
		do shell script "sudo rm -rf \"/Library/ScriptingAdditions/TotalTerminal.osax\"" with administrator privileges
	on error
		set stdout to stdout & "    unable to remove /Library/ScriptingAdditions/TotalTerminal.osax" & newline
	end try

	set stdout to stdout & "  relaunch Terminal ..." & newline
	try
		tell application "Terminal" to launch
	on error
		set stdout to stdout & "    failed to relaunch Terminal" & newline
	end try

	set stdout to stdout & "TotalTerminal uninstallation done" & newline

	-- at this point Terminal should start cleanly and with no signs of TotalTerminal
	-- you may check Events/Replies tab to see if there were no issues with uninstallation

	stdout -- this is needed for platypus to display output in details window
end run

add shutdown step for TotalTerminalCrashWatcher


-- 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 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

					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

Add generic iTunes search as an additional fallback


(*
  Script object to make calling a defined function easier while in the
  iTunes and System Events name space.
*)
script remoteSpeakerFinder
  -- Given a list of buttons, find the remote speakers button
  -- by finding the first button with a name that isn't in a
  -- rejection list.
  on findCorrectButton (in_buttons)
    set buttons_to_skip to {"Burn Disc"}

    repeat with a_button in in_buttons
      try -- some buttons don't have names
        set the_name to name of a_button
        if buttons_to_skip does not contain {the_name} then
          return the_name
        end if
      end try
    end repeat

    return 16 -- default response
  end findCorrectButton
end script

(*
  Tell iTunes to use the "Mobile" set of remote speakers.
*)
tell application "iTunes"
  set the_bwindow to (get first browser window)
  set the_state to (get minimized of the_bwindow)
  set visible of the_bwindow to true
  set minimized of the_bwindow to false
end tell

tell application "System Events"
  tell process "iTunes"

     set the_buttons to (get buttons of window 1)
     set the_speaker_button to (remoteSpeakerFinder's findCorrectButton(the_buttons))

     -- Switch to the speakers in my bedroom
     set frontmost to true
     click button the_speaker_button of window 1
     key code 115                -- Home Key (first speaker in list)
     key code 125                -- Down Arrow
     key code 125                -- Down Arrow
     key code 36                 -- Return Key

     -- Wait for iTunes to connect to the speakers
     delay 5
  end tell
end tell

tell application "iTunes"
  -- Return window to previous state
  set minimized of the_bwindow to the_state
end tell

(*
  Script object to make calling a defined function easier while in the
  iTunes and System Events name space.
*)
script remoteSpeakerFinder
	global the_bwindow, the_state

	-- Given a list of buttons, find the remote speakers button
	-- by finding the first button with a name that isn't in a
	-- rejection list.
	on findCorrectButton(in_buttons)
		set buttons_to_skip to {"Burn Disc"}

		repeat with a_button in in_buttons
			try -- some buttons don't have names
				set the_name to name of a_button
				if buttons_to_skip does not contain {the_name} then
					return the_name
				end if
			end try
		end repeat

		return 16 -- default response
	end findCorrectButton

	on setRemoteSpeakersTo(item_index)
		tell application "System Events"
			tell process "iTunes"

				set the_buttons to (get buttons of window 1)
				set the_speaker_button to (remoteSpeakerFinder's findCorrectButton(the_buttons))

				-- Switch to the speakers in my bedroom
				set frontmost to true
				click button the_speaker_button of window 1
				key code 115 -- Home Key (first speaker in list)

				repeat item_index times
					key code 125 -- Down Arrow
				end repeat

				key code 36 -- Return Key

				-- Wait for iTunes to connect to the speakers
				delay 5
			end tell
		end tell
	end setRemoteSpeakersTo

	on prepareWindow()
		tell application "iTunes"
			set the_bwindow to (get first browser window)
			set the_state to (get minimized of the_bwindow)
			set visible of the_bwindow to true
			set minimized of the_bwindow to false
		end tell
	end prepareWindow

	on resetWindow()
		tell application "iTunes"
			-- Return window to previous state
			set minimized of the_bwindow to the_state
		end tell
	end resetWindow
end script

on handle_string(s)
	remoteSpeakerFinder's prepareWindow()
	remoteSpeakerFinder's setRemoteSpeakersTo(s)
	remoteSpeakerFinder's resetWindow()
	set volume 1
	tell application "iTunes"
		play (get the first item of (get every track where database ID is 24759))
	end tell
end handle_string

on run
	remoteSpeakerFinder's prepareWindow()
	remoteSpeakerFinder's setRemoteSpeakersTo(2)
	remoteSpeakerFinder's resetWindow()
end run

Add a way to select which speakers to set


JsOsaDAS1.001.00bplist00Vscript_var app = Application("Unity");
app.activate();
delay(0.15);
sys = Application("System Events");
sys.keystroke("p", { using: "command down"});jscrޭ

convert textfile

convert textfile


JsOsaDAS1.001.00bplist00Vscripto4//  The MIT License (MIT)
//  Copyright  2017 Lex Tang, http://lexrus.com
//
//  https://github.com/lexrus/LTFinderButtons
//
//  Permission is hereby granted, free of charge, to any person obtaining a copy
//  of this software and associated documentation files (the  Software ), to deal
//  in the Software without restriction, including without limitation the rights
//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
//  copies of the Software, and to permit persons to whom the Software is
//  furnished to do so, subject to the following conditions:
//
//  The above copyright notice and this permission notice shall be included in
//  all copies or substantial portions of the Software.
//
//  THE SOFTWARE IS PROVIDED  AS IS , WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
//  THE SOFTWARE.

var app = Application.currentApplication()
app.includeStandardAdditions = true

ObjC.import("Foundation")

function urlPathToPosixPath(path) {
	return unescape(path)
	    .replace(/^\/?file[:]\/+/i, '/')
		.replace(/(")/g, '\\$1')
}

function getCurrentFinderPath() {
	var Finder = Application('Finder')
	var item = Finder.selection()[0]
	if (!item) {
		try {
			return urlPathToPosixPath(Finder.windows[0].target().url())
		} catch (e) {
			return urlPathToPosixPath(Finder.desktop().url())
		}
	}
	return urlPathToPosixPath(item.url())
}

var currentFolderPath = getCurrentFinderPath()
app.setTheClipboardTo(currentFolderPath)~jscrޭ

JsOsaDAS1.001.00bplist00VscriptoO//  The MIT License (MIT)
//  Copyright  2017 Lex Tang, http://lexrus.com
//
//  https://github.com/lexrus/LTFinderButtons
//
//  Permission is hereby granted, free of charge, to any person obtaining a copy
//  of this software and associated documentation files (the  Software ), to deal
//  in the Software without restriction, including without limitation the rights
//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
//  copies of the Software, and to permit persons to whom the Software is
//  furnished to do so, subject to the following conditions:
//
//  The above copyright notice and this permission notice shall be included in
//  all copies or substantial portions of the Software.
//
//  THE SOFTWARE IS PROVIDED  AS IS , WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
//  THE SOFTWARE.

var app = Application.currentApplication()
app.includeStandardAdditions = true

ObjC.import("Foundation")

function urlPathToPosixPath(path) {
	return unescape(path)
	    .replace(/^\/?file[:]\/+/i, '/')
		.replace(/(")/g, '\\$1')
}

function getCurrentFinderPath() {
	var Finder = Application('Finder')
	var item = Finder.selection()[0]
	if (!item) {
		try {
			return urlPathToPosixPath(Finder.windows[0].target().url())
		} catch (e) {
			return urlPathToPosixPath(Finder.desktop().url())
		}
	}
	return urlPathToPosixPath(item.url())
}

var currentFolderPath =decodeURIComponent(escape(getCurrentFinderPath()))
app.setTheClipboardTo(currentFolderPath)jscrޭ

support file name utf8 encoded

support file name utf8 encoded


-- core configuration --

-- configurable options --

-- limit number of songs to improve efficiency
property resultLimit : 15
-- 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:Workflow Data:")
property bundleId : "com.calebevans.playsong"
property workflowCacheFolder : (alfredWorkflowDataFolder & bundleId & ":") as text
property artworkDocsFolder : ((get path to library folder from user domain as text) & "Containers:com.apple.AMPArtworkAgent:Data:Documents")
property artworkImageFolder : (artworkDocsFolder & ":artwork:")
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"
-- 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

-- query path to artwork image file cached natively by Music.app in Catalina
on getSongArtworkPath(theSong)

	try

		if albumArtEnabled is false then return defaultIconName

		tell application "Music"

			-- get persistent ID of song and convert from hexadecimal to decimal (base-10)
			set hexSongId to persistent ID of theSong
			set decSongId to (do shell script "echo $((16#" & hexSongId & "))")

			-- retrieve filename of cached artwork
			set artworkName to (do shell script ("/usr/bin/sqlite3 " & (POSIX path of artworkDocsFolder) & "/artworkd.sqlite '" & ¬
			"select ZHASHSTRING, ZKIND from ZIMAGEINFO where Z_PK = (" & ¬
			"(select ZIMAGEINFO from ZSOURCEINFO where Z_PK = (" & ¬
			"select ZSOURCEINFO from ZDATABASEITEMINFO where ZPERSISTENTID = " & ¬
			decSongId & ")))' | " & ¬
			"awk '{split($0,a,\"|\"); print a[1] \"_sk_\" a[2] \"_cid_1.jpeg\"}'"))

			set artworkPath to (artworkImageFolder & artworkName)

		end tell

		return artworkPath

	on error errorMessage

		log errorMessage

	end try

end getNativeCachedArtworkPath

-- creates album artwork cache
on createWorkflowPlaylist()

	tell application "Music"

		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 "Music"

		if user playlist workflowPlaylistName exists then

			delete tracks of user playlist workflowPlaylistName

		end if

	end tell

end clearQueue

on queueSongs(theSongs)

	tell application "Music"

		repeat with theSong in theSongs

			duplicate theSong to user playlist workflowPlaylistName

		end repeat

	end tell

end queueSongs

on playQueue()

	tell application "Music"

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

			play user playlist workflowPlaylistName

		end if

	end tell

end playQueue

on getPlaylist(playlistId)

	tell application "Music"

		return (first playlist whose id is playlistId)

	end tell

end getPlaylist

on getPlaylistSongs(playlistId)

	tell application "Music"

		set thePlaylist to getPlaylist(playlistId) of me
		set playlistSongs to every track of thePlaylist

	end tell

	return playlistSongs

end getPlaylistSongs

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

	tell application "Music"

		set genreSongs to every track of playlist 2 whose genre is genreName
		set artistNames to {}

		repeat with theSong in genreSongs

			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 genreSongs to {}

	repeat with artistName in artistNames

		set genreSongs to genreSongs & getArtistSongs(artistName) of me

	end repeat

	return genreSongs

end getGenreSongs

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

	tell application "Music"

		set artistSongs to every track of playlist 2 whose artist is artistName
		set albumNames to {}

		repeat with theSong in artistSongs

			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 "Music"

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

		repeat with albumName in albumNames

			set albumSongs to (every track of playlist 2 whose artist is artistName and album is albumName)
			set artistSongs to artistSongs & albumSongs

		end repeat

	end tell

	return artistSongs

end getArtistSongs

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

	tell application "Music"

		set albumSongs to every track of playlist 2 whose album is albumName

	end tell

	return albumSongs

end getAlbumSongs

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

	tell application "Music"

		set theSong to first track of playlist 2 whose database ID is songId

	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)

			tell application \"Music\"

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

				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)

				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))

				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)

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 albumSongs to getAlbumSongs(albumName)
	queueSongs(albumSongs)

end queueAlbum

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

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

end queueArtist

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

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

end queueGenre

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

	set playlistSongs to getPlaylistSongs(playlistId)
	queueSongs(playlistSongs)

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 "Unsupported type: " & theType
	end if

end queue

on play(query)

	clearQueue()
	queue(query)
	playQueue()

end play

-- core configuration --

-- configurable options --

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

-- workflow parameters --

-- workflow folders
property libraryFolder : (path to library folder from user domain as text)
property cacheFolder : (libraryFolder & "Caches:")
property alfredWorkflowDataFolder : (cacheFolder & "com.runningwithcrayons.Alfred:Workflow Data:")
property bundleId : "com.calebevans.playsong"
property workflowCacheFolder : (alfredWorkflowDataFolder & bundleId & ":") as text

-- album artwork folders
property artworkDocsFolder : ((get path to library folder from user domain as text) & "Containers:com.apple.AMPArtworkAgent:Data:Documents")
property artworkImageFolder : (artworkDocsFolder & ":artwork:")

-- the default icon used for search results without album artwork
property defaultIconName : "resources/icon-noartwork.png"
-- the name of the playlist used by the workflow for playing songs
property workflowPlaylistName : "Alfred Play Song"
-- 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

-- query path to artwork image file cached natively by Music.app in Catalina
on getSongArtworkPath(theSong)

	try

		if albumArtEnabled is false then return defaultIconName

		tell application "Music"

			-- get persistent ID of song and convert from hexadecimal to decimal (base-10)
			set hexSongId to persistent ID of theSong
			set decSongId to (do shell script "echo $((16#" & hexSongId & "))")

			-- retrieve filename of cached artwork
			set artworkName to (do shell script ("/usr/bin/sqlite3 " & (POSIX path of artworkDocsFolder) & "/artworkd.sqlite '" & ¬
			"select ZHASHSTRING, ZKIND from ZIMAGEINFO where Z_PK = (" & ¬
			"(select ZIMAGEINFO from ZSOURCEINFO where Z_PK = (" & ¬
			"select ZSOURCEINFO from ZDATABASEITEMINFO where ZPERSISTENTID = " & ¬
			decSongId & ")))' | " & ¬
			"awk '{split($0,a,\"|\"); print a[1] \"_sk_\" a[2] \"_cid_1.jpeg\"}'"))

			set artworkPath to (artworkImageFolder & artworkName)

		end tell

		return artworkPath

	on error errorMessage

		log errorMessage

	end try

end getNativeCachedArtworkPath

-- creates album artwork cache
on createWorkflowPlaylist()

	tell application "Music"

		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 "Music"

		if user playlist workflowPlaylistName exists then

			delete tracks of user playlist workflowPlaylistName

		end if

	end tell

end clearQueue

on queueSongs(theSongs)

	tell application "Music"

		repeat with theSong in theSongs

			duplicate theSong to user playlist workflowPlaylistName

		end repeat

	end tell

end queueSongs

on playQueue()

	tell application "Music"

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

			play user playlist workflowPlaylistName

		end if

	end tell

end playQueue

on getPlaylist(playlistId)

	tell application "Music"

		return (first playlist whose id is playlistId)

	end tell

end getPlaylist

on getPlaylistSongs(playlistId)

	tell application "Music"

		set thePlaylist to getPlaylist(playlistId) of me
		set playlistSongs to every track of thePlaylist

	end tell

	return playlistSongs

end getPlaylistSongs

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

	tell application "Music"

		set genreSongs to every track of playlist 2 whose genre is genreName
		set artistNames to {}

		repeat with theSong in genreSongs

			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 genreSongs to {}

	repeat with artistName in artistNames

		set genreSongs to genreSongs & getArtistSongs(artistName) of me

	end repeat

	return genreSongs

end getGenreSongs

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

	tell application "Music"

		set artistSongs to every track of playlist 2 whose artist is artistName
		set albumNames to {}

		repeat with theSong in artistSongs

			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 "Music"

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

		repeat with albumName in albumNames

			set albumSongs to (every track of playlist 2 whose artist is artistName and album is albumName)
			set artistSongs to artistSongs & albumSongs

		end repeat

	end tell

	return artistSongs

end getArtistSongs

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

	tell application "Music"

		set albumSongs to every track of playlist 2 whose album is albumName

	end tell

	return albumSongs

end getAlbumSongs

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

	tell application "Music"

		set theSong to first track of playlist 2 whose database ID is songId

	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)

			tell application \"Music\"

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

				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)

				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))

				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)

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 albumSongs to getAlbumSongs(albumName)
	queueSongs(albumSongs)

end queueAlbum

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

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

end queueArtist

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

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

end queueGenre

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

	set playlistSongs to getPlaylistSongs(playlistId)
	queueSongs(playlistSongs)

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 "Unsupported type: " & theType
	end if

end queue

on play(query)

	clearQueue()
	queue(query)
	playQueue()

end play

Reorganize and clean up workflow parameters