Skip to content

Instantly share code, notes, and snippets.

@autumnjolitz
Created April 11, 2025 22:43
Show Gist options
  • Save autumnjolitz/208fc8c086836c5f561c18dd8b9cca40 to your computer and use it in GitHub Desktop.
Save autumnjolitz/208fc8c086836c5f561c18dd8b9cca40 to your computer and use it in GitHub Desktop.
Control a local OSX spotify client or the tv.lan spotify client (login to it over SSH)
#!/usr/bin/env sh
# use like . spotify-shell-osx.sh
# then call the funcs
set -euo pipefail
SHELL_RELAY_SOCKET=/tmp/relay.sock
function overlaps () {
local set1="${1:-}"
local set2="${2:-}"
local has_overlap=0
for item in $set1
do
if contains $item "${set2}"; then
echo "$item"
has_overlap=1
fi
done
if [ $has_overlap -eq 0 ]; then
return 1
fi
return 0
}
function select_by_index () {
local targetindex="${1}"
shift
set -- ${@:-}
local s="${@}"
local limit="$#"
local index=0
if [ $targetindex -ge $limit ]; then
>&2 echo 'IndexError: Index '"${targetindex}"' exceeds range of '"${limit}"
return 2
fi
for item in $s
do
if [ $index -eq $targetindex ]; then
echo "$item"
return 0
fi
index="$(expr "${index:-0}" '+' 1)"
done
return 1
}
function contains () {
local needle="${1}"
shift
local items="$(echo "${1}" | xargs)"
shift
local sep="${1:- }"
case "${items}" in
*"$sep$needle$sep"*|*"$sep$needle"|"$needle$sep"*)
return 0
;;
*)
return 1
;;
esac
}
SPOTIFY_APPLE_SCRIPT_ACTIVATE='
tell application "Spotify"
activate
end tell
'
SPOTIFY_APPLE_SCRIPT_START='
tell application "Spotify"
launch
end tell
'
SPOTIFY_APPLE_SCRIPT_START_IF_NOT_RUNNING='
if application "Spotify" is not running then
'"${SPOTIFY_APPLE_SCRIPT_START}"'
end if
'
SPOTIFY_APPLE_SCRIPT_TELL='
tell application "Spotify" to ${verb} ${arg}
'
# https://open.spotify.com/playlist/0weokFip9oT968ZYSsC6o2?si=26216acaa784481e
function spotify_play () {
spotify_do 'play' $@
}
function spotify_pause () {
case "$(spotify_do status)" in
paused)
spotify_do 'play'
return
;;
esac
spotify_do 'pause'
}
function spotify_do () {
local noun=''
local entity_id=''
local arg=''
local code=0
local activate=0
local verb="${1:-}"
shift
local url="${1:-}"
case "${verb:-}" in
play-track|play-url|'play url'|'play album'|'play playlist'|play-playlist)
if [ "x${url:-}" = 'x' ]; then
>&2 echo 'You must provide a spotify share url or uri!'
return 2
fi
verb='play track'
case "${verb}" in
*album)
noun=album
;;
*playlist)
noun=playlist
;;
*track)
noun=track
;;
esac
;;
start)
verb=activate
;;
exit)
verb=quit
;;
stop)
verb=pause
;;
status)
verb='player state'
url=
;;
play|pause|quit|activate|'play track')
;;
--raw)
shift
case "$url" in
--activate)
activate=1
url=''
;;
--*)
>&2 echo 'wtf is this option ('"$url"')?!'
return 75
;;
esac
verb="$url $@"
url=
;;
*)
echo 'Unknown verb ('"${verb}"')!'
return 1
;;
esac
if [ "x${url}" != 'x' ]; then
case "$url" in
https://open.spotify.com/*)
# reduce to noun/id
url="$(echo "${url}" | cut -d/ -f4,5)"
# remove any tracking junk...
url="$(echo "${url}" | sed 's/[\?].*$//')"
noun="$(echo "${url}" | cut -d/ -f1)"
entity_id="$(echo "${url}" | cut -d/ -f2)"
;;
spotify:*)
url="$(echo "${url}" | cut -d: -f2,3)"
# remove any tracking junk...
url="$(echo "${url}" | sed 's/[\?].*$//')"
noun="$(echo "${url}" | cut -d: -f1)"
entity_id="$(echo "${url}" | cut -d: -f2)"
;;
*)
>&2 echo "Unknown url: '${url}'"'!'
return 2
;;
esac
if ! [ "x${entity_id}" != 'x' -a "x${noun}" != 'x' ]; then
>&2 echo 'invalid url: Parsed url='"${url}"' to noun='"${noun}"' and entity_id='"${entity_id}"
return 4
fi
verb='play track'
arg='"'"spotify:${noun}:${entity_id}"'"'
fi
local use_relay=false
local current_uid="$(id -u)"
local pre_exec_fn=''
local TMPDIR='/tmp'
local script_filename="/tmp/$(basename "$(mktemp)")"
trap "rm -f '$script_filename'" RETURN
export verb arg
>>"$script_filename" echo "$SPOTIFY_APPLE_SCRIPT_START_IF_NOT_RUNNING"
if [ $activate -eq 1 ]; then
>>"$script_filename" echo "$SPOTIFY_APPLE_SCRIPT_ACTIVATE"
fi
>>"$script_filename" echo "$SPOTIFY_APPLE_SCRIPT_TELL"
for name in verb arg
do
sed -i '' 's|${'"${name}"'}|'"$(eval echo "\$${name}")"'|g' "$script_filename"
done
if [ "x${SPOTIFY_DEBUG:-}" = 'x1' ]; then
>&2 echo 'spotify-shell-osx[debug] script body:'
>&2 cat "$script_filename"
>&2 echo 'spotify-shell-osx[debug] script end'
fi
if [ -e "${SHELL_RELAY_SOCKET}" ]; then
use_relay=true
local relay_socket_permissions=770
local script_permissions=540
local socket_user="$(echo "$(set -- $( ls -l "${SHELL_RELAY_SOCKET}" ) ; echo $3)")"
local socket_uid="$(id -u "${socket_user}")"
if [ "${current_uid}" != "${socket_uid}" ]; then
pre_exec_fn="sudo -u ${socket_user} --"
fi
local socket_group="$(echo "$(set -- $( ls -l "${SHELL_RELAY_SOCKET}" ) ; echo $4)")"
if ! contains "${socket_group}" "$(groups)" ; then
local common_group="$(select_by_index 0 "$(overlaps "$(groups)" "$(groups tv)" )")"
if [ "x$common_group" != 'x' ]; then
eval $pre_exec_fn chgrp "${common_group}" "${SHELL_RELAY_SOCKET}"
socket_group="${common_group}"
else
relay_socket_permissions=777
script_permissions=544
fi
fi
if [ "x${SPOTIFY_DEBUG:-}" = 'x1' ]; then
>&2 echo 'Setting group of '"$script_filename"' -> '"${socket_group}"'!'
fi
chgrp "${socket_group}" "${script_filename}"
# ensure the relay socket permissions are least-priv'ed
eval $pre_exec_fn chmod $relay_socket_permissions "${SHELL_RELAY_SOCKET}"
local target=
local line=
local output_f=$(mktemp)
trap "rm -f '$script_filename' '$output_f'" RETURN
local seen_hello=0
if [ "x${SPOTIFY_DEBUG:-}" = 'x1' ]; then
>&2 echo 'sending script_filename='"${script_filename}"' to '"${SHELL_RELAY_SOCKET}"'...'
fi
#
# set the permissions so the other side can read this script
if [ "x${SPOTIFY_DEBUG:-}" = 'x1' ]; then
>&2 echo 'Setting '"$script_filename"' -> '"${script_permissions}"'!'
fi
chmod "$script_permissions" "$script_filename"
>"$output_f" 2>&1 PYTHONUNBUFFERED=1 python3.7 /usr/local/bin/shellrelay.py "${SHELL_RELAY_SOCKET}" send osascript "$script_filename"
while IFS="" read -r line || [ -n "$line" ]
do
if [ "x${SPOTIFY_DEBUG:-}" = 'x1' ]; then
>&2 printf 'raw: %s\n' "$line"
fi
case "$line" in
"b'Hello'")
if [ $seen_hello -eq 0 ]; then
if [ "x${SPOTIFY_DEBUG:-}" = 'x1' ]; then
>&2 echo 'debug: found hello'
fi
seen_hello=1
continue
fi
;;
'stdout:'*)
target=stdout
line="$(echo "$line" | sed 's|^stdout\: ||g' )"
case "${line}" in
'stderr:'*)
line="$(echo "$line" | sed 's|^stderr\: ||g' )"
target=stderr
;;
esac
;;
'stderr:'*)
target=stderr
line="$(echo "$line" | sed 's|^stderr\: ||g' )"
case "${line}" in
'stdout:'*)
line="$(echo "$line" | sed 's|^stdout\: ||g' )"
target=stdout
;;
esac
;;
'exit:'*)
code="$(echo "$line" | cut -d: -f2)"
return "${code:-24}"
;;
*)
if [ "x${SPOTIFY_DEBUG:-}" = 'x1' ]; then
2>&1 echo 'skipping: '"${line}"
fi
;;
esac
if [ "x${line}" != 'x' ]; then
case "$target" in
stdout)
printf '%s\n' "$line"
;;
stderr)
>&2 printf '%s\n' "$line"
;;
esac
fi
done < "$output_f"
else
osascript "$script_filename" || code=$?
return $code
fi
}
if [[ "$(basename -- "$0")" == "spotify-shell-osx.sh" ]]; then
case "${1:-}" in
"")
>&2 echo 'spotify-shell-osx [verb] [arg1]'
exit 254
;;
spotify_*)
eval "$@"
;;
*)
spotify_do $@
;;
esac
fi
@autumnjolitz
Copy link
Author

Either chmod +x spotify-shell-osx.sh and use it as a normal command or add to your $HOME/.bash_profile:

. spotify-shell-osx.sh

then you can call spotify_do, spotify_play, spotify_pause as shell commands

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment