Created
April 11, 2025 22:43
-
-
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)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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 | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Either
chmod +x spotify-shell-osx.sh
and use it as a normal command or add to your$HOME/.bash_profile
:then you can call
spotify_do
,spotify_play
,spotify_pause
as shell commands