From 7e02b9735e0e1610b486cc37fc83a7fc66c8f54b Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 8 Dec 2017 10:03:23 +0100 Subject: [PATCH] Added a bunch of features as well as a non-JQ version for more details see http://blog.loetzimmer.de/2017/10/amazon-alexa-hort-auf-die-shell-echo.html --- alexa_remote_control.sh | 737 ++++++++++++++++++++++++++-------- alexa_remote_control_plain.sh | 670 +++++++++++++++++++++++++++++++ 2 files changed, 1241 insertions(+), 166 deletions(-) create mode 100644 alexa_remote_control_plain.sh diff --git a/alexa_remote_control.sh b/alexa_remote_control.sh index 084a63e..9c975ff 100644 --- a/alexa_remote_control.sh +++ b/alexa_remote_control.sh @@ -7,6 +7,13 @@ # 2017-10-11: v0.2 TuneIn Station Search # 2017-10-11: v0.2a commands on special device "ALL" are executed on all ECHO+WHA # 2017-10-16: v0.3 added playback of library tracks +# 2017-10-24: v0.4 added playback information +# 2017-11-21: v0.5 added Prime station and playlist +# 2017-11-22: v0.6 added Prime historical queue and replaced getopts +# 2017-11-25: v0.6a cURL is now configurable +# 2017-11-25: v0.7 added multiroom create/delete, playback of library playlist +# 2017-11-30: v0.7a added US config, fixed device names containing spaces +# 2017-12-07: v0.7b added Bluetooth connect/disconnect # ### # @@ -17,105 +24,252 @@ # ########################################## -EMAIL="amazon_account@email.address" -PASSWORD="Very_Secret_Amazon_Account_Password" +EMAIL='amazon_account@email.address' +PASSWORD='Very_Secret_Amazon_Account_Password' + +LANGUAGE="de,en" +#LANGUAGE="en-us" + +AMAZON='amazon.de' +#AMAZON='amazon.com' + +ALEXA='layla.amazon.de' +#ALEXA='pitangui.amazon.com' + +# cURL binary +CURL='/usr/bin/curl' + +# cURL options +# -k : if your cURL cannot verify CA certificates, you'll have to trust any +# --compressed : if your cURL was compiled with libz you may use compression +OPTS='--compressed' +#OPTS='-k --compressed' ########################################### # nothing to configure below here # -COOKIE="/tmp/.alexa.cookie" -DEVLIST="/tmp/.alexa.devicelist.json" -TMP="/tmp/" +TMP="/tmp" +COOKIE="${TMP}/.alexa.cookie" +DEVLIST="${TMP}/.alexa.devicelist.json" -GUIVERSION=1.24.2698.0 +GUIVERSION=0 -OPTIND=1 LIST="" LOGOFF="" COMMAND="" STATIONID="" +QUEUE="" SONG="" -while getopts "h?lad:e:r:s:" opt; do - case "$opt" in - s) - SONG=$OPTARG - ;; - e) - COMMAND=$OPTARG - ;; - r) - STATIONID=$OPTARG - # stationIDs are "s1234" or "s12345" - if [ -n "${STATIONID##s[0-9][0-9][0-9][0-9]}" -a -n "${STATIONID##s[0-9][0-9][0-9][0-9][0-9]}" ] ; then - # search for station name - STATIONID=$(curl -s --data-urlencode "query=${STATIONID}" -G "https://api.tunein.com/profiles?fullTextSearch=true" | jq -r '.Items[] | select(.ContainerType == "Stations") | .Children[] | select( .Index==1 ) | .GuideId') - if [ -z "$STATIONID" ] ; then - echo "ERROR: no Station \"$OPTARG\" found on TuneIn" - exit 1 - fi - fi - ;; - d) - DEVICE=$OPTARG - ;; - l) - LOGOFF="true" - ;; - a) - LIST="true" - ;; - *) - echo "$0 [-d |ALL] -e > | -r <\"station name\"|stationid> | -s | -a | -l | -h" - echo " -e : run command" - echo " -r : play tunein radio" - echo " -s : play library track" - echo " -a : list available devices" - echo " -l : logoff" - echo " -h : help" - exit 0 - ;; - esac +TYPE="" +ASIN="" +SEEDID="" +HIST="" +LEMUR="" +CHILD="" +PLIST="" +BLUETOOTH="" + +usage() +{ + echo "$0 [-d |ALL] -e > | -b [<\"AA:BB:CC:DD:EE:FF\">] | -q | -r <\"station name\"|stationid> | -s | -t |" + echo " -u | -v | -w | -i | -p | -P | -S | -a | -m [device_1 .. device_X] | -l | -h" + echo " -e : run command" + echo " -b : connect/disconnect bluetooth device" + echo " -q : query queue" + echo " -r : play tunein radio" + echo " -s : play library track" + echo " -t : play Prime playlist" + echo " -u : play Prime station" + echo " -v : play Prime historical queue" + echo " -w : play library playlist" + echo " -i : list imported library tracks" + echo " -p : list purchased library tracks" + echo " -P : list Prime playlists" + echo " -S : list Prime stations" + echo " -a : list available devices" + echo " -m : delete multiroom and/or create new multiroom containing devices" + echo " -l : logoff" + echo " -h : help" +} + +while [ "$#" -gt 0 ] ; do + case "$1" in + -d) + if [ "${2#-}" != "${2}" -o -z "$2" ] ; then + echo "ERROR: missing argument for ${1}" + usage + exit 1 + fi + DEVICE=$2 + shift + ;; + -e) + if [ "${2#-}" != "${2}" -o -z "$2" ] ; then + echo "ERROR: missing argument for ${1}" + usage + exit 1 + fi + COMMAND=$2 + shift + ;; + -b) + if [ "${2#-}" = "${2}" -a -n "$2" ] ; then + BLUETOOTH=$2 + shift + else + BLUETOOTH="null" + fi + ;; + -m) + if [ "${2#-}" != "${2}" -o -z "$2" ] ; then + echo "ERROR: missing argument for ${1}" + usage + exit 1 + fi + LEMUR=$2 + shift + while [ "${2#-}" = "${2}" -a -n "$2" ] ; do + CHILD="${CHILD} ${2}" + shift + done + ;; + -r) + if [ "${2#-}" != "${2}" -o -z "$2" ] ; then + echo "ERROR: missing argument for ${1}" + usage + exit 1 + fi + STATIONID=$2 + shift + # stationIDs are "s1234" or "s12345" + if [ -n "${STATIONID##s[0-9][0-9][0-9][0-9]}" -a -n "${STATIONID##s[0-9][0-9][0-9][0-9][0-9]}" ] ; then + # search for station name + STATIONID=$(${CURL} ${OPTS} -s --data-urlencode "query=${STATIONID}" -G "https://api.tunein.com/profiles?fullTextSearch=true" | jq -r '.Items[] | select(.ContainerType == "Stations") | .Children[] | select( .Index==1 ) | .GuideId') + if [ -z "$STATIONID" ] ; then + echo "ERROR: no Station \"$2\" found on TuneIn" + exit 1 + fi + fi + ;; + -s) + if [ "${2#-}" != "${2}" -o -z "$2" ] ; then + echo "ERROR: missing argument for ${1}" + usage + exit 1 + fi + SONG=$2 + shift + ;; + -t) + if [ "${2#-}" != "${2}" -o -z "$2" ] ; then + echo "ERROR: missing argument for ${1}" + usage + exit 1 + fi + ASIN=$2 + shift + ;; + -u) + if [ "${2#-}" != "${2}" -o -z "$2" ] ; then + echo "ERROR: missing argument for ${1}" + usage + exit 1 + fi + SEEDID=$2 + shift + ;; + -v) + if [ "${2#-}" != "${2}" -o -z "$2" ] ; then + echo "ERROR: missing argument for ${1}" + usage + exit 1 + fi + HIST=$2 + shift + ;; + -w) + if [ "${2#-}" != "${2}" -o -z "$2" ] ; then + echo "ERROR: missing argument for ${1}" + usage + exit 1 + fi + PLIST=$2 + shift + ;; + -l) + LOGOFF="true" + ;; + -a) + LIST="true" + ;; + -i) + TYPE="IMPORTED" + ;; + -p) + TYPE="PURCHASES" + ;; + -P) + PRIME="prime-playlist-browse-nodes" + ;; + -S) + PRIME="prime-sections" + ;; + -q) + QUEUE="true" + ;; + -h|-\?|--help) + usage + exit 0 + ;; + *) + echo "ERROR: unknown option ${1}" + usage + exit 1 + ;; + esac + shift done case "$COMMAND" in - pause) - COMMAND='{"type":"PauseCommand"}' - ;; - play) - COMMAND='{"type":"PlayCommand"}' - ;; - next) - COMMAND='{"type":"NextCommand"}' - ;; - prev) - COMMAND='{"type":"PreviousCommand"}' - ;; - fwd) - COMMAND='{"type":"ForwardCommand"}' - ;; - rwd) - COMMAND='{"type":"RewindCommand"}' - ;; - shuffle) - COMMAND='{"type":"ShuffleCommand","shuffle":"true"}' - ;; - vol:*) - VOL=${COMMAND##*:} - # volume as integer! - if [ $VOL -le 100 -a $VOL -ge 0 ] ; then - COMMAND='{"type":"VolumeLevelCommand","volumeLevel":'${VOL}'}' - else - echo "ERROR: volume should be an integer between 0 and 100" - exit 1 - fi - ;; - "") - ;; - *) - echo "ERROR: unknown command \"${COMMAND}\"!" - echo " valid commands are pause|play|next|prev|fwd|rwd|shuffle|vol:<0-100>" - exit 1 - ;; + pause) + COMMAND='{"type":"PauseCommand"}' + ;; + play) + COMMAND='{"type":"PlayCommand"}' + ;; + next) + COMMAND='{"type":"NextCommand"}' + ;; + prev) + COMMAND='{"type":"PreviousCommand"}' + ;; + fwd) + COMMAND='{"type":"ForwardCommand"}' + ;; + rwd) + COMMAND='{"type":"RewindCommand"}' + ;; + shuffle) + COMMAND='{"type":"ShuffleCommand","shuffle":"true"}' + ;; + vol:*) + VOL=${COMMAND##*:} + # volume as integer! + if [ $VOL -le 100 -a $VOL -ge 0 ] ; then + COMMAND='{"type":"VolumeLevelCommand","volumeLevel":'${VOL}'}' + else + echo "ERROR: volume should be an integer between 0 and 100" + usage + exit 1 + fi + ;; + "") + ;; + *) + echo "ERROR: unknown command \"${COMMAND}\"!" + usage + exit 1 + ;; esac # @@ -126,51 +280,55 @@ log_in() ################################################################ # # following headers are required: -# Accept-Language (possibly for determining login region) -# User-Agent (CURL wouldn't store cookies without) +# Accept-Language (possibly for determining login region) +# User-Agent (cURL wouldn't store cookies without) # ################################################################ rm -f ${DEVLIST} rm -f ${COOKIE} +rm -f ${TMP}/.alexa.*.list # # get first cookie and write redirection target into referer # -curl -s -D "${TMP}.alexa.header" -c ${COOKIE} -b ${COOKIE} -A "Mozilla/5.0" -H "Accept-Language: de,en" --compressed -H "DNT: 1" -H "Connection: keep-alive" -H "Upgrade-Insecure-Requests: 1" -L\ - https://alexa.amazon.de | grep "hidden" | sed 's/hidden/\n/g' | grep "value=\"" | sed -E 's/^.*name="([^"]+)".*value="([^"]+)".*/\1=\2\&/g' > "${TMP}.alexa.postdata" +${CURL} ${OPTS} -s -D "${TMP}/.alexa.header" -c ${COOKIE} -b ${COOKIE} -A "Mozilla/5.0" -H "Accept-Language: ${LANGUAGE}" -H "DNT: 1" -H "Connection: keep-alive" -H "Upgrade-Insecure-Requests: 1" -L\ + https://alexa.${AMAZON} | grep "hidden" | sed 's/hidden/\n/g' | grep "value=\"" | sed -r 's/^.*name="([^"]+)".*value="([^"]+)".*/\1=\2\&/g' > "${TMP}/.alexa.postdata" # # login empty to generate sessiion # -curl -s -c ${COOKIE} -b ${COOKIE} -A "Mozilla/5.0" -H "Accept-Language: de,en" --compressed -H "DNT: 1" -H "Connection: keep-alive" -H "Upgrade-Insecure-Requests: 1" -L\ - -H "$(grep 'Location: ' ${TMP}.alexa.header | sed 's/Location: /Referer: /')" -d "@${TMP}.alexa.postdata" https://www.amazon.de/ap/signin | grep "hidden" | sed 's/hidden/\n/g' | grep "value=\"" | sed -E 's/^.*name="([^"]+)".*value="([^"]+)".*/\1=\2\&/g' > "${TMP}.alexa.postdata2" +${CURL} ${OPTS} -s -c ${COOKIE} -b ${COOKIE} -A "Mozilla/5.0" -H "Accept-Language: ${LANGUAGE}" -H "DNT: 1" -H "Connection: keep-alive" -H "Upgrade-Insecure-Requests: 1" -L\ + -H "$(grep 'Location: ' ${TMP}/.alexa.header | sed 's/Location: /Referer: /')" -d "@${TMP}/.alexa.postdata" https://www.${AMAZON}/ap/signin | grep "hidden" | sed 's/hidden/\n/g' | grep "value=\"" | sed -r 's/^.*name="([^"]+)".*value="([^"]+)".*/\1=\2\&/g' > "${TMP}/.alexa.postdata2" # # login with filled out form # !!! referer now contains session in URL # -curl -s -c ${COOKIE} -b ${COOKIE} -A "Mozilla/5.0" -H "Accept-Language: de,en" --compressed -H "DNT: 1" -H "Connection: keep-alive" -H "Upgrade-Insecure-Requests: 1" -L\ - -H "Referer: https://www.amazon.de/ap/signin/$(awk '$0 ~/.amazon.de.*session-id[\s\t]/ {print $7}' ${COOKIE})" --data-urlencode "email=${EMAIL}" --data-urlencode "password=${PASSWORD}" -d "@${TMP}.alexa.postdata2" https://www.amazon.de/ap/signin > /dev/null +${CURL} ${OPTS} -s -c ${COOKIE} -b ${COOKIE} -A "Mozilla/5.0" -H "Accept-Language: ${LANGUAGE}" -H "DNT: 1" -H "Connection: keep-alive" -H "Upgrade-Insecure-Requests: 1" -L\ + -H "Referer: https://www.${AMAZON}/ap/signin/$(awk "\$0 ~/.${AMAZON}.*session-id[\\s\\t]/ {print \$7}" ${COOKIE})" --data-urlencode "email=${EMAIL}" --data-urlencode "password=${PASSWORD}" -d "@${TMP}/.alexa.postdata2" https://www.${AMAZON}/ap/signin > /dev/null # # get CSRF # -curl -s -c ${COOKIE} -b ${COOKIE} -A "Mozilla/5.0" --compressed -H "DNT: 1" -H "Connection: keep-alive" -L\ - -H "Referer: https://alexa.amazon.de/spa/index.html" -H "Origin: https://alexa.amazon.de"\ - https://layla.amazon.de/api/language > /dev/null +${CURL} ${OPTS} -s -c ${COOKIE} -b ${COOKIE} -A "Mozilla/5.0" -H "DNT: 1" -H "Connection: keep-alive" -L\ + -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ + https://${ALEXA}/api/language > /dev/null + +rm -f "${TMP}/.alexa.header" +rm -f "${TMP}/.alexa.postdata" +rm -f "${TMP}/.alexa.postdata2" +} # # get JSON device list # -curl -s -b ${COOKIE} -A "Mozilla/5.0" --compressed -H "DNT: 1" -H "Connection: keep-alive" -L\ - -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.amazon.de/spa/index.html" -H "Origin: https://alexa.amazon.de"\ +get_devlist() +{ +${CURL} ${OPTS} -s -b ${COOKIE} -A "Mozilla/5.0" -H "DNT: 1" -H "Connection: keep-alive" -L\ + -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ -H "csrf: $(awk '$0 ~/.amazon.de.*csrf[\s\t]/ {print $7}' ${COOKIE})"\ - https://layla.amazon.de/api/devices-v2/device > ${DEVLIST} - -rm -f "${TMP}.alexa.header" -rm -f "${TMP}.alexa.postdata" -rm -f "${TMP}.alexa.postdata2" + "https://${ALEXA}/api/devices-v2/device?cached=false" > ${DEVLIST} } check_status() @@ -179,13 +337,13 @@ check_status() # bootstrap with GUI-Version writes GUI version to cookie # returns among other the current authentication state # - AUTHSTATUS=$(curl -s -c ${COOKIE} -b ${COOKIE} -A "Mozilla/5.0" --compressed -H "DNT: 1" -H "Connection: keep-alive" -L https://layla.amazon.de/api/bootstrap?version=${GUIVERSION} | sed -E 's/^.*"authenticated":([^,]+),.*$/\1/g') + AUTHSTATUS=$(${CURL} ${OPTS} -s -c ${COOKIE} -b ${COOKIE} -A "Mozilla/5.0" -H "DNT: 1" -H "Connection: keep-alive" -L https://${ALEXA}/api/bootstrap?version=${GUIVERSION} | sed -r 's/^.*"authenticated":([^,]+),.*$/\1/g') - if [ "$AUTHSTATUS" = "true" ] ; then - return 1 - fi + if [ "$AUTHSTATUS" = "true" ] ; then + return 1 + fi - return 0 + return 0 } # @@ -193,21 +351,23 @@ check_status() # set_var() { - if [ -z "${DEVICE}" ] ; then - # if no device was supplied, use the first Echo(dot) in device list - echo "setting default device to:" - DEVICE=$(jq -r '[ .devices[] | select(.deviceFamily == "ECHO" ) | .accountName] | .[0]' ${DEVLIST}) - echo ${DEVICE} - fi + DEVICE=$(echo ${DEVICE} | sed -r 's/%20/ /g') + + if [ -z "${DEVICE}" ] ; then + # if no device was supplied, use the first Echo(dot) in device list + echo "setting default device to:" + DEVICE=$(jq -r '[ .devices[] | select(.deviceFamily == "ECHO" ) | .accountName] | .[0]' ${DEVLIST}) + echo ${DEVICE} + fi - DEVICETYPE=$(jq --arg device ${DEVICE} -r '.devices[] | select(.accountName == $device) | .deviceType' ${DEVLIST}) - DEVICESERIALNUMBER=$(jq --arg device ${DEVICE} -r '.devices[] | select(.accountName == $device) | .serialNumber' ${DEVLIST}) - MEDIAOWNERCUSTOMERID=$(jq --arg device ${DEVICE} -r '.devices[] | select(.accountName == $device) | .deviceOwnerCustomerId' ${DEVLIST}) + DEVICETYPE=$(jq --arg device "${DEVICE}" -r '.devices[] | select(.accountName == $device) | .deviceType' ${DEVLIST}) + DEVICESERIALNUMBER=$(jq --arg device "${DEVICE}" -r '.devices[] | select(.accountName == $device) | .serialNumber' ${DEVLIST}) + MEDIAOWNERCUSTOMERID=$(jq --arg device "${DEVICE}" -r '.devices[] | select(.accountName == $device) | .deviceOwnerCustomerId' ${DEVLIST}) - if [ -z "${DEVICESERIALNUMBER}" ] ; then - echo "ERROR: unkown device dev:${DEVICE}" - exit 1 - fi + if [ -z "${DEVICESERIALNUMBER}" ] ; then + echo "ERROR: unkown device dev:${DEVICE}" + exit 1 + fi } # @@ -215,7 +375,7 @@ set_var() # list_devices() { - jq -r '.devices[].accountName' ${DEVLIST} + jq -r '.devices[].accountName' ${DEVLIST} } # @@ -223,10 +383,10 @@ list_devices() # run_cmd() { -curl -s -b ${COOKIE} -A "Mozilla/5.0" --compressed -H "DNT: 1" -H "Connection: keep-alive" -L\ - -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.amazon.de/spa/index.html" -H "Origin: https://alexa.amazon.de"\ +${CURL} ${OPTS} -s -b ${COOKIE} -A "Mozilla/5.0" -H "DNT: 1" -H "Connection: keep-alive" -L\ + -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ -H "csrf: $(awk '$0 ~/.amazon.de.*csrf[\s\t]/ {print $7}' ${COOKIE})" -X POST -d ${COMMAND}\ - "https://layla.amazon.de/api/np/command?deviceSerialNumber=${DEVICESERIALNUMBER}&deviceType=${DEVICETYPE}" > /dev/null + "https://${ALEXA}/api/np/command?deviceSerialNumber=${DEVICESERIALNUMBER}&deviceType=${DEVICETYPE}" } # @@ -234,10 +394,10 @@ curl -s -b ${COOKIE} -A "Mozilla/5.0" --compressed -H "DNT: 1" -H "Connection: k # play_radio() { -curl -s -b ${COOKIE} -A "Mozilla/5.0" --compressed -H "DNT: 1" -H "Connection: keep-alive" -L\ - -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.amazon.de/spa/index.html" -H "Origin: https://alexa.amazon.de"\ +${CURL} ${OPTS} -s -b ${COOKIE} -A "Mozilla/5.0" -H "DNT: 1" -H "Connection: keep-alive" -L\ + -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ -H "csrf: $(awk '$0 ~/.amazon.de.*csrf[\s\t]/ {print $7}' ${COOKIE})" -X POST\ - "https://layla.amazon.de/api/tunein/queue-and-play?deviceSerialNumber=${DEVICESERIALNUMBER}&deviceType=${DEVICETYPE}&guideId=${STATIONID}&contentType=station&callSign=&mediaOwnerCustomerId=${MEDIAOWNERCUSTOMERID}" > /dev/null + "https://${ALEXA}/api/tunein/queue-and-play?deviceSerialNumber=${DEVICESERIALNUMBER}&deviceType=${DEVICETYPE}&guideId=${STATIONID}&contentType=station&callSign=&mediaOwnerCustomerId=${MEDIAOWNERCUSTOMERID}" } # @@ -245,72 +405,317 @@ curl -s -b ${COOKIE} -A "Mozilla/5.0" --compressed -H "DNT: 1" -H "Connection: k # play_song() { -curl -s -b ${COOKIE} -A "Mozilla/5.0" --compressed -H "DNT: 1" -H "Connection: keep-alive" -L\ - -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.amazon.de/spa/index.html" -H "Origin: https://alexa.amazon.de"\ - -H "csrf: $(awk '$0 ~/.amazon.de.*csrf[\s\t]/ {print $7}' ${COOKIE})" -X POST -d "{\"trackId\":\"${SONG}\",\"isoTimestamp\":\"$(date --utc +%FT%T.%3NZ)\",\"playQueuePrime\":false}"\ - "https://layla.amazon.de/api/cloudplayer/queue-and-play?deviceSerialNumber=${DEVICESERIALNUMBER}&deviceType=${DEVICETYPE}&guideId=${STATIONID}&contentType=station&callSign=&mediaOwnerCustomerId=${MEDIAOWNERCUSTOMERID}&shuffle=false" > /dev/null +${CURL} ${OPTS} -s -b ${COOKIE} -A "Mozilla/5.0" -H "DNT: 1" -H "Connection: keep-alive" -L\ + -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ + -H "csrf: $(awk '$0 ~/.amazon.de.*csrf[\s\t]/ {print $7}' ${COOKIE})" -X POST -d "{\"trackId\":\"${SONG}\",\"playQueuePrime\":true}"\ + "https://${ALEXA}/api/cloudplayer/queue-and-play?deviceSerialNumber=${DEVICESERIALNUMBER}&deviceType=${DEVICETYPE}&mediaOwnerCustomerId=${MEDIAOWNERCUSTOMERID}&shuffle=false" } +# +# play library playlist +# +play_playlist() +{ +${CURL} ${OPTS} -s -b ${COOKIE} -A "Mozilla/5.0" -H "DNT: 1" -H "Connection: keep-alive" -L\ + -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ + -H "csrf: $(awk '$0 ~/.amazon.de.*csrf[\s\t]/ {print $7}' ${COOKIE})" -X POST -d "{\"playlistId\":\"${PLIST}\",\"playQueuePrime\":true}"\ + "https://${ALEXA}/api/cloudplayer/queue-and-play?deviceSerialNumber=${DEVICESERIALNUMBER}&deviceType=${DEVICETYPE}&mediaOwnerCustomerId=${MEDIAOWNERCUSTOMERID}&shuffle=false" +} +# +# play PRIME playlist +# +play_prime_playlist() +{ +${CURL} ${OPTS} -s -b ${COOKIE} -A "Mozilla/5.0" -H "DNT: 1" -H "Connection: keep-alive" -L\ + -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ + -H "csrf: $(awk '$0 ~/.amazon.de.*csrf[\s\t]/ {print $7}' ${COOKIE})" -X POST -d "{\"asin\":\"${ASIN}\"}"\ + "https://${ALEXA}/api/prime/prime-playlist-queue-and-play?deviceSerialNumber=${DEVICESERIALNUMBER}&deviceType=${DEVICETYPE}&mediaOwnerCustomerId=${MEDIAOWNERCUSTOMERID}" +} +# +# play PRIME station +# +play_prime_station() +{ +${CURL} ${OPTS} -s -b ${COOKIE} -A "Mozilla/5.0" -H "DNT: 1" -H "Connection: keep-alive" -L\ + -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ + -H "csrf: $(awk '$0 ~/.amazon.de.*csrf[\s\t]/ {print $7}' ${COOKIE})" -X POST -d "{\"seed\":\"{\\\"type\\\":\\\"KEY\\\",\\\"seedId\\\":\\\"${SEEDID}\\\"}\",\"stationName\":\"none\",\"seedType\":\"KEY\"}"\ + "https://${ALEXA}/api/gotham/queue-and-play?deviceSerialNumber=${DEVICESERIALNUMBER}&deviceType=${DEVICETYPE}&mediaOwnerCustomerId=${MEDIAOWNERCUSTOMERID}" +} + +# +# play PRIME historical queue +# +play_prime_hist_queue() +{ +${CURL} ${OPTS} -s -b ${COOKIE} -A "Mozilla/5.0" -H "DNT: 1" -H "Connection: keep-alive" -L\ + -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ + -H "csrf: $(awk '$0 ~/.amazon.de.*csrf[\s\t]/ {print $7}' ${COOKIE})" -X POST -d "{\"deviceType\":\"${DEVICETYPE}\",\"deviceSerialNumber\":\"${DEVICESERIALNUMBER}\",\"mediaOwnerCustomerId\":\"${MEDIAOWNERCUSTOMERID}\",\"queueId\":\"${HIST}\",\"service\":null,\"trackSource\":\"TRACK\"}"\ + "https://${ALEXA}/api/media/play-historical-queue" +} + +# +# show library tracks +# +show_library() +{ + OFFSET=""; + SIZE=50; + TOTAL=0; + FILE=${TMP}/.alexa.${TYPE}.list + + if [ ! -f ${FILE} ] ; then + echo -n '{"playlist":{"entryList":[' > ${FILE} + + while [ 50 -le ${SIZE} ] ; do + +${CURL} ${OPTS} -s -b ${COOKIE} -A "Mozilla/5.0" -H "DNT: 1" -H "Connection: keep-alive" -L\ + -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ + -H "csrf: $(awk '$0 ~/.amazon.de.*csrf[\s\t]/ {print $7}' ${COOKIE})" -X GET \ + "https://${ALEXA}/api/cloudplayer/playlists/${TYPE}-V0-OBJECTID?deviceSerialNumber=${DEVICESERIALNUMBER}&deviceType=${DEVICETYPE}&size=${SIZE}&offset=${OFFSET}&mediaOwnerCustomerId=${MEDIAOWNERCUSTOMERID}" > ${FILE}.tmp + + OFFSET=$(jq -r '.nextResultsToken' ${FILE}.tmp) + SIZE=$(jq -r '.playlist | .trackCount' ${FILE}.tmp) + jq -r -c '.playlist | .entryList' ${FILE}.tmp >> ${FILE} + echo "," >> ${FILE} + TOTAL=$((TOTAL+SIZE)) + done + echo "[]],\"trackCount\":\"${TOTAL}\"}}" >> ${FILE} + rm -f ${FILE}.tmp + fi + jq -r '.playlist.trackCount' ${FILE} + jq '.playlist.entryList[] | .[]' ${FILE} +} + +# +# show Prime stations and playlists +# +show_prime() +{ + FILE=${TMP}/.alexa.${PRIME}.list + + if [ ! -f ${FILE} ] ; then +${CURL} ${OPTS} -s -b ${COOKIE} -A "Mozilla/5.0" -H "DNT: 1" -H "Connection: keep-alive" -L\ + -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ + -H "csrf: $(awk '$0 ~/.amazon.de.*csrf[\s\t]/ {print $7}' ${COOKIE})" -X GET \ + "https://${ALEXA}/api/prime/{$PRIME}?deviceSerialNumber=${DEVICESERIALNUMBER}&deviceType=${DEVICETYPE}&mediaOwnerCustomerId=${MEDIAOWNERCUSTOMERID}" > ${FILE} + + if [ "$PRIME" = "prime-playlist-browse-nodes" ] ; then + for I in $(jq -r '.primePlaylistBrowseNodeList[].subNodes[].nodeId' ${FILE} 2>/dev/null) ; do +${CURL} ${OPTS} -s -b ${COOKIE} -A "Mozilla/5.0" -H "DNT: 1" -H "Connection: keep-alive" -L\ + -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ + -H "csrf: $(awk '$0 ~/.amazon.de.*csrf[\s\t]/ {print $7}' ${COOKIE})" -X GET \ + "https://${ALEXA}/api/prime/prime-playlists-by-browse-node?browseNodeId=${I}&deviceSerialNumber=${DEVICESERIALNUMBER}&deviceType=${DEVICETYPE}&mediaOwnerCustomerId=${MEDIAOWNERCUSTOMERID}" >> ${FILE} + done + fi + fi + jq '.' ${FILE} +} + +# +# current queue +# +show_queue() +{ + PARENT="" + PARENTID=$(jq --arg device "${DEVICE}" -r '.devices[] | select(.accountName == $device) | .parentClusters[0]' ${DEVLIST}) + if [ "$PARENTID" != "null" ] ; then + PARENTDEVICE=$(jq --arg serial ${PARENTID} -r '.devices[] | select(.serialNumber == $serial) | .deviceType' ${DEVLIST}) + PARENT="&lemurId=${PARENTID}&lemurDeviceType=${PARENTDEVICE}" + fi + +${CURL} ${OPTS} -s -b ${COOKIE} -A "Mozilla/5.0" -H "DNT: 1" -H "Connection: keep-alive" -L\ + -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ + -H "csrf: $(awk '$0 ~/.amazon.de.*csrf[\s\t]/ {print $7}' ${COOKIE})" -X GET \ + "https://${ALEXA}/api/np/player?deviceSerialNumber=${DEVICESERIALNUMBER}&deviceType=${DEVICETYPE}${PARENT}" | jq '.' + +${CURL} ${OPTS} -s -b ${COOKIE} -A "Mozilla/5.0" -H "DNT: 1" -H "Connection: keep-alive" -L\ + -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ + -H "csrf: $(awk '$0 ~/.amazon.de.*csrf[\s\t]/ {print $7}' ${COOKIE})" -X GET \ + "https://${ALEXA}/api/media/state?deviceSerialNumber=${DEVICESERIALNUMBER}&deviceType=${DEVICETYPE}" | jq '.' + +${CURL} ${OPTS} -s -b ${COOKIE} -A "Mozilla/5.0" -H "DNT: 1" -H "Connection: keep-alive" -L\ + -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ + -H "csrf: $(awk '$0 ~/.amazon.de.*csrf[\s\t]/ {print $7}' ${COOKIE})" -X GET \ + "https://${ALEXA}/api/np/queue?deviceSerialNumber=${DEVICESERIALNUMBER}&deviceType=${DEVICETYPE}" | jq '.' +} + +# +# deletes a multiroom device +# +delete_multiroom() +{ +${CURL} ${OPTS} -s -b ${COOKIE} -A "Mozilla/5.0" -H "DNT: 1" -H "Connection: keep-alive" -L\ + -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ + -H "csrf: $(awk '$0 ~/.amazon.de.*csrf[\s\t]/ {print $7}' ${COOKIE})" -X DELETE \ + "https://${ALEXA}/api/lemur/tail/${DEVICESERIALNUMBER}" +} + +# +# creates a multiroom device +# +create_multiroom() +{ + JSON="{\"id\":null,\"name\":\"${LEMUR}\",\"members\":[" + for DEVICE in $CHILD ; do + set_var + JSON="${JSON}{\"dsn\":\"${DEVICESERIALNUMBER}\",\"deviceType\":\"${DEVICETYPE}\"}," + done + JSON="${JSON%,}]}" + +${CURL} ${OPTS} -s -b ${COOKIE} -A "Mozilla/5.0" -H "DNT: 1" -H "Connection: keep-alive" -L\ + -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ + -H "csrf: $(awk '$0 ~/.amazon.de.*csrf[\s\t]/ {print $7}' ${COOKIE})" -X POST -d "${JSON}" \ + "https://${ALEXA}/api/lemur/tail" +} + +# +# connect bluetooth device +# +connect_bluetooth() +{ +${CURL} ${OPTS} -s -b ${COOKIE} -A "Mozilla/5.0" -H "DNT: 1" -H "Connection: keep-alive" -L\ + -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ + -H "csrf: $(awk '$0 ~/.amazon.de.*csrf[\s\t]/ {print $7}' ${COOKIE})" -X POST -d "{\"bluetoothDeviceAddress\":\"${BLUETOOTH}\"}"\ + "https://${ALEXA}/api/bluetooth/pair-sink/${DEVICETYPE}/${DEVICESERIALNUMBER}" +} + +# +# disconnect bluetooth device +# +disconnect_bluetooth() +{ +${CURL} ${OPTS} -s -b ${COOKIE} -A "Mozilla/5.0" -H "DNT: 1" -H "Connection: keep-alive" -L\ + -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ + -H "csrf: $(awk '$0 ~/.amazon.de.*csrf[\s\t]/ {print $7}' ${COOKIE})" -X POST \ + "https://${ALEXA}/api/bluetooth/disconnect-sink/${DEVICETYPE}/${DEVICESERIALNUMBER}" +} # # logout # log_off() { -curl -s -c ${COOKIE} -b ${COOKIE} -A "Mozilla/5.0" --compressed -H "DNT: 1" -H "Connection: keep-alive" -L\ - https://layla.amazon.de/logout > /dev/null +${CURL} ${OPTS} -s -c ${COOKIE} -b ${COOKIE} -A "Mozilla/5.0" -H "DNT: 1" -H "Connection: keep-alive" -L\ + https://${ALEXA}/logout > /dev/null rm -f ${DEVLIST} rm -f ${COOKIE} +rm -f ${TMP}/.alexa.*.list } -if [ -z "$LIST" -a -z "$COMMAND" -a -z "$STATIONID" -a -z "$SONG" -a -n "$LOGOFF" ] ; then - echo "only logout option present, logging off ..." - log_off - exit 0 +if [ -z "$BLUETOOTH" -a -z "$LEMUR" -a -z "$PLIST" -a -z "$HIST" -a -z "$SEEDID" -a -z "$ASIN" -a -z "$PRIME" -a -z "$TYPE" -a -z "$QUEUE" -a -z "$LIST" -a -z "$COMMAND" -a -z "$STATIONID" -a -z "$SONG" -a -n "$LOGOFF" ] ; then + echo "only logout option present, logging off ..." + log_off + exit 0 fi -if [ ! -f ${DEVLIST} -o ! -f ${COOKIE} ] ; then - echo "files do not exist. logging in ..." - log_in +if [ ! -f ${COOKIE} ] ; then + echo "cookie do not exist. logging in ..." + log_in +fi + +if [ ! -f ${DEVLIST} ] ; then + echo "device list do not exist. downloading ..." + get_devlist fi check_status if [ $? -eq 0 ] ; then - echo "cookie expired, logging in again ..." - log_in + echo "cookie expired, logging in again ..." + log_in fi -if [ -n "$COMMAND" ] ; then - if [ "${DEVICE}" = "ALL" ] ; then - for DEVICE in $(jq -r '.devices[] | select( .deviceFamily == "ECHO" or .deviceFamily == "WHA") | .accountName' ${DEVLIST}) ; do - set_var - echo "sending cmd:${COMMAND} to dev:${DEVICE} type:${DEVICETYPE} serial:${DEVICESERIALNUMBER}" - run_cmd - done - else - set_var - echo "sending cmd:${COMMAND} to dev:${DEVICE} type:${DEVICETYPE} serial:${DEVICESERIALNUMBER}" - run_cmd - fi +if [ -n "$COMMAND" -o -n "$QUEUE" ] ; then + if [ "${DEVICE}" = "ALL" ] ; then + for DEVICE in $(jq -r '.devices[] | select( .deviceFamily == "ECHO" or .deviceFamily == "WHA") | .accountName' ${DEVLIST} | sed -r 's/ /%20/g') ; do + set_var + if [ -n "$COMMAND" ] ; then + echo "sending cmd:${COMMAND} to dev:${DEVICE} type:${DEVICETYPE} serial:${DEVICESERIALNUMBER}" + run_cmd + else + echo "queue info for dev:${DEVICE} type:${DEVICETYPE} serial:${DEVICESERIALNUMBER}" + show_queue + fi + done + else + set_var + if [ -n "$COMMAND" ] ; then + echo "sending cmd:${COMMAND} to dev:${DEVICE} type:${DEVICETYPE} serial:${DEVICESERIALNUMBER}" + run_cmd + else + echo "queue info for dev:${DEVICE} type:${DEVICETYPE} serial:${DEVICESERIALNUMBER}" + show_queue + fi + fi +elif [ -n "$LEMUR" ] ; then + DEVICESERIALNUMBER=$(jq --arg device "${LEMUR}" -r '.devices[] | select(.accountName == $device and .deviceFamily == "WHA") | .serialNumber' ${DEVLIST}) + if [ -n "$DEVICESERIALNUMBER" ] ; then + delete_multiroom + else + if [ -z "$CHILD" ] ; then + echo "ERROR: ${LEMUR} is no multiroom device. Cannot delete ${LEMUR}". + exit 1 + fi + fi + if [ -z "$CHILD" ] ; then + echo "Deleted multi room dev:${LEMUR} serial:${DEVICESERIALNUMBER}" + else + echo "Creating multi room dev:${LEMUR} member_dev(s):${CHILD}" + create_multiroom + fi + rm -f ${DEVLIST} + get_devlist +elif [ -n "$BLUETOOTH" ] ; then + set_var + if [ "$BLUETOOTH" = "null" ] ; then + echo "disconnecting dev:${DEVICE} type:${DEVICETYPE} serial:${DEVICESERIALNUMBER} from bluetooth" + disconnect_bluetooth + else + echo "connecting dev:${DEVICE} type:${DEVICETYPE} serial:${DEVICESERIALNUMBER} to bluetooth device:${BLUETOOTH}" + connect_bluetooth + fi elif [ -n "$STATIONID" ] ; then - set_var - echo "playing stationID:${STATIONID} on dev:${DEVICE} type:${DEVICETYPE} serial:${DEVICESERIALNUMBER} mediaownerid:${MEDIAOWNERCUSTOMERID}" - play_radio + set_var + echo "playing stationID:${STATIONID} on dev:${DEVICE} type:${DEVICETYPE} serial:${DEVICESERIALNUMBER} mediaownerid:${MEDIAOWNERCUSTOMERID}" + play_radio elif [ -n "$SONG" ] ; then - set_var - echo "playing library track:${SONG} on dev:${DEVICE} type:${DEVICETYPE} serial:${DEVICESERIALNUMBER} mediaownerid:${MEDIAOWNERCUSTOMERID}" - play_song + set_var + echo "playing library track:${SONG} on dev:${DEVICE} type:${DEVICETYPE} serial:${DEVICESERIALNUMBER} mediaownerid:${MEDIAOWNERCUSTOMERID}" + play_song +elif [ -n "$PLIST" ] ; then + set_var + echo "playing library playlist:${PLIST} on dev:${DEVICE} type:${DEVICETYPE} serial:${DEVICESERIALNUMBER} mediaownerid:${MEDIAOWNERCUSTOMERID}" + play_playlist elif [ -n "$LIST" ] ; then - echo "the following devices exist in your account:" - list_devices + echo "the following devices exist in your account:" + list_devices +elif [ -n "$TYPE" ] ; then + set_var + echo -n "the following songs exist in your ${TYPE} library: " + show_library +elif [ -n "$PRIME" ] ; then + set_var + echo "the following songs exist in your PRIME ${PRIME}:" + show_prime +elif [ -n "$ASIN" ] ; then + set_var + echo "playing PRIME playlist ${ASIN}" + play_prime_playlist +elif [ -n "$SEEDID" ] ; then + set_var + echo "playing PRIME station ${SEEDID}" + play_prime_station +elif [ -n "$HIST" ] ; then + set_var + echo "playing PRIME historical queue ${HIST}" + play_prime_hist_queue else - echo "no alexa command received" + echo "no alexa command received" fi if [ -n "$LOGOFF" ] ; then - echo "logout option present, logging off ..." - log_off + echo "logout option present, logging off ..." + log_off fi diff --git a/alexa_remote_control_plain.sh b/alexa_remote_control_plain.sh new file mode 100644 index 0000000..cbc05ae --- /dev/null +++ b/alexa_remote_control_plain.sh @@ -0,0 +1,670 @@ +#!/bin/sh +# +# Amazon Alexa Remote Control (PLAIN shell) +# alex(at)loetzimmer.de +# +# 2017-12-07: v0.7b (for updates see http://blog.loetzimmer.de/2017/10/amazon-alexa-hort-auf-die-shell-echo.html) +# +### +# +# (no BASHisms were used, should run with any shell) +# - requires cURL for web communication +# - sed and awk for extraction +# +########################################## + +EMAIL='amazon_account@email.address' +PASSWORD='Very_Secret_Amazon_Account_Password' + +LANGUAGE="de,en" +#LANGUAGE="en-us" + +AMAZON='amazon.de' +#AMAZON='amazon.com' + +ALEXA='layla.amazon.de' +#ALEXA='pitangui.amazon.com' + +# cURL binary +CURL='/usr/bin/curl' + +# cURL options +# -k : if your cURL cannot verify CA certificates, you'll have to trust any +# --compressed : if your cURL was compiled with libz you may use compression +OPTS='--compressed' +#OPTS='-k --compressed' + +########################################### +# nothing to configure below here +# +TMP="/tmp" +COOKIE="${TMP}/.alexa.cookie" +DEVLIST="${TMP}/.alexa.devicelist.json" +DEVTXT="${TMP}/.alexa.devicelist.txt" + +GUIVERSION=0 + +LIST="" +LOGOFF="" +COMMAND="" +STATIONID="" +QUEUE="" +SONG="" +ASIN="" +SEEDID="" +HIST="" +LEMUR="" +CHILD="" +PLIST="" +BLUETOOTH="" + +usage() +{ + echo "$0 [-d |ALL] -e > | -b [<\"AA:BB:CC:DD:EE:FF\">] | -q | -r <\"station name\"|stationid> | -s | -t |" + echo " -u | -v | -w | -i | -p | -a | -m [device_1 .. device_X] | -l | -h" + echo " -e : run command" + echo " -q : query queue" + echo " -b : connect/disconnect bluetooth device" + echo " -r : play tunein radio" + echo " -s : play library track" + echo " -t : play Prime playlist" + echo " -u : play Prime station" + echo " -v : play Prime historical queue" + echo " -w : play library playlist" + echo " -i : list imported library tracks" + echo " -p : list purchased library tracks" + echo " -a : list available devices" + echo " -m : delete multiroom and/or create new multiroom containing devices" + echo " -l : logoff" + echo " -h : help" +} + +while [ "$#" -gt 0 ] ; do + case "$1" in + -d) + if [ "${2#-}" != "${2}" -o -z "$2" ] ; then + echo "ERROR: missing argument for ${1}" + usage + exit 1 + fi + DEVICE=$2 + shift + ;; + -e) + if [ "${2#-}" != "${2}" -o -z "$2" ] ; then + echo "ERROR: missing argument for ${1}" + usage + exit 1 + fi + COMMAND=$2 + shift + ;; + -b) + if [ "${2#-}" = "${2}" -a -n "$2" ] ; then + BLUETOOTH=$2 + shift + else + BLUETOOTH="null" + fi + ;; + -m) + if [ "${2#-}" != "${2}" -o -z "$2" ] ; then + echo "ERROR: missing argument for ${1}" + usage + exit 1 + fi + LEMUR=$2 + shift + while [ "${2#-}" = "${2}" -a -n "$2" ] ; do + CHILD="${CHILD} ${2}" + shift + done + ;; + -r) + if [ "${2#-}" != "${2}" -o -z "$2" ] ; then + echo "ERROR: missing argument for ${1}" + usage + exit 1 + fi + STATIONID=$2 + shift + ;; + -s) + if [ "${2#-}" != "${2}" -o -z "$2" ] ; then + echo "ERROR: missing argument for ${1}" + usage + exit 1 + fi + SONG=$2 + shift + ;; + -t) + if [ "${2#-}" != "${2}" -o -z "$2" ] ; then + echo "ERROR: missing argument for ${1}" + usage + exit 1 + fi + ASIN=$2 + shift + ;; + -u) + if [ "${2#-}" != "${2}" -o -z "$2" ] ; then + echo "ERROR: missing argument for ${1}" + usage + exit 1 + fi + SEEDID=$2 + shift + ;; + -v) + if [ "${2#-}" != "${2}" -o -z "$2" ] ; then + echo "ERROR: missing argument for ${1}" + usage + exit 1 + fi + HIST=$2 + shift + ;; + -w) + if [ "${2#-}" != "${2}" -o -z "$2" ] ; then + echo "ERROR: missing argument for ${1}" + usage + exit 1 + fi + PLIST=$2 + shift + ;; + -d) + if [ "${2#-}" != "${2}" -o -z "$2" ] ; then + echo "ERROR: missing argument for ${1}" + usage + exit 1 + fi + DEVICE=$2 + shift + ;; + -l) + LOGOFF="true" + ;; + -a) + LIST="true" + ;; + -q) + QUEUE="true" + ;; + -h|-\?|--help) + usage + exit 0 + ;; + *) + echo "ERROR: unknown option ${1}" + usage + exit 1 + ;; + esac + shift +done + +case "$COMMAND" in + pause) + COMMAND='{"type":"PauseCommand"}' + ;; + play) + COMMAND='{"type":"PlayCommand"}' + ;; + next) + COMMAND='{"type":"NextCommand"}' + ;; + prev) + COMMAND='{"type":"PreviousCommand"}' + ;; + fwd) + COMMAND='{"type":"ForwardCommand"}' + ;; + rwd) + COMMAND='{"type":"RewindCommand"}' + ;; + shuffle) + COMMAND='{"type":"ShuffleCommand","shuffle":"true"}' + ;; + vol:*) + VOL=${COMMAND##*:} + # volume as integer! + if [ $VOL -le 100 -a $VOL -ge 0 ] ; then + COMMAND='{"type":"VolumeLevelCommand","volumeLevel":'${VOL}'}' + else + echo "ERROR: volume should be an integer between 0 and 100" + usage + exit 1 + fi + ;; + "") + ;; + *) + echo "ERROR: unknown command \"${COMMAND}\"!" + usage + exit 1 + ;; +esac + +# +# Amazon Login +# +log_in() +{ +################################################################ +# +# following headers are required: +# Accept-Language (possibly for determining login region) +# User-Agent (CURL wouldn't store cookies without) +# +################################################################ + +rm -f ${DEVLIST} +rm -f ${DEVTXT} +rm -f ${COOKIE} + +# +# get first cookie and write redirection target into referer +# +${CURL} ${OPTS} -s -D "${TMP}/.alexa.header" -c ${COOKIE} -b ${COOKIE} -A "Mozilla/5.0" -H "Accept-Language: ${LANGUAGE}" -H "DNT: 1" -H "Connection: keep-alive" -H "Upgrade-Insecure-Requests: 1" -L\ + https://alexa.${AMAZON} | grep "hidden" | sed 's/hidden/\n/g' | grep "value=\"" | sed -r 's/^.*name="([^"]+)".*value="([^"]+)".*/\1=\2\&/g' > "${TMP}/.alexa.postdata" + +# +# login empty to generate sessiion +# +${CURL} ${OPTS} -s -c ${COOKIE} -b ${COOKIE} -A "Mozilla/5.0" -H "Accept-Language: ${LANGUAGE}" -H "DNT: 1" -H "Connection: keep-alive" -H "Upgrade-Insecure-Requests: 1" -L\ + -H "$(grep 'Location: ' ${TMP}/.alexa.header | sed 's/Location: /Referer: /')" -d "@${TMP}/.alexa.postdata" https://www.${AMAZON}/ap/signin | grep "hidden" | sed 's/hidden/\n/g' | grep "value=\"" | sed -r 's/^.*name="([^"]+)".*value="([^"]+)".*/\1=\2\&/g' > "${TMP}/.alexa.postdata2" + +# +# login with filled out form +# !!! referer now contains session in URL +# +${CURL} ${OPTS} -s -c ${COOKIE} -b ${COOKIE} -A "Mozilla/5.0" -H "Accept-Language: ${LANGUAGE}" -H "DNT: 1" -H "Connection: keep-alive" -H "Upgrade-Insecure-Requests: 1" -L\ + -H "Referer: https://www.${AMAZON}/ap/signin/$(awk "\$0 ~/.${AMAZON}.*session-id[\\s\\t]/ {print \$7}" ${COOKIE})" --data-urlencode "email=${EMAIL}" --data-urlencode "password=${PASSWORD}" -d "@${TMP}/.alexa.postdata2" https://www.${AMAZON}/ap/signin > /dev/null + +# +# get CSRF +# +${CURL} ${OPTS} -s -c ${COOKIE} -b ${COOKIE} -A "Mozilla/5.0" -H "DNT: 1" -H "Connection: keep-alive" -L\ + -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ + https://${ALEXA}/api/language > /dev/null + +rm -f "${TMP}/.alexa.header" +rm -f "${TMP}/.alexa.postdata" +rm -f "${TMP}/.alexa.postdata2" +} + +# +# get JSON device list +# +get_devlist() +{ +${CURL} ${OPTS} -s -b ${COOKIE} -A "Mozilla/5.0" -H "DNT: 1" -H "Connection: keep-alive" -L\ + -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ + -H "csrf: $(awk '$0 ~/.amazon.de.*csrf[\s\t]/ {print $7}' ${COOKIE})"\ + "https://${ALEXA}/api/devices-v2/device?cached=false" > ${DEVLIST} + + if [ ! -f ${DEVTXT} ] ; then + cat ${DEVLIST}| sed 's/\\\\\//\//g' | sed 's/[{}]//g' | awk -v k="text" '{n=split($0,a,","); for (i=1; i<=n; i++) print a[i]}' | sed 's/\"\:\"/\|/g' | sed 's/[\,]/ /g' | sed 's/\"//g' > ${DEVTXT} + fi +} + +check_status() +{ +# +# bootstrap with GUI-Version writes GUI version to cookie +# returns among other the current authentication state +# + AUTHSTATUS=$(${CURL} ${OPTS} -s -c ${COOKIE} -b ${COOKIE} -A "Mozilla/5.0" -H "DNT: 1" -H "Connection: keep-alive" -L https://${ALEXA}/api/bootstrap?version=${GUIVERSION} | sed -r 's/^.*"authenticated":([^,]+),.*$/\1/g') + + if [ "$AUTHSTATUS" = "true" ] ; then + return 1 + fi + + return 0 +} + +# +# set device specific variables from JSON device list +# +set_var() +{ + ATTR="accountName" + NAME=`grep ${ATTR}\| ${DEVTXT} | sed "s/^.*${ATTR}|//" | sed 's/ /_/g'` + + ATTR="deviceType" + TYPE=`grep ${ATTR}\| ${DEVTXT} | sed "s/^.*${ATTR}|//" | sed 's/ /_/g'` + + ATTR="serialNumber" + SERIAL=`grep ${ATTR}\| ${DEVTXT} | sed "s/^.*${ATTR}|//" | sed 's/ /_/g'` + + ATTR="deviceOwnerCustomerId" + MEDIAID=`grep ${ATTR}\| ${DEVTXT} | sed "s/^.*${ATTR}|//" | sed 's/ /_/g'` + + ATTR="deviceFamily" + FAMILY=`grep ${ATTR}\| ${DEVTXT} | sed "s/^.*${ATTR}|//" | sed 's/ /_/g'` + + + if [ -z "${DEVICE}" ] ; then + # if no device was supplied, use the first Echo(dot) in device list + IDX=0 + for I in $FAMILY ; do + if [ "$I" = "ECHO" ] ; then + break; + fi + IDX=$((IDX+1)) + done + + C=0 + for I in $NAME ; do + if [ $C -eq $IDX ] ; then + DEVICE=$I + break + fi + C=$((C+1)) + done + echo "setting default device to:" + echo ${DEVICE} + else + DEVICE=`echo $DEVICE | sed 's/ /_/g'` + IDX=0 + for I in $NAME ; do + if [ "$I" = "$DEVICE" ] ; then + break; + fi + IDX=$((IDX+1)) + done + fi + + C=0 + for I in $MEDIAID ; do + if [ $C -eq $IDX ] ; then + MEDIAOWNERCUSTOMERID=$I + break + fi + C=$((C+1)) + done + + C=0 + for I in $TYPE ; do + if [ $C -eq $IDX ] ; then + DEVICETYPE=$I + break + fi + C=$((C+1)) + done + + C=0 + for I in $SERIAL ; do + if [ $C -eq $IDX ] ; then + DEVICESERIALNUMBER=$I + break + fi + C=$((C+1)) + done + + if [ -z "${DEVICESERIALNUMBER}" ] ; then + echo "ERROR: unkown device dev:${DEVICE}" + exit 1 + fi +} + +# +# execute command +# +run_cmd() +{ +${CURL} ${OPTS} -s -b ${COOKIE} -A "Mozilla/5.0" -H "DNT: 1" -H "Connection: keep-alive" -L\ + -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ + -H "csrf: $(awk '$0 ~/.amazon.de.*csrf[\s\t]/ {print $7}' ${COOKIE})" -X POST -d ${COMMAND}\ + "https://${ALEXA}/api/np/command?deviceSerialNumber=${DEVICESERIALNUMBER}&deviceType=${DEVICETYPE}" +} + +# +# play TuneIn radio station +# +play_radio() +{ +${CURL} ${OPTS} -s -b ${COOKIE} -A "Mozilla/5.0" -H "DNT: 1" -H "Connection: keep-alive" -L\ + -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ + -H "csrf: $(awk '$0 ~/.amazon.de.*csrf[\s\t]/ {print $7}' ${COOKIE})" -X POST\ + "https://${ALEXA}/api/tunein/queue-and-play?deviceSerialNumber=${DEVICESERIALNUMBER}&deviceType=${DEVICETYPE}&guideId=${STATIONID}&contentType=station&callSign=&mediaOwnerCustomerId=${MEDIAOWNERCUSTOMERID}" +} + +# +# play library track +# +play_song() +{ +${CURL} ${OPTS} -s -b ${COOKIE} -A "Mozilla/5.0" -H "DNT: 1" -H "Connection: keep-alive" -L\ + -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ + -H "csrf: $(awk '$0 ~/.amazon.de.*csrf[\s\t]/ {print $7}' ${COOKIE})" -X POST -d "{\"trackId\":\"${SONG}\",\"playQueuePrime\":true}"\ + "https://${ALEXA}/api/cloudplayer/queue-and-play?deviceSerialNumber=${DEVICESERIALNUMBER}&deviceType=${DEVICETYPE}&mediaOwnerCustomerId=${MEDIAOWNERCUSTOMERID}&shuffle=false" +} + +# +# play library playlist +# +play_playlist() +{ +${CURL} ${OPTS} -s -b ${COOKIE} -A "Mozilla/5.0" -H "DNT: 1" -H "Connection: keep-alive" -L\ + -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ + -H "csrf: $(awk '$0 ~/.amazon.de.*csrf[\s\t]/ {print $7}' ${COOKIE})" -X POST -d "{\"playlistId\":\"${PLIST}\",\"playQueuePrime\":true}"\ + "https://${ALEXA}/api/cloudplayer/queue-and-play?deviceSerialNumber=${DEVICESERIALNUMBER}&deviceType=${DEVICETYPE}&mediaOwnerCustomerId=${MEDIAOWNERCUSTOMERID}&shuffle=false" +} + +# +# play PRIME playlist +# +play_prime_playlist() +{ +${CURL} ${OPTS} -s -b ${COOKIE} -A "Mozilla/5.0" -H "DNT: 1" -H "Connection: keep-alive" -L\ + -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ + -H "csrf: $(awk '$0 ~/.amazon.de.*csrf[\s\t]/ {print $7}' ${COOKIE})" -X POST -d "{\"asin\":\"${ASIN}\"}"\ + "https://${ALEXA}/api/prime/prime-playlist-queue-and-play?deviceSerialNumber=${DEVICESERIALNUMBER}&deviceType=${DEVICETYPE}&mediaOwnerCustomerId=${MEDIAOWNERCUSTOMERID}" +} + +# +# play PRIME station +# +play_prime_station() +{ +${CURL} ${OPTS} -s -b ${COOKIE} -A "Mozilla/5.0" -H "DNT: 1" -H "Connection: keep-alive" -L\ + -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ + -H "csrf: $(awk '$0 ~/.amazon.de.*csrf[\s\t]/ {print $7}' ${COOKIE})" -X POST -d "{\"seed\":\"{\\\"type\\\":\\\"KEY\\\",\\\"seedId\\\":\\\"${SEEDID}\\\"}\",\"stationName\":\"none\",\"seedType\":\"KEY\"}"\ + "https://${ALEXA}/api/gotham/queue-and-play?deviceSerialNumber=${DEVICESERIALNUMBER}&deviceType=${DEVICETYPE}&mediaOwnerCustomerId=${MEDIAOWNERCUSTOMERID}" +} + +# +# play PRIME historical queue +# +play_prime_hist_queue() +{ +${CURL} ${OPTS} -s -b ${COOKIE} -A "Mozilla/5.0" -H "DNT: 1" -H "Connection: keep-alive" -L\ + -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ + -H "csrf: $(awk '$0 ~/.amazon.de.*csrf[\s\t]/ {print $7}' ${COOKIE})" -X POST -d "{\"deviceType\":\"${DEVICETYPE}\",\"deviceSerialNumber\":\"${DEVICESERIALNUMBER}\",\"mediaOwnerCustomerId\":\"${MEDIAOWNERCUSTOMERID}\",\"queueId\":\"${HIST}\",\"service\":null,\"trackSource\":\"TRACK\"}"\ + "https://${ALEXA}/api/media/play-historical-queue" +} + +# +# current queue +# +show_queue() +{ + echo "/api/np/player" +${CURL} ${OPTS} -s -b ${COOKIE} -A "Mozilla/5.0" -H "DNT: 1" -H "Connection: keep-alive" -L\ + -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ + -H "csrf: $(awk '$0 ~/.amazon.de.*csrf[\s\t]/ {print $7}' ${COOKIE})" -X GET \ + "https://${ALEXA}/api/np/player?deviceSerialNumber=${DEVICESERIALNUMBER}&deviceType=${DEVICETYPE}" + echo + echo "/api/np/queue" +${CURL} ${OPTS} -s -b ${COOKIE} -A "Mozilla/5.0" -H "DNT: 1" -H "Connection: keep-alive" -L\ + -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ + -H "csrf: $(awk '$0 ~/.amazon.de.*csrf[\s\t]/ {print $7}' ${COOKIE})" -X GET \ + "https://${ALEXA}/api/np/queue?deviceSerialNumber=${DEVICESERIALNUMBER}&deviceType=${DEVICETYPE}" + echo + echo "/api/media/state" + ${CURL} ${OPTS} -s -b ${COOKIE} -A "Mozilla/5.0" -H "DNT: 1" -H "Connection: keep-alive" -L\ + -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ + -H "csrf: $(awk '$0 ~/.amazon.de.*csrf[\s\t]/ {print $7}' ${COOKIE})" -X GET \ + "https://${ALEXA}/api/media/state?deviceSerialNumber=${DEVICESERIALNUMBER}&deviceType=${DEVICETYPE}" + echo +} + +# +# deletes a multiroom device +# +delete_multiroom() +{ +${CURL} ${OPTS} -s -b ${COOKIE} -A "Mozilla/5.0" -H "DNT: 1" -H "Connection: keep-alive" -L\ + -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ + -H "csrf: $(awk '$0 ~/.amazon.de.*csrf[\s\t]/ {print $7}' ${COOKIE})" -X DELETE \ + "https://${ALEXA}/api/lemur/tail/${DEVICESERIALNUMBER}" +} + +# +# creates a multiroom device +# +create_multiroom() +{ + JSON="{\"id\":null,\"name\":\"${LEMUR}\",\"members\":[" + for DEVICE in $CHILD ; do + set_var + JSON="${JSON}{\"dsn\":\"${DEVICESERIALNUMBER}\",\"deviceType\":\"${DEVICETYPE}\"}," + done + JSON="${JSON%,}]}" + +${CURL} ${OPTS} -s -b ${COOKIE} -A "Mozilla/5.0" -H "DNT: 1" -H "Connection: keep-alive" -L\ + -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ + -H "csrf: $(awk '$0 ~/.amazon.de.*csrf[\s\t]/ {print $7}' ${COOKIE})" -X POST -d "${JSON}" \ + "https://${ALEXA}/api/lemur/tail" +} + +# +# connect bluetooth device +# +connect_bluetooth() +{ +${CURL} ${OPTS} -s -b ${COOKIE} -A "Mozilla/5.0" -H "DNT: 1" -H "Connection: keep-alive" -L\ + -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ + -H "csrf: $(awk '$0 ~/.amazon.de.*csrf[\s\t]/ {print $7}' ${COOKIE})" -X POST -d "{\"bluetoothDeviceAddress\":\"${BLUETOOTH}\"}"\ + "https://${ALEXA}/api/bluetooth/pair-sink/${DEVICETYPE}/${DEVICESERIALNUMBER}" +} + +# +# disconnect bluetooth device +# +disconnect_bluetooth() +{ +${CURL} ${OPTS} -s -b ${COOKIE} -A "Mozilla/5.0" -H "DNT: 1" -H "Connection: keep-alive" -L\ + -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ + -H "csrf: $(awk '$0 ~/.amazon.de.*csrf[\s\t]/ {print $7}' ${COOKIE})" -X POST \ + "https://${ALEXA}/api/bluetooth/disconnect-sink/${DEVICETYPE}/${DEVICESERIALNUMBER}" +} + +# +# logout +# +log_off() +{ +${CURL} ${OPTS} -s -c ${COOKIE} -b ${COOKIE} -A "Mozilla/5.0" -H "DNT: 1" -H "Connection: keep-alive" -L\ + https://${ALEXA}/logout > /dev/null + +rm -f ${DEVLIST} +rm -f ${DEVTXT} +rm -f ${COOKIE} +} + +if [ -z "$BLUETOOTH" -a -z "$LEMUR" -a -z "$PLIST" -a -z "$HIST" -a -z "$SEEDID" -a -z "$ASIN" -a -z "$QUEUE" -a -z "$COMMAND" -a -z "$STATIONID" -a -z "$SONG" -a -n "$LOGOFF" ] ; then + echo "only logout option present, logging off ..." + log_off + exit 0 +fi + +if [ ! -f ${COOKIE} ] ; then + echo "cookie do not exist. logging in ..." + log_in +fi + +if [ ! -f ${DEVLIST} ] ; then + echo "device list do not exist. downloading ..." + get_devlist +fi + +check_status +if [ $? -eq 0 ] ; then + echo "cookie expired, logging in again ..." + log_in +fi + +if [ -n "$COMMAND" ] ; then + set_var + if [ -n "$COMMAND" ] ; then + echo "sending cmd:${COMMAND} to dev:${DEVICE} type:${DEVICETYPE} serial:${DEVICESERIALNUMBER}" + run_cmd + fi +elif [ -n "$LEMUR" ] ; then + DEVICE="${LEMUR}" + set_var + if [ -n "$DEVICESERIALNUMBER" ] ; then + delete_multiroom + fi + if [ -z "$CHILD" ] ; then + echo "Deleted multi room dev:${LEMUR} serial:${DEVICESERIALNUMBER}" + else + echo "Creating multi room dev:${LEMUR} member_dev(s):${CHILD}" + create_multiroom + fi + rm -f ${DEVLIST} + rm -f ${DEVTXT} + get_devlist +elif [ -n "$BLUETOOTH" ] ; then + set_var + if [ "$BLUETOOTH" = "null" ] ; then + echo "disconnecting dev:${DEVICE} type:${DEVICETYPE} serial:${DEVICESERIALNUMBER} from bluetooth" + disconnect_bluetooth + else + echo "connecting dev:${DEVICE} type:${DEVICETYPE} serial:${DEVICESERIALNUMBER} to bluetooth device:${BLUETOOTH}" + connect_bluetooth + fi +elif [ -n "$STATIONID" ] ; then + set_var + echo "playing stationID:${STATIONID} on dev:${DEVICE} type:${DEVICETYPE} serial:${DEVICESERIALNUMBER} mediaownerid:${MEDIAOWNERCUSTOMERID}" + play_radio +elif [ -n "$SONG" ] ; then + set_var + echo "playing library track:${SONG} on dev:${DEVICE} type:${DEVICETYPE} serial:${DEVICESERIALNUMBER} mediaownerid:${MEDIAOWNERCUSTOMERID}" + play_song +elif [ -n "$PLIST" ] ; then + set_var + echo "playing library playlist:${PLIST} on dev:${DEVICE} type:${DEVICETYPE} serial:${DEVICESERIALNUMBER} mediaownerid:${MEDIAOWNERCUSTOMERID}" + play_playlist +elif [ -n "$ASIN" ] ; then + set_var + echo "playing PRIME playlist ${ASIN}" + play_prime_playlist +elif [ -n "$SEEDID" ] ; then + set_var + echo "playing PRIME station ${SEEDID}" + play_prime_station +elif [ -n "$HIST" ] ; then + set_var + echo "playing PRIME historical queue ${HIST}" + play_prime_hist_queue +elif [ -n "$QUEUE" ]; then + set_var + echo "queue info for dev:${DEVICE} type:${DEVICETYPE} serial:${DEVICESERIALNUMBER}" + show_queue +elif [ -n "$LIST" ] ; then + ATTR="accountName" + echo "the following devices exist in your account:" + grep ${ATTR}\| ${DEVTXT} | sed "s/^.*${ATTR}|//" | sed 's/ /_/g' +else + echo "no alexa command received" +fi + +if [ -n "$LOGOFF" ] ; then + echo "logout option present, logging off ..." + log_off +fi