From d267c30fef64f5c32d1025690baae73327cbc150 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 2 Sep 2021 15:19:46 +0200 Subject: [PATCH] Fixed TuneIn support --- README.md | 8 ++-- alexa_remote_control.sh | 85 ++++++++++++++++++++++++++++++----- alexa_remote_control_plain.sh | 12 +++-- 3 files changed, 87 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index fc75d31..e8272c0 100755 --- a/README.md +++ b/README.md @@ -22,9 +22,7 @@ DEVICEVOLNAME - a list of device names with specific volume settings (space se DEVICEVOLSPEAK - a list of speak volume levels - matching the devices above DEVICEVOLNORMAL - a list of normal volume levels- matching the devices above (current playing volume takes precedence for normal volume) -USE_ANNOUNCEMENT_FOR_SPEAK - Announcements can be made to multiple devices, while - regular SPEAK cannot but the announcement feature has - to be turned on for those devices. Also supports SSML! + ``` You will very likely want to set the language to: ``` @@ -41,9 +39,11 @@ alexa-remote-control [-d |ALL] -e ',automation:'',sound:, - textcommand:'' + textcommand:'', + playmusic::'' -b : connect/disconnect/list bluetooth device + -c : list 'playmusic' channels -q : query queue -n : query notifications -r : play tunein radio diff --git a/alexa_remote_control.sh b/alexa_remote_control.sh index d5bb4df..4b9694c 100755 --- a/alexa_remote_control.sh +++ b/alexa_remote_control.sh @@ -67,6 +67,9 @@ # (thanks to Ingo Fischer) # 2021-05-27: v0.18 complete rework of sequence commands especially for TTS # Announcement feature is no longer required due to inconsistent SSML handling +# 2021-09-02: v0.19 Playing TuneIn works again using new entertainment API endpoint +# Added playmusic (Alexa.Music.PlaySearchPhrase) as command, for available channels use "-c" +# Note: playmusic is not multi-room capable, doing so might lead to unexpected results # ### # @@ -74,6 +77,7 @@ # - requires cURL for web communication # - (GNU) sed and awk for extraction # - jq as command line JSON parser (optional for the fancy bits) +# - base64 for B64 encoding (make sure "-w 0" option is available on your platform) # - oathtool as OATH one-time password tool (optional for two-factor authentication) # ########################################## @@ -167,7 +171,10 @@ TTS="" UTTERANCE="" SEQUENCECMD="" SEQUENCEVAL="" +SEARCHPHRASE="" +PROVIDERID="" STATIONID="" +CHANNEL="" QUEUE="" SONG="" ALBUM="" @@ -188,15 +195,17 @@ NOTIFICATIONS="" usage() { echo "$0 [-d |ALL] -e > |" - echo " -b [list|<\"AA:BB:CC:DD:EE:FF\">] | -q | -n | -r <\"station name\"|stationid> |" + echo " -b [list|<\"AA:BB:CC:DD:EE:FF\">] | -q | -n | -r <\"station name\"|stationId> |" echo " -s | -t | -u | -v | -w |" echo " -i | -p | -P | -S | -a | -m [device_1 .. device_X] | -lastalexa | -lastcommand | -z | -l | -h" echo echo " -e : run command, additional SEQUENCECMDs:" echo " weather,traffic,flashbriefing,goodmorning,singasong,tellstory," echo " speak:'',automation:'',sound:," - echo " textcommand:''" + echo " textcommand:''," + echo " playmusic::''" echo " -b : connect/disconnect/list bluetooth device" + echo " -c : list 'playmusic' channels" echo " -q : query queue" echo " -n : query notifications" echo " -r : play tunein radio" @@ -222,7 +231,7 @@ usage() while [ "$#" -gt 0 ] ; do case "$1" in --version) - echo "v0.18" + echo "v0.19" exit 0 ;; -d) @@ -341,6 +350,9 @@ while [ "$#" -gt 0 ] ; do -a) LIST="true" ;; + -c) + CHANNEL="true" + ;; -i) TYPE="IMPORTED" ;; @@ -455,6 +467,13 @@ case "$COMMAND" in tellstory) SEQUENCECMD='Alexa.TellStory.Play' ;; + playmusic:*) + SEQUENCECMD='Alexa.Music.PlaySearchPhrase' + PROVIDERID=${COMMAND#*:} + PROVIDERID=${PROVIDERID%:*} + SEQUENCEVAL=',\"musicProviderId\":\"'${PROVIDERID}'\",' + SEARCHPHRASE=$(echo ${COMMAND##*:} | sed s/\"/\'/g) + ;; "") ;; *) @@ -626,6 +645,26 @@ list_devices() jq -r '.devices[].accountName' ${DEVLIST} } +# +# sanitize search phrase +# ARG1 - sequence command (e.g. Alexa.Music.PlaySearchPhrase) +# ARG2 - musicProviderID ( TUNEIN, AMASON_MUSIC, CLOUDPLAYER, SPOTIFY, APPLE_MUSIC, DEEZER, I_HEART_RADIO ) +# ARG3 - search phrase +# +sanitize_search() +{ + if [ -n "$1" -a -n "$2" -a -n "$3" ] ; then + JSON='{"type":"'${1}'","operationPayload":"{\"locale\":\"'${TTS_LOCALE}'\",\"musicProviderId\":\"'${2}'\",\"searchPhrase\":\"'${3}'\"}"}' + else + JSON='{"type":"'${SEQUENCECMD}'","operationPayload":"{\"locale\":\"'${TTS_LOCALE}'\",\"musicProviderId\":\"'${PROVIDERID}'\",\"searchPhrase\":\"'${SEARCHPHRASE}'\"}"}' + fi + + ${CURL} ${OPTS} -s -b ${COOKIE} -A "${BROWSER}" -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}.*csrf[ \\s\\t]+/ {print \$7}" ${COOKIE})" -X POST -d "${JSON}" \ + "https://${ALEXA}/api/behaviors/operation/validate" | jq -r '.operationPayload.sanitizedSearchPhrase' +} + # # build node_to_execute string # ARG1 - SEQUENCECMD @@ -684,6 +723,14 @@ if [ -n "${SEQUENCECMD}" ] ; then VOLUMEPRENODESTOEXECUTE='' VOLUMEPOSTNODESTOEXECUTE='' NODESTOEXECUTE='' + + # sanitize search phrase + if [ -n "${SEARCHPHRASE}" -a -n "${PROVIDERID}" ] ; then + SEQUENCEVAL=${SEQUENCEVAL}'\"searchPhrase\":\"'${SEARCHPHRASE}'\",\"sanitizedSearchPhrase\":\"'$(sanitize_search)'\"' + fi + + # iterate over member devices if target is multiroom + # !!! this is no true multi-room - it just tries to play on every member device in parallel !!! if [ "${DEVICEFAMILY}" = "WHA" ] ; then MEMBERDEVICESERIALS=$(jq --arg device "${DEVICE}" -r '.devices[] | select(.accountName == $device) | .clusterMembers[]' ${DEVLIST}) for DEVICESERIALNUMBER in $MEMBERDEVICESERIALS ; do @@ -691,7 +738,8 @@ if [ -n "${SEQUENCECMD}" ] ; then NODESTOEXECUTE=$(add_node "$(node)" "${NODESTOEXECUTE}") # add volume setting per device - the WHA volume is unrelyable - if [ $SPEAKVOL -gt 0 -o -n "${DEVICEVOLSPEAK}" ] ; then + # don't set volume if Alexa.Music.PlaySearchPhrase is used + if [ \( $SPEAKVOL -gt 0 -o -n "${DEVICEVOLSPEAK}" \) -a "${SEQUENCECMD}" != "Alexa.Music.PlaySearchPhrase" ] ; then DEVICE=$(jq --arg device "${DEVICESERIALNUMBER}" -r '.devices[] | select(.serialNumber == $device) | .accountName' ${DEVLIST}) get_volumes VOLUMEPRENODESTOEXECUTE=$(add_node $(node Alexa.DeviceControls.Volume ',\"value\":\"'${SVOL}'\"') ${VOLUMEPRENODESTOEXECUTE}) @@ -705,7 +753,9 @@ if [ -n "${SEQUENCECMD}" ] ; then fi else NODESTOEXECUTE=$(add_node "$(node)" "${NODESTOEXECUTE}") - if [ $SPEAKVOL -gt 0 -o -n "${DEVICEVOLSPEAK}" ] ; then + + # don't set volume if Alexa.Music.PlaySearchPhrase is used + if [ \( $SPEAKVOL -gt 0 -o -n "${DEVICEVOLSPEAK}" \) -a "${SEQUENCECMD}" != "Alexa.Music.PlaySearchPhrase" ] ; then get_volumes VOLUMEPRENODESTOEXECUTE=$(add_node $(node Alexa.DeviceControls.Volume ',\"value\":\"'${SVOL}'\"') ${VOLUMEPRENODESTOEXECUTE}) VOLUMEPOSTNODESTOEXECUTE=$(add_node $(node Alexa.DeviceControls.Volume ',\"value\":\"'${VOL}'\"') ${VOLUMEPOSTNODESTOEXECUTE}) @@ -730,7 +780,7 @@ if [ -n "${SEQUENCECMD}" ] ; then -H "csrf: $(awk "\$0 ~/.${AMAZON}.*csrf[ \\s\\t]+/ {print \$7}" ${COOKIE})" -X POST -d @"${TMP}/.alexa.cmd" \ "https://${ALEXA}/api/behaviors/preview" -# rm -f "${TMP}/.alexa.cmd" + rm -f "${TMP}/.alexa.cmd" else ${CURL} ${OPTS} -s -b ${COOKIE} -A "${BROWSER}" -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}"\ @@ -744,10 +794,12 @@ fi # play_radio() { -${CURL} ${OPTS} -s -b ${COOKIE} -A "${BROWSER}" -H "DNT: 1" -H "Connection: keep-alive" -L\ + JSON='{"contentToken":"music:'$(echo '["music/tuneIn/stationId","'${STATIONID}'"]|{"previousPageId":"TuneIn_SEARCH"}'| base64 -w 0| base64 -w 0 )'"}' + + ${CURL} ${OPTS} -s -b ${COOKIE} -A "${BROWSER}" -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}.*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}" + -H "csrf: $(awk "\$0 ~/.${AMAZON}.*csrf[ \\s\\t]+/ {print \$7}" ${COOKIE})" -X PUT -d "${JSON}" \ + "https://${ALEXA}/api/entertainment/v1/player/queue?deviceSerialNumber=${DEVICESERIALNUMBER}&deviceType=${DEVICETYPE}" } # @@ -897,6 +949,14 @@ show_queue() "https://${ALEXA}/api/np/queue?deviceSerialNumber=${DEVICESERIALNUMBER}&deviceType=${DEVICETYPE}" | jq '.' } +get_music_channels() +{ + ${CURL} ${OPTS} -s -b ${COOKIE} -A "${BROWSER}" -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}.*csrf[ \\s\\t]+/ {print \$7}" ${COOKIE})" -X GET \ + "https://${ALEXA}/api/behaviors/entities?skillId=amzn1.ask.1p.music" | jq -r '.[] | select( .supportedProperties[] == "Alexa.Music.PlaySearchPhrase" ) | "\(.id) - \(.displayName) \(.description)"' +} + # # device specific SPEAKVOL/NORMALVOL (sets SVOL/VOL) # @@ -1103,7 +1163,7 @@ rm -f ${TMP}/.alexa.*.list rm -f ${TMP}/.alexa.volume.* } -if [ -z "$LASTALEXA" -a -z "$LASTCOMMAND" -a -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 "$NOTIFICATIONS" -a -z "$LIST" -a -z "$COMMAND" -a -z "$STATIONID" -a -z "$SONG" -a -z "$GETVOL" -a -n "$LOGOFF" ] ; then +if [ -z "$LASTALEXA" -a -z "$LASTCOMMAND" -a -z "$CHANNEL" -a -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 "$NOTIFICATIONS" -a -z "$LIST" -a -z "$COMMAND" -a -z "$STATIONID" -a -z "$SONG" -a -z "$GETVOL" -a -n "$LOGOFF" ] ; then echo "only logout option present, logging off ..." log_off exit 0 @@ -1139,6 +1199,11 @@ if [ -n "$LOGIN" ] ; then exit 0 fi +if [ -n "$CHANNEL" ] ; then + get_music_channels + exit 0 +fi + if [ -n "$COMMAND" -o -n "$QUEUE" -o -n "$NOTIFICATIONS" -o -n "$GETVOL" ] ; then if [ "${DEVICE}" = "ALL" ] ; then for DEVICE in $( jq -r '.devices[] | select( ( .deviceFamily == "ECHO" or .deviceFamily == "KNIGHT" or .deviceFamily == "ROOK" or .deviceFamily == "WHA" ) and .online == true ) | .accountName' ${DEVLIST} | sed -r 's/ /%20/g') ; do diff --git a/alexa_remote_control_plain.sh b/alexa_remote_control_plain.sh index d4d2cfe..ba0155e 100755 --- a/alexa_remote_control_plain.sh +++ b/alexa_remote_control_plain.sh @@ -4,6 +4,7 @@ # alex(at)loetzimmer.de # # 2021-01-28: v0.17c (for updates see http://blog.loetzimmer.de/2017/10/amazon-alexa-hort-auf-die-shell-echo.html) +# 2021-09-02: v0.17d includes fixes for playing tunein (base64 required) # # !!! THIS IS THE FINAL VERSION !!! # @@ -15,6 +16,7 @@ # (no BASHisms were used, should run with any shell) # - requires cURL for web communication # - (GNU) sed and awk for extraction +# - base64 for B64 encoding (make sure "-w 0" option is available on your platform) # - oathtool as OATH one-time password tool (optional for two-factor authentication) # ########################################## @@ -148,7 +150,7 @@ usage() while [ "$#" -gt 0 ] ; do case "$1" in --version) - echo "v0.17a" + echo "v0.17d" exit 0 ;; -d) @@ -719,10 +721,12 @@ fi # play_radio() { -${CURL} ${OPTS} -s -b ${COOKIE} -A "${BROWSER}" -H "DNT: 1" -H "Connection: keep-alive" -L\ + JSON='{"contentToken":"music:'$(echo '["music/tuneIn/stationId","'${STATIONID}'"]|{"previousPageId":"TuneIn_SEARCH"}'| base64 -w 0| base64 -w 0 )'"}' + + ${CURL} ${OPTS} -s -b ${COOKIE} -A "${BROWSER}" -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}.*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}" + -H "csrf: $(awk "\$0 ~/.${AMAZON}.*csrf[ \\s\\t]+/ {print \$7}" ${COOKIE})" -X PUT -d "${JSON}" \ + "https://${ALEXA}/api/entertainment/v1/player/queue?deviceSerialNumber=${DEVICESERIALNUMBER}&deviceType=${DEVICETYPE}" } #