Repository: thorsten-gehrig/alexa-remote-control Branch: master Commit: b7077400cf18 Files: 4 Total size: 98.4 KB Directory structure: gitextract_czte_yi_/ ├── LICENSE ├── README.md ├── alexa_remote_control.sh └── alexa_remote_control_plain.sh ================================================ FILE CONTENTS ================================================ ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: README.md ================================================ # alexa-remote-control control Amazon Alexa from command Line The settings can now be controlled via environment variables. ``` BROWSER - the User-Agent your browser sends in the request header AMAZON - your Amazon domain ALEXA - the URL you would use for the Alexa Web App CURL - location of your cURL binary OPTS - any cURL options you require TMP - location of the temp dir SPEAKVOL - the volume for speak messages ( if set to 0, volume levels are left untouched) NORMALVOL - if no current playing volume can be determined, fall back to normal volume VOLMAXAGE - max. age in minutes before volume is re-read from API DEVICEVOLNAME - a list of device names with specific volume settings (space separated) 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) REFRESH_TOKEN - the new preference over EMAIL/PASSWORD can be obtained here: https://github.com/adn77/alexa-cookie-cli ``` ``` alexa-remote-control [-d |ALL] -e > | -b [list|<"AA:BB:CC:DD:EE:FF">] | -q | -n | -r <"station name"|stationid> | -s | -t | -u | -v | -w | -i | -p | -P | -S | -a | -z | -l | -h | -m [device_1 .. device_X] | -lastalexa | -lastcommand -e : run command, additional SEQUENCECMDs: weather,traffic,flashbriefing,goodmorning,singasong,tellstory, speak:'',automation:'',sound:, textcommand:'', playmusic::'' -b : connect/disconnect/list bluetooth device -c : list 'playmusic' channels -q : query queue -n : query notifications -r : play tunein radio -s : play library track/library album -t : play Prime playlist -u : play Prime station -v : play Prime historical queue -w : play library playlist -i : list imported library tracks -p : list purchased library tracks -P : list Prime playlists -S : list Prime stations -a : list available devices -m : delete multiroom and/or create new multiroom containing devices -lastalexa : print device that received the last voice command -lastcommand : print last voice command or last voice command of specific device -login : Logs in, without further command (downloads cookie) -z : print current volume level -l : logoff -h : help ``` Login via REFRESH_TOKEN ---- The Alexa-App way of logging in is using a REFRESH_TOKEN which allows for obtaining the session cookies. This replaces EMAIL/PASSWORD/MFA so those will not be exposed in any scripts anymore. For convenience I created a binary, ready to run: https://github.com/adn77/alexa-cookie-cli https://blog.loetzimmer.de/2021/09/alexa-remote-control-shell-script.html ================================================ FILE: alexa_remote_control.sh ================================================ #!/bin/sh # # Amazon Alexa Remote Control # alex(at)loetzimmer.de # # 2017-10-10: v0.1 initial release # 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 # 2017-12-18: v0.7c fixed US version # 2017-12-19: v0.7d fixed AWK csrf extraction on some systems # 2017-12-20: v0.7e moved get_devlist after check_status # 2018-01-08: v0.7f added echo-show to ALL group, TuneIn station can now be up to 6 digits # 2018-01-08: v0.8 added bluetooth list function # 2018-01-10: v0.8a abort when login was unsuccessful # 2018-01-25: v0.8b added echo-spot to ALL group # 2018-01-28: v0.8c added configurable browser string # 2018-02-17: v0.8d no need to write the cookie file on every "check_status" # 2018-02-27: v0.8e added "lastalexa" option for HA-Bridge to send its command to a specific device # (Markus Wennesheimer: https://wennez.wordpress.com/light-on-with-alexa-for-each-room/) # 2018-02-27: v0.9 unsuccessful logins will now give a short info how to debug the login # 2018-03-09: v0.9a workaround for login problem, force curl to use http1.1 # 2018-05-17: v0.9b update browser string and accept language # 2018-05-23: v0.9c update accept language (again) # 2018-06-12: v0.10 introducing TTS and more # (thanks to Michael Geramb and his openHAB2 Amazon Echo Control binding) # https://github.com/openhab/openhab2-addons/tree/master/addons/binding/org.openhab.binding.amazonechocontrol # (thanks to Ralf Otto for implementing this feature in this script) # 2018-06-13: v0.10a added album play of imported library # 2018-06-18: v0.10b added Alexa routine execution # 2019-01-22: v0.11 added repeat command, added environment variable parsing # 2019-02-03: v0.11a fixed string escape for automation and speak commands # 2019-02-10: v0.12 added "-d ALL" to the plain version, lastalexa now checks for SUCCESS activityStatus # 2019-02-14: v0.12a reduced the number of replaced characters for TTS and automation # 2019-06-18: v0.12b fixed CSRF # 2019-06-28: v0.12c properly fixed CSRF # 2019-07-08: v0.13 added support for Multi-Factor Authentication # (thanks to rich-gepp https://github.com/rich-gepp) # 2019-08-05: v0.14 added Volume setting via routine, and $SPEAKVOL # 2019-11-18: v0.14a download 200 routines instead of only the first 20 # 2019-12-23: v0.14b Trigger routines by either utterance or routine name # 2019-12-30: v0.15 re-worked the volume setting for TTS commands # 2020-01-03: v0.15a introduce some proper "get_volume" # 2020-01-08: v0.15b cleaned merge errors # 2020-02-03: v0.15c SPEAKVOL of 0 leaves the volume setting untouched # 2020-02-09: v0.16 TTS to Multiroom groups via USE_ANNOUNCEMENT_FOR_SPEAK + SSML for TTS # (!!! requires Announcement feature to be enabled in each device !!!) # 2020-02-09: v0.16a added sound library - only very few sounds are actually supported # ( https://developer.amazon.com/en-US/docs/alexa/custom-skills/ask-soundlibrary.html ) # 2020-06-15: v0.16b added "lastcommand" option # (thanks to Trinitus01 https://github.com/trinitus01) # 2020-07-07: v0.16c fixed NORMALVOL if USE_ANNOUNCEMENT_FOR_SPEAK is set # 2020-12-12: v0.17 added textcommand which lets you send anything via CLI you would otherwise say to Alexa # ( https://github.com/thorsten-gehrig/alexa-remote-control/issues/108 ) # 2020-12-12: v0.17a sounds now benefit from SPEAKVOL # fixed TuneIn IDs to also play podcasts # 2021-01-28: v0.17b fixed new API endpoint for automations # (thanks to Michael Winkler) # 2021-01-28: v0.17c simplified volume detection using new DeviceVolumes endpoint # (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 # 2021-09-13: v0.20 implemented device registration refresh_token cookie exchange flow as an alternative # to logging in # 2021-09-15: v0.20a optimized speak commands to use less JQ. This is useful in low-resource environments # 2021-10-07: v0.20b fixed different cookie naming for amazon.com # 2021-11-16: v0.20c fixed AlexaApp device selection: since they're all called "This Device" use corresponding # line in /tmp/.alexa.devicelist.txt, e.g.: -d "This Device=A2TF17PFR55MTB=ce0123456789abcdef01=VOX" # -lastalexa now returns this string. Make sure to put the device in double quotes! # 2022-02-04: v0.20d minor volume fix (write volume to volume cache when volume is changed) # 2022-06-29: v0.20e removed call to jq's strptime function, replaced with bash function using 'date' to convert to epoch # 2024-01-29: v0.21 removed legacy login methods as they were no longer working # implemented new API calls for -lastalexa and -lastcommand # there is now an OS-type switch that hopefully handles OSX and BSD date creation # 2024-01-31: v0.21a trying all different date options which come to mind (first working wins) # 2024-02-01: v0.21b changed the output of -lastalexa back to the output of devicelist.txt # 2024-04-06: v0.22 changed the date calculation once again, now the date processing ignores the actual cookie validity # and simply sets it to "now + COOKIE_LIFETIME" # 2025-11-07: v0.23 /api/bootstrap is gone, switched to /api/customer-status # (thanks once again to Ingo Fischer) # ### # # (no BASHisms were used, should run with any shell) # - 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) # ########################################## # this can be obtained by doing the device registration login flow # e.g. from here: https://github.com/adn77/alexa-cookie-cli/ SET_REFRESH_TOKEN='' SET_TTS_LOCALE='de-DE' SET_AMAZON='amazon.de' #SET_AMAZON='amazon.com' SET_ALEXA='alexa.amazon.de' #SET_ALEXA='pitangui.amazon.com' # cURL binary SET_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 # --http1.1 : cURL defaults to HTTP/2 on HTTPS connections if available SET_OPTS='--compressed --http1.1' #SET_OPTS='-k --compressed --http1.1' # browser identity SET_BROWSER='Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:1.0) bash-script/1.0' #SET_BROWSER='Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:65.0) Gecko/20100101 Firefox/65.0' # jq binary SET_JQ='/usr/bin/jq' # tmp path SET_TMP="/tmp" # Volume for speak commands (a SPEAKVOL of 0 leaves the volume settings untouched) SET_SPEAKVOL="0" # if no current playing volume can be determined, fall back to normal volume SET_NORMALVOL="10" # Device specific volumes (overriding the above) # SET_DEVICEVOLNAME="EchoDot2ndGen Echo1stGen" # SET_DEVICEVOLSPEAK="100 30" # SET_DEVICEVOLNORMAL="100 20" SET_DEVICEVOLNAME="" SET_DEVICEVOLSPEAK="" SET_DEVICEVOLNORMAL="" # max. age in minutes before volume is read from API (local cache time) SET_VOLMAXAGE="1" ########################################### # nothing to configure below here # # retrieving environment variables if any are set REFRESH_TOKEN=${REFRESH_TOKEN:-$SET_REFRESH_TOKEN} AMAZON=${AMAZON:-$SET_AMAZON} ALEXA=${ALEXA:-$SET_ALEXA} BROWSER=${BROWSER:-$SET_BROWSER} CURL=${CURL:-$SET_CURL} OPTS=${OPTS:-$SET_OPTS} TTS_LOCALE=${TTS_LOCALE:-$SET_TTS_LOCALE} TMP=${TMP:-$SET_TMP} JQ=${JQ:-$SET_JQ} SPEAKVOL=${SPEAKVOL:-$SET_SPEAKVOL} NORMALVOL=${NORMALVOL:-$SET_NORMALVOL} VOLMAXAGE=${VOLMAXAGE:-$SET_VOLMAXAGE} DEVICEVOLNAME=${DEVICEVOLNAME:-$SET_DEVICEVOLNAME} DEVICEVOLSPEAK=${DEVICEVOLSPEAK:-$SET_DEVICEVOLSPEAK} DEVICEVOLNORMAL=${DEVICEVOLNORMAL:-$SET_DEVICEVOLNORMAL} COOKIE="${TMP}/.alexa.cookie" DEVLIST="${TMP}/.alexa.devicelist" COOKIE_LIFETIME=$(( 24 * 60 * 60 )) # default lifetime of one day before revalidation LIST="" LOGOFF="" COMMAND="" TTS="" UTTERANCE="" SEQUENCECMD="" SEQUENCEVAL="" SEARCHPHRASE="" PROVIDERID="" STATIONID="" CHANNEL="" QUEUE="" SONG="" ALBUM="" ARTIST="" TYPE="" ASIN="" SEEDID="" HIST="" LEMUR="" CHILD="" PLIST="" BLUETOOTH="" LASTALEXA="" LASTCOMMAND="" GETVOL="" NOTIFICATIONS="" usage() { echo "$0 [-d |ALL] -e > |" 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 " 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" echo " -s : play library track/library album" 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 " -lastalexa : print device that received the last voice command" echo " -lastcommand : print last voice command or last voice command of specific device" echo " -z : print current volume level" echo " -login : Logs in, without further command" echo " -l : logoff" echo " -h : help" } while [ "$#" -gt 0 ] ; do case "$1" in --version) echo "v0.23" exit 0 ;; -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##p[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 if [ "${2#-}" = "${2}" -a -n "$2" ] ; then ALBUM=$2 ARTIST=$SONG shift fi ;; -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 ;; -login) LOGIN="true" ;; -l) LOGOFF="true" ;; -a) LIST="true" ;; -c) CHANNEL="true" ;; -i) TYPE="IMPORTED" ;; -p) TYPE="PURCHASES" ;; -P) PRIME="prime-playlist-browse-nodes" ;; -S) PRIME="prime-sections" ;; -q) QUEUE="true" ;; -n) NOTIFICATIONS="true" ;; -lastalexa) LASTALEXA="true" ;; -lastcommand) LASTCOMMAND="true" ;; -z) GETVOL="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"}' ;; repeat) COMMAND='{"type":"RepeatCommand","repeat":true}' ;; vol:*) VOL=${COMMAND##*:} # volume as integer! if [ $VOL -le 100 -a $VOL -ge 0 ] ; then SEQUENCECMD='Alexa.DeviceControls.Volume' SEQUENCEVAL=',\"value\":\"'${VOL}'\"' else echo "ERROR: volume should be an integer between 0 and 100" usage exit 1 fi ;; textcommand:*) SEQUENCECMD='Alexa.TextCommand\",\"skillId\":\"amzn1.ask.1p.tellalexa' SEQUENCEVAL=$(echo ${COMMAND##textcommand:} | sed s/\"/\'/g) SEQUENCEVAL=',\"text\":\"'${SEQUENCEVAL}'\"' ;; speak:*) TTS=$(echo ${COMMAND##speak:} | sed s/\"/\'/g) TTS=',\"textToSpeak\":\"'${TTS}'\"' SEQUENCECMD='Alexa.Speak' SEQUENCEVAL=$TTS ;; sound:*) SEQUENCECMD='Alexa.Sound' SEQUENCEVAL=',\"soundStringId\":\"'${COMMAND##sound:}'\"' ;; automation:*) SEQUENCECMD='automation' UTTERANCE=$(echo ${COMMAND##automation:} | sed -r 's/["\\]/ /g') ;; weather) SEQUENCECMD='Alexa.Weather.Play' ;; traffic) SEQUENCECMD='Alexa.Traffic.Play' ;; flashbriefing) SEQUENCECMD='Alexa.FlashBriefing.Play' ;; goodmorning) SEQUENCECMD='Alexa.GoodMorning.Play' ;; singasong) SEQUENCECMD='Alexa.SingASong.Play' ;; tellstory) SEQUENCECMD='Alexa.TellStory.Play' ;; playmusic:*) SEQUENCECMD='Alexa.Music.PlaySearchPhrase' PROVIDERID=${COMMAND#*:} PROVIDERID=${PROVIDERID%:*} SEQUENCEVAL=',\"musicProviderId\":\"'${PROVIDERID}'\",' SEARCHPHRASE=$(echo ${COMMAND##*:} | sed s/\"/\'/g) ;; "") ;; *) echo "ERROR: unknown command \"${COMMAND}\"!" usage exit 1 ;; esac # # Amazon Login # log_in() { rm -f ${DEVLIST}.json rm -f ${COOKIE} rm -f ${TMP}/.alexa.*.list if [ -z "${REFRESH_TOKEN}" ] ; then echo "Sorry, the very thing this project started with, namely the reverse engineered" echo " login to the Amazon web page does no longer work. The Alexa login page has" echo " been shut down in favor of a much more modern login process." echo echo "Please use the device login process https://github.com/adn77/alexa-cookie-cli" echo " all you need is the 'refreshToken' looking sth. like 'Atnr|...'" exit 1 else now=$(date +%s) exp=$(( now + COOKIE_LIFETIME )) # the date processing ignores the actual cookie validity and simply sets it to "now + COOKIE_LIFETIME" ${CURL} ${OPTS} -s -X POST --data "app_name=Amazon%20Alexa&requested_token_type=auth_cookies&domain=www.${AMAZON}&source_token_type=refresh_token" --data-urlencode "source_token=${REFRESH_TOKEN}" -H "x-amzn-identity-auth-domain: api.${AMAZON}" https://api.${AMAZON}/ap/exchangetoken/cookies |\ ${JQ} -r --arg exp $exp '.response.tokens.cookies | to_entries[] | .key as $domain | .value[] | map_values(if . == true then "TRUE" elif . == false then "FALSE" else . end) | .Expires |= $exp | [(if .HttpOnly=="TRUE" then ("#HttpOnly_" + $domain) else $domain end), "TRUE", .Path, .Secure, .Expires, .Name, .Value] | @tsv' > ${COOKIE} if [ -z "$(grep "\.${AMAZON}.*\sat-" ${COOKIE})" ] ; then echo "ERROR: cookie retrieval with refresh_token didn't work" exit 1 fi fi # # get CSRF # ${CURL} ${OPTS} -s -c ${COOKIE} -b ${COOKIE} -A "${BROWSER}" -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 if [ -z "$(grep "\.${AMAZON}.*\scsrf" ${COOKIE})" ] ; then echo "trying to get CSRF from handlebars" ${CURL} ${OPTS} -s -c ${COOKIE} -b ${COOKIE} -A "${BROWSER}" -H "DNT: 1" -H "Connection: keep-alive" -L\ -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ https://${ALEXA}/templates/oobe/d-device-pick.handlebars > /dev/null fi if [ -z "$(grep "\.${AMAZON}.*\scsrf" ${COOKIE})" ] ; then echo "trying to get CSRF from devices-v2" ${CURL} ${OPTS} -s -c ${COOKIE} -b ${COOKIE} -A "${BROWSER}" -H "DNT: 1" -H "Connection: keep-alive" -L\ -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ https://${ALEXA}/api/devices-v2/device?cached=false > /dev/null fi if [ -z "$(grep "\.${AMAZON}.*\scsrf" ${COOKIE})" ] ; then echo "ERROR: no CSRF cookie received" exit 1 fi } # # get JSON device list # get_devlist() { ${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})"\ "https://${ALEXA}/api/devices-v2/device?cached=false" > ${DEVLIST}.json ${JQ} -r '.devices[] | "\(.accountName)=\(.deviceType)=\(.serialNumber)=\(.deviceFamily)"' ${DEVLIST}.json > ${DEVLIST}.txt ${JQ} -r '.devices[] | select( .appDeviceList | length >0 ) as $p | .appDeviceList[] | "\($p.accountName)=\(.deviceType)=\(.serialNumber)=\($p.deviceFamily)"' ${DEVLIST}.json >> ${DEVLIST}.txt ${JQ} -r '.devices[] | select(.deviceFamily == "WHA") | "\(.accountName)=\(.clusterMembers[])"' ${DEVLIST}.json > ${DEVLIST}_wha.txt } check_status() { # # returns the current authentication state (HTTP/200) # AUTHSTATUS=$(${CURL} ${OPTS} -s -w "%{http_code}" -b ${COOKIE} -A "${BROWSER}" -H "DNT: 1" -H "Connection: keep-alive" -L https://${ALEXA}/api/customer-status -o ${TMP}/.alexa.authstatus.json) case $AUTHSTATUS in 200) MEDIAOWNERCUSTOMERID=$(${CURL} ${OPTS} -s -b ${COOKIE} -A "${BROWSER}" -H "DNT: 1" -H "Connection: keep-alive" -L https://${ALEXA}/api/users/me | ${JQ} -r '.id') return 1 ;; 401|403) return 0 ;; *) ;; esac echo "ERROR: /api/customer-status returned unexpected HTTP/${AUTHSTATUS}" } # # set device specific variables from JSON device list # set_var() { 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 -n "setting default device to: " DEVICE=$(grep -m 1 -E "ECHO|KNIGHT|ROOK" ${DEVLIST}.txt | cut -d'=' -f1) echo ${DEVICE} fi DEVICESERIALNUMBER=$(grep -m 1 "${DEVICE}" ${DEVLIST}.txt) DEVICESERIALNUMBER=${DEVICESERIALNUMBER#*=} DEVICEFAMILY=${DEVICESERIALNUMBER##*=} DEVICETYPE=${DEVICESERIALNUMBER%%=*} DEVICESERIALNUMBER=${DEVICESERIALNUMBER#*=} DEVICESERIALNUMBER=${DEVICESERIALNUMBER%=*} # customerId is now retrieved from the logged in user # the customerId in the device list is always from the user registering the device initially # MEDIAOWNERCUSTOMERID=$(${JQ} --arg device "${DEVICE}" -r '.devices[] | select(.accountName == $device) | .deviceOwnerCustomerId' ${DEVLIST}.json) if [ -z "${DEVICESERIALNUMBER}" ] ; then echo "ERROR: unkown device dev:${DEVICE}" exit 1 fi } # # list available devices from JSON device list # list_devices() { ${JQ} -r '.devices[].accountName' ${DEVLIST}.json } # # 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 # ARG2 - SEQUENCEVAL # node() { if [ -n "$1" -a -n "$2" ] ; then echo '{\"@type\":\"com.amazon.alexa.behaviors.model.OpaquePayloadOperationNode\",\"type\":\"'${1}'\",\"operationPayload\":{\"deviceType\":\"'${DEVICETYPE}'\",\"deviceSerialNumber\":\"'${DEVICESERIALNUMBER}'\",\"customerId\":\"'${MEDIAOWNERCUSTOMERID}'\",\"locale\":\"'${TTS_LOCALE}'\"'${2}'}}' else echo '{\"@type\":\"com.amazon.alexa.behaviors.model.OpaquePayloadOperationNode\",\"type\":\"'${SEQUENCECMD}'\",\"operationPayload\":{\"deviceType\":\"'${DEVICETYPE}'\",\"deviceSerialNumber\":\"'${DEVICESERIALNUMBER}'\",\"customerId\":\"'${MEDIAOWNERCUSTOMERID}'\",\"locale\":\"'${TTS_LOCALE}'\"'${SEQUENCEVAL}'}}' fi } # # create comma separated string # add_node() { if [ -n "$1" ] ; then if [ -n "$2" ] ; then echo ${1}','${2} else echo ${1} fi fi } # # execute command # run_cmd() { if [ -n "${SEQUENCECMD}" ] ; then if [ "${SEQUENCECMD}" = 'automation' ] ; then ${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/v2/automations?limit=200" > "${TMP}/.alexa.automation" AUTOMATION=$(${JQ} --arg utterance "${UTTERANCE}" -r '.[] | select( .triggers[].payload.utterance == $utterance) | .automationId' "${TMP}/.alexa.automation") if [ -z "${AUTOMATION}" ] ; then AUTOMATION=$(${JQ} --arg utterance "${UTTERANCE}" -r '.[] | select( .name == $utterance) | .automationId' "${TMP}/.alexa.automation") if [ -z "${AUTOMATION}" ] ; then echo "ERROR: no such utterance '${UTTERANCE}' in Alexa routines" rm -f "${TMP}/.alexa.automation" exit 1 fi fi SEQUENCE=$(${JQ} --arg automation "${AUTOMATION}" -r -c '.[] | select( .automationId == $automation) | .sequence' "${TMP}/.alexa.automation" | sed 's/"/\\"/g' | sed "s/ALEXA_CURRENT_DEVICE_TYPE/${DEVICETYPE}/g" | sed "s/ALEXA_CURRENT_DSN/${DEVICESERIALNUMBER}/g" | sed "s/ALEXA_CUSTOMER_ID/${MEDIAOWNERCUSTOMERID}/g") rm -f "${TMP}/.alexa.automation" ALEXACMD='{"behaviorId":"'${AUTOMATION}'","sequenceJson":"'${SEQUENCE}'","status":"ENABLED"}' else 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=$(grep "${DEVICE}" ${DEVLIST}_wha.txt | cut -d'=' -f 2) for DEVICESERIALNUMBER in $MEMBERDEVICESERIALS ; do DEVICETYPE=$(grep "${DEVICESERIALNUMBER}" ${DEVLIST}.txt | cut -d'=' -f 2) NODESTOEXECUTE=$(add_node "$(node)" "${NODESTOEXECUTE}") # if SequenceCommand is "Alexa.DeviceControls.Volume" we have to adjust the local volume cache if [ "$SEQUENCECMD" = "Alexa.DeviceControls.Volume" ] ; then VOL=${SEQUENCEVAL%\\\"} VOL=${VOL##*\\\"} if [ $VOL -gt 0 ] ; then echo $VOL false > "${TMP}/.alexa.volume.${DEVICESERIALNUMBER}" else echo 0 true > "${TMP}/.alexa.volume.${DEVICESERIALNUMBER}" fi # add volume setting per device - the WHA volume is unrelyable # don't set volume if Alexa.Music.PlaySearchPhrase is used elif [ \( $SPEAKVOL -gt 0 -o -n "${DEVICEVOLSPEAK}" \) -a "${SEQUENCECMD}" != "Alexa.Music.PlaySearchPhrase" ] ; then DEVICE=$(grep "${DEVICESERIALNUMBER}" ${DEVLIST}.txt | cut -d'=' -f 1) get_volumes VOLUMEPRENODESTOEXECUTE=$(add_node $(node Alexa.DeviceControls.Volume ',\"value\":\"'${SVOL}'\"') ${VOLUMEPRENODESTOEXECUTE}) VOLUMEPOSTNODESTOEXECUTE=$(add_node $(node Alexa.DeviceControls.Volume ',\"value\":\"'${VOL}'\"') ${VOLUMEPOSTNODESTOEXECUTE}) fi done if [ -z "${NODESTOEXECUTE}" ] ; then echo "No clusterMembers found for command: ${COMMAND} on dev:${DEVICE} type:${DEVICETYPE} serial:${DEVICESERIALNUMBER} family:${DEVICEFAMILY}" return fi else NODESTOEXECUTE=$(add_node "$(node)" "${NODESTOEXECUTE}") if [ "$SEQUENCECMD" = "Alexa.DeviceControls.Volume" ] ; then VOL=${SEQUENCEVAL%\\\"} VOL=${VOL##*\\\"} if [ $VOL -gt 0 ] ; then echo $VOL false > "${TMP}/.alexa.volume.${DEVICESERIALNUMBER}" else echo 0 true > "${TMP}/.alexa.volume.${DEVICESERIALNUMBER}" fi # don't set volume if Alexa.Music.PlaySearchPhrase is used elif [ \( $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}) fi fi if [ -n "${VOLUMEPRENODESTOEXECUTE}" -a -n "${VOLUMEPOSTNODESTOEXECUTE}" ] ; then # execute serially "set_speak_volume" => "sequence_command" => "set_normal_volume" # (each subtask is executed in parallel) ALEXACMD='{"behaviorId":"PREVIEW","sequenceJson":"{\"@type\":\"com.amazon.alexa.behaviors.model.Sequence\",\"startNode\":{\"@type\":\"com.amazon.alexa.behaviors.model.SerialNode\",\"nodesToExecute\":[{\"@type\":\"com.amazon.alexa.behaviors.model.ParallelNode\",\"nodesToExecute\":['${VOLUMEPRENODESTOEXECUTE}']},{\"@type\":\"com.amazon.alexa.behaviors.model.ParallelNode\",\"nodesToExecute\":['${NODESTOEXECUTE}']},{\"@type\":\"com.amazon.alexa.behaviors.model.ParallelNode\",\"nodesToExecute\":['${VOLUMEPOSTNODESTOEXECUTE}']}]}}","status":"ENABLED"}' else # execute in parallel "sequence_command" ALEXACMD='{"behaviorId":"PREVIEW","sequenceJson":"{\"@type\":\"com.amazon.alexa.behaviors.model.Sequence\",\"startNode\":{\"@type\":\"com.amazon.alexa.behaviors.model.ParallelNode\",\"nodesToExecute\":['${NODESTOEXECUTE}']}}","status":"ENABLED"}' fi fi # Due to some weird shell-escape-behavior the command has to be written to a file before POSTing it echo $ALEXACMD > "${TMP}/.alexa.cmd" ${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 @"${TMP}/.alexa.cmd" \ "https://${ALEXA}/api/behaviors/preview" 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}"\ -H "csrf: $(awk "\$0 ~/.${AMAZON}.*csrf[ \\s\\t]+/ {print \$7}" ${COOKIE})" -X POST -d ${COMMAND}\ "https://${ALEXA}/api/np/command?deviceSerialNumber=${DEVICESERIALNUMBER}&deviceType=${DEVICETYPE}" fi } # # play TuneIn radio station # play_radio() { 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 PUT -d "${JSON}" \ "https://${ALEXA}/api/entertainment/v1/player/queue?deviceSerialNumber=${DEVICESERIALNUMBER}&deviceType=${DEVICETYPE}" } # # play library track # play_song() { if [ -z "${ALBUM}" ] ; then JSON="{\"trackId\":\"${SONG}\",\"playQueuePrime\":true}" else JSON="{\"albumArtistName\":\"${ARTIST}\",\"albumName\":\"${ALBUM}\"}" 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/cloudplayer/queue-and-play?deviceSerialNumber=${DEVICESERIALNUMBER}&deviceType=${DEVICETYPE}&mediaOwnerCustomerId=${MEDIAOWNERCUSTOMERID}&shuffle=false" } # # play library playlist # play_playlist() { ${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 "{\"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 "${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 "{\"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 "${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 "{\"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 "${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 "{\"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 "${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/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 "${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/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 "${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/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}.json) if [ "$PARENTID" != "null" ] ; then PARENTDEVICE=$(${JQ} --arg serial ${PARENTID} -r '.devices[] | select(.serialNumber == $serial) | .deviceType' ${DEVLIST}.json) PARENT="&lemurId=${PARENTID}&lemurDeviceType=${PARENTDEVICE}" 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 GET \ "https://${ALEXA}/api/np/player?deviceSerialNumber=${DEVICESERIALNUMBER}&deviceType=${DEVICETYPE}${PARENT}" | ${JQ} '.' ${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/media/state?deviceSerialNumber=${DEVICESERIALNUMBER}&deviceType=${DEVICETYPE}" | ${JQ} '.' ${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/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) # get_volumes() { VOL="" SVOL="" # Not using arrays here in order to be compatible with non-Bash # Get the list position of the current device type IDX=0 for D in $DEVICEVOLNAME ; do if [ "${D}" = "${DEVICE}" ] ; then break; fi IDX=$((IDX+1)) done # get the speak volume at that position C=0 for D in $DEVICEVOLSPEAK ; do if [ $C -eq $IDX ] ; then if [ -n "${D}" ] ; then SVOL=$D ; fi break fi C=$((C+1)) done if [ -z "${SVOL}" ] ; then SVOL=$SPEAKVOL fi # try to retrieve the "currently playing" volume VOLMAXAGE=1 VOL=$(get_volume) if [ -z "${VOL}" ] ; then # get the normal volume of the current device type C=0 for D in $DEVICEVOLNORMAL; do if [ $C -eq $IDX ] ; then VOL=$D break fi C=$((C+1)) done # if the volume is still undefined, use $NORMALVOL if [ -z "${VOL}" ] ; then VOL=$NORMALVOL fi fi } # # current volume level # get_volume() { VOLFILE=$(find "${TMP}/.alexa.volume.${DEVICESERIALNUMBER}" -mmin -${VOLMAXAGE} 2>/dev/null) if [ -z "${VOLFILE}" ] ; then VOL=$(${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/devices/deviceType/dsn/audio/v1/allDeviceVolumes" | ${JQ} -r --arg device "${DEVICESERIALNUMBER}" '.volumes[] | "\(.dsn) \(.speakerVolume) \(.speakerMuted)"') if [ -n "${VOL}" ] ; then # write volume and mute state to file OIFS=$IFS IFS=' ' set -o noglob for LINE in $VOL ; do SERIAL=$(echo "${LINE}" | cut -d' ' -f1) VOLUME=$(echo "${LINE}" | cut -d' ' -f2) MUTED=$(echo "${LINE}" | cut -d' ' -f3) echo "${VOLUME} ${MUTED}" > "${TMP}/.alexa.volume.${SERIAL}" done IFS=$OIFS cut -d' ' -f1 "${TMP}/.alexa.volume.${DEVICESERIALNUMBER}" fi else cut -d' ' -f1 "${TMP}/.alexa.volume.${DEVICESERIALNUMBER}" fi } # # show notifications and alarms # show_notifications() { echo "/api/notifications" ${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/notifications?deviceSerialNumber=${DEVICESERIALNUMBER}&deviceType=${DEVICETYPE}" echo } # # deletes a multiroom device # delete_multiroom() { ${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 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 "${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/lemur/tail" } # # list bluetooth devices # list_bluetooth() { ${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/bluetooth?cached=false" | ${JQ} --arg serial "${DEVICESERIALNUMBER}" -r '.bluetoothStates[] | select(.deviceSerialNumber == $serial) | "\(.pairedDeviceList[]?.address) \(.pairedDeviceList[]?.friendlyName)"' } # # connect bluetooth device # connect_bluetooth() { ${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 "{\"bluetoothDeviceAddress\":\"${BLUETOOTH}\"}"\ "https://${ALEXA}/api/bluetooth/pair-sink/${DEVICETYPE}/${DEVICESERIALNUMBER}" } # # disconnect bluetooth device # disconnect_bluetooth() { ${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/bluetooth/disconnect-sink/${DEVICETYPE}/${DEVICESERIALNUMBER}" } # # get activity CSRF token # get_activity_csrf() { ${CURL} ${OPTS} -s -b ${COOKIE} -A "${BROWSER}" -H "DNT: 1" -H "Connection: keep-alive" -L\ -H "Content-Type: application/json; charset=UTF-8" \ -H "csrf: $(awk "\$0 ~/.${AMAZON}.*csrf[ \\s\\t]+/ {print \$7}" ${COOKIE})" -X GET\ "https://www.${AMAZON}/alexa-privacy/apd/activity?ref=activityHistory" | grep 'meta name="csrf-token" content="' | sed -r 's/^.*content="([^"]+)".*$/\1/g' > ${TMP}/.alexa.activity.csrf } # # get customer history records # get_history() { if ! [ -f ${TMP}/.alexa.activity.csrf ] ; then get_activity_csrf fi RES=$(${CURL} ${OPTS} -s -b ${COOKIE} -A "${BROWSER}" -H "DNT: 1" -H "Connection: keep-alive" -L -w "%{http_code}" \ -H "Content-Type: application/json; charset=UTF-8" -H "anti-csrftoken-a2z: $(cat ${TMP}/.alexa.activity.csrf)" \ -H "csrf: $(awk "\$0 ~/.${AMAZON}.*csrf[ \\s\\t]+/ {print \$7}" ${COOKIE})" -X POST -d '{"previousRequestToken": null}'\ "https://www.${AMAZON}/alexa-privacy/apd/rvh/customer-history-records-v2/?startTime=0&endTime=2147483647000&pageType=VOICE_HISTORY" -o ${TMP}/.alexa.activity.json) # try again in case CSRF timed out if [ $RES -ne 200 ] ; then if [ -z "${try}" ] ; then try=1 rm -f ${TMP}/.alexa.activity.csrf get_history else echo "ERROR: unable to retrieve customer history records" exit 1 fi fi } # # device that sent the last command # last_alexa() { get_history ${JQ} -r '.customerHistoryRecords | sort_by(.timestamp) | reverse | .[0] | .recordKey' ${TMP}/.alexa.activity.json | cut -d'#' -f4 | xargs -i grep -m 1 {} ${DEVLIST}.txt } # # last command or last command of a specific device # last_command() { get_history if [ -z "$DEVICE" ] ; then ${JQ} -r --arg device "$DEVICE" '.customerHistoryRecords | sort_by(.timestamp) | reverse | .[0] | .voiceHistoryRecordItems | map({key: .recordItemType, value: .transcriptText})' ${TMP}/.alexa.activity.json else ${JQ} -r --arg device "$DEVICE" '[ .customerHistoryRecords | sort_by(.timestamp) | reverse | .[] | select( .device.deviceName == $device) ][0] | .voiceHistoryRecordItems | map({key: .recordItemType, value: .transcriptText})' ${TMP}/.alexa.activity.json fi } # # logout # log_off() { ${CURL} ${OPTS} -s -c ${COOKIE} -b ${COOKIE} -A "${BROWSER}" -H "DNT: 1" -H "Connection: keep-alive" -L\ https://${ALEXA}/logout > /dev/null rm -f ${DEVLIST}.json rm -f ${DEVLIST}.txt rm -f ${DEVLIST}_wha.txt rm -f ${COOKIE} rm -f ${TMP}/.alexa.*.list rm -f ${TMP}/.alexa.volume.* } 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 fi if [ ! -f ${COOKIE} ] ; then echo "cookie does not exist. logging in ..." log_in fi check_status if [ $? -eq 0 ] ; then echo "cookie expired, logging in again ..." log_in check_status if [ $? -eq 0 ] ; then echo "log in failed, aborting" exit 1 fi fi if [ ! -f ${DEVLIST}.json -o ! -f ${DEVLIST}.txt ] ; then echo "device list does not exist. downloading ..." get_devlist if [ ! -f ${DEVLIST}.json ] ; then echo "failed to download device list, aborting" exit 1 fi fi if [ -n "$LOGIN" ] ; then echo "logged in" 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}.json | sed -r 's/ /%20/g') ; do set_var if [ -n "$COMMAND" ] ; then echo "sending cmd:${COMMAND} to dev:${DEVICE} type:${DEVICETYPE} serial:${DEVICESERIALNUMBER} customerid:${MEDIAOWNERCUSTOMERID}" run_cmd # in order to prevent a "Rate exceeded" we need to delay the command sleep 1 echo elif [ -n "$GETVOL" ] ; then echo "get volume for dev:${DEVICE} type:${DEVICETYPE} serial:${DEVICESERIALNUMBER}" get_volume elif [ -n "$NOTIFICATIONS" ] ; then echo "notifications info for dev:${DEVICE} type:${DEVICETYPE} serial:${DEVICESERIALNUMBER}" show_notifications else echo "queue info for dev:${DEVICE} type:${DEVICETYPE} serial:${DEVICESERIALNUMBER}" show_queue echo fi done else set_var if [ -n "$COMMAND" ] ; then echo "sending cmd:${COMMAND} to dev:${DEVICE} type:${DEVICETYPE} serial:${DEVICESERIALNUMBER} customerid:${MEDIAOWNERCUSTOMERID}" run_cmd echo elif [ -n "$GETVOL" ] ; then echo "get volume for dev:${DEVICE} type:${DEVICETYPE} serial:${DEVICESERIALNUMBER}" get_volume elif [ -n "$NOTIFICATIONS" ] ; then echo "notifications info for dev:${DEVICE} type:${DEVICETYPE} serial:${DEVICESERIALNUMBER}" show_notifications else echo "queue info for dev:${DEVICE} type:${DEVICETYPE} serial:${DEVICESERIALNUMBER}" show_queue echo fi fi elif [ -n "$LEMUR" ] ; then DEVICESERIALNUMBER=$(${JQ} --arg device "${LEMUR}" -r '.devices[] | select(.accountName == $device and .deviceFamily == "WHA") | .serialNumber' ${DEVLIST}.json) 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 echo fi rm -f ${DEVLIST}.json rm -f ${DEVLIST}.txt rm -f ${DEVLIST}_wha.txt get_devlist elif [ -n "$BLUETOOTH" ] ; then if [ "$BLUETOOTH" = "list" -o "$BLUETOOTH" = "List" -o "$BLUETOOTH" = "LIST" ] ; then if [ "${DEVICE}" = "ALL" ] ; then for DEVICE in $(${JQ} -r '.devices[] | select( .deviceFamily == "ECHO" or .deviceFamily == "KNIGHT" or .deviceFamily == "ROOK" or .deviceFamily == "WHA") | .accountName' ${DEVLIST}.json | sed -r 's/ /%20/g') ; do set_var echo "bluetooth devices for dev:${DEVICE} type:${DEVICETYPE} serial:${DEVICESERIALNUMBER}:" list_bluetooth echo done else set_var echo "bluetooth devices for dev:${DEVICE} type:${DEVICETYPE} serial:${DEVICESERIALNUMBER}:" list_bluetooth echo fi elif [ "$BLUETOOTH" = "null" ] ; then set_var echo "disconnecting dev:${DEVICE} type:${DEVICETYPE} serial:${DEVICESERIALNUMBER} from bluetooth" disconnect_bluetooth echo else set_var echo "connecting dev:${DEVICE} type:${DEVICETYPE} serial:${DEVICESERIALNUMBER} to bluetooth device:${BLUETOOTH}" connect_bluetooth echo 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 "$LIST" ] ; then 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 elif [ -n "$LASTALEXA" ] ; then last_alexa elif [ -n "$LASTCOMMAND" ] ; then last_command else echo "no alexa command received" fi if [ -n "$LOGOFF" ] ; then echo "logout option present, logging off ..." log_off fi ================================================ FILE: alexa_remote_control_plain.sh ================================================ #!/bin/sh # # Amazon Alexa Remote Control (PLAIN shell) # 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 !!! # # Due to JQ being widely available across platforms there is no need to expose oneself to the hacks # required when parsing JSON with BASH. # ### # # (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) # ########################################## SET_EMAIL='amazon_account@email.address' SET_PASSWORD='Very_Secret_Amazon_Account_Password' SET_MFA_SECRET='' # something like: # 1234 5678 9ABC DEFG HIJK LMNO PQRS TUVW XYZ0 1234 5678 9ABC DEFG SET_LANGUAGE='de,en-US;q=0.7,en;q=0.3' #SET_LANGUAGE='en-US' SET_TTS_LOCALE='de-DE' SET_AMAZON='amazon.de' #SET_AMAZON='amazon.com' SET_ALEXA='alexa.amazon.de' #SET_ALEXA='pitangui.amazon.com' # cURL binary SET_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 # --http1.1 : cURL defaults to HTTP/2 on HTTPS connections if available SET_OPTS='--compressed --http1.1' #SET_OPTS='-k --compressed --http1.1' # browser identity SET_BROWSER='Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:1.0) bash-script/1.0' #SET_BROWSER='Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:65.0) Gecko/20100101 Firefox/65.0' # oathtool command line tool SET_OATHTOOL='/usr/bin/oathtool' # tmp path SET_TMP="/tmp" # Volume for speak commands (a SPEAKVOL of 0 leaves the volume settings untouched) SET_SPEAKVOL="0" # if no current playing volume can be determined, fall back to normal volume SET_NORMALVOL="10" # Device specific volumes (overriding the above) SET_DEVICEVOLNAME="EchoDot2ndGen Echo1stGen" SET_DEVICEVOLSPEAK="100 30" SET_DEVICEVOLNORMAL="100 20" ########################################### # nothing to configure below here # # retrieving environment variables if any are set EMAIL=${EMAIL:-$SET_EMAIL} PASSWORD=${PASSWORD:-$SET_PASSWORD} MFA_SECRET=${MFA_SECRET:-$SET_MFA_SECRET} AMAZON=${AMAZON:-$SET_AMAZON} ALEXA=${ALEXA:-$SET_ALEXA} LANGUAGE=${LANGUAGE:-$SET_LANGUAGE} BROWSER=${BROWSER:-$SET_BROWSER} CURL=${CURL:-$SET_CURL} OPTS=${OPTS:-$SET_OPTS} TTS_LOCALE=${TTS_LOCALE:-$SET_TTS_LOCALE} TMP=${TMP:-$SET_TMP} OATHTOOL=${OATHTOOL:-$SET_OATHTOOL} SPEAKVOL=${SPEAKVOL:-$SET_SPEAKVOL} NORMALVOL=${NORMALVOL:-$SET_NORMALVOL} DEVICEVOLNAME=${DEVICEVOLNAME:-$SET_DEVICEVOLNAME} DEVICEVOLSPEAK=${DEVICEVOLSPEAK:-$SET_DEVICEVOLSPEAK} DEVICEVOLNORMAL=${DEVICEVOLNORMAL:-$SET_DEVICEVOLNORMAL} COOKIE="${TMP}/.alexa.cookie" DEVLIST="${TMP}/.alexa.devicelist.json" DEVTXT="${TMP}/.alexa.devicelist.txt" DEVALL="${TMP}/.alexa.devicelist.all" GUIVERSION=0 LIST="" LOGOFF="" COMMAND="" TTS="" SEQUENCECMD="" SEQUENCEVAL="" STATIONID="" QUEUE="" SONG="" ALBUM="" ARTIST="" ASIN="" SEEDID="" HIST="" LEMUR="" CHILD="" PLIST="" BLUETOOTH="" LASTALEXA="" NOTIFICATIONS="" usage() { echo "$0 [-d |ALL] -e > |" echo " -b [list|<\"AA:BB:CC:DD:EE:FF\">] | -q | -n | -r <\"station name\"|stationid> |" echo " -s | -t | -u | -v | -w |" echo " -a | -m [device_1 .. device_X] | -lastalexa | -l | -h" echo echo " -e : run command, additional SEQUENCECMDs:" echo " weather,traffic,flashbriefing,goodmorning,singasong,tellstory," echo " speak:'',sound:," echo " textcommand:''" echo " -b : connect/disconnect/list bluetooth device" echo " -q : query queue" echo " -n : query notifications" echo " -r : play tunein radio" echo " -s : play library track/library album" echo " -t : play Prime playlist" echo " -u : play Prime station" echo " -v : play Prime historical queue" echo " -w : play library playlist" echo " -a : list available devices" echo " -m : delete multiroom and/or create new multiroom containing devices" echo " -lastalexa : print serial number that received the last voice command" echo " -login : Logs in, without further command" echo " -l : logoff" echo " -h : help" } while [ "$#" -gt 0 ] ; do case "$1" in --version) echo "v0.17d" exit 0 ;; -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 if [ "${2#-}" = "${2}" -a -n "$2" ] ; then ALBUM=$2 ARTIST=$SONG shift fi ;; -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 ;; -login) LOGIN="true" ;; -l) LOGOFF="true" ;; -a) LIST="true" ;; -q) QUEUE="true" ;; -n) NOTIFICATIONS="true" ;; -lastalexa) LASTALEXA="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"}' ;; repeat) COMMAND='{"type":"RepeatCommand","repeat":true}' ;; vol:*) VOL=${COMMAND##*:} # volume as integer! if [ $VOL -le 100 -a $VOL -ge 0 ] ; then # COMMAND='{"type":"VolumeLevelCommand","volumeLevel":'${VOL}'}' SEQUENCECMD='Alexa.DeviceControls.Volume' SEQUENCEVAL=',\"value\":\"'${VOL}'\"' else echo "ERROR: volume should be an integer between 0 and 100" usage exit 1 fi ;; textcommand:*) SEQUENCECMD='Alexa.TextCommand\",\"skillId\":\"amzn1.ask.1p.tellalexa' SEQUENCEVAL=$(echo ${COMMAND##textcommand:} | sed -r s/\"/\'/g) SEQUENCEVAL=',\"text\":\"'${SEQUENCEVAL}'\"' ;; speak:*) TTS=$(echo ${COMMAND##*:} | sed -r 's/["\\]/ /g') TTS=',\"textToSpeak\":\"'${TTS}'\"' SEQUENCECMD='Alexa.Speak' SEQUENCEVAL=$TTS ;; sound:*) SEQUENCECMD='Alexa.Sound' SEQUENCEVAL=',\"soundStringId\":\"'${COMMAND##sound:}'\"' ;; weather) SEQUENCECMD='Alexa.Weather.Play' ;; traffic) SEQUENCECMD='Alexa.Traffic.Play' ;; flashbriefing) SEQUENCECMD='Alexa.FlashBriefing.Play' ;; goodmorning) SEQUENCECMD='Alexa.GoodMorning.Play' ;; singasong) SEQUENCECMD='Alexa.SingASong.Play' ;; tellstory) SEQUENCECMD='Alexa.TellStory.Play' ;; "") ;; *) 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 ${DEVALL} rm -f ${COOKIE} # # get first cookie and write redirection target into referer # ${CURL} ${OPTS} -s -D "${TMP}/.alexa.header" -c ${COOKIE} -b ${COOKIE} -A "${BROWSER}" -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 session # ${CURL} ${OPTS} -s -c ${COOKIE} -b ${COOKIE} -A "${BROWSER}" -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" # # add OTP if using MFA # if [ -n "${MFA_SECRET}" ] ; then OTP=$(${OATHTOOL} -b --totp "${MFA_SECRET}") PASSWORD="${PASSWORD}${OTP}" fi # # login with filled out form # !!! referer now contains session in URL # ${CURL} ${OPTS} -s -D "${TMP}/.alexa.header2" -c ${COOKIE} -b ${COOKIE} -A "${BROWSER}" -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 > "${TMP}/.alexa.login" # check whether the login has been successful or exit otherwise if [ -z "$(grep 'Location: https://alexa.*html' ${TMP}/.alexa.header2)" ] ; then echo "ERROR: Amazon Login was unsuccessful. Possibly you get a captcha login screen." echo " Try logging in to https://alexa.${AMAZON} with your browser. In your browser" echo " make sure to have all Amazon related cookies deleted and Javascript disabled!" echo echo " (For more information have a look at ${TMP}/.alexa.login)" echo echo " To avoid issues with captcha, try using Multi-Factor Authentication." echo " To do so, first set up Two-Step Verification on your Amazon account, then" echo " configure this script (or the environment) with your MFA secret." echo " Support for Multi-Factor Authentication requires 'oathtool' to be installed." rm -f ${COOKIE} rm -f "${TMP}/.alexa.header" rm -f "${TMP}/.alexa.header2" rm -f "${TMP}/.alexa.postdata" rm -f "${TMP}/.alexa.postdata2" exit 1 fi # # get CSRF # ${CURL} ${OPTS} -s -c ${COOKIE} -b ${COOKIE} -A "${BROWSER}" -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 if [ -z "$(grep ".${AMAZON}.*csrf" ${COOKIE})" ] ; then echo "trying to get CSRF from handlebars" ${CURL} ${OPTS} -s -c ${COOKIE} -b ${COOKIE} -A "${BROWSER}" -H "DNT: 1" -H "Connection: keep-alive" -L\ -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ https://${ALEXA}/templates/oobe/d-device-pick.handlebars > /dev/null fi if [ -z "$(grep ".${AMAZON}.*csrf" ${COOKIE})" ] ; then echo "trying to get CSRF from devices-v2" ${CURL} ${OPTS} -s -c ${COOKIE} -b ${COOKIE} -A "${BROWSER}" -H "DNT: 1" -H "Connection: keep-alive" -L\ -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ https://${ALEXA}/api/devices-v2/device?cached=false > /dev/null fi rm -f "${TMP}/.alexa.login" rm -f "${TMP}/.alexa.header" rm -f "${TMP}/.alexa.header2" rm -f "${TMP}/.alexa.postdata" rm -f "${TMP}/.alexa.postdata2" if [ -z "$(grep ".${AMAZON}.*csrf" ${COOKIE})" ] ; then echo "ERROR: no CSRF cookie received" exit 1 fi } # # get JSON device list # get_devlist() { ${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})"\ "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 # create a file that contains valid device names for the "ALL" device ATTR="accountName" NAME=$(grep ${ATTR}\| ${DEVTXT} | sed "s/^.*${ATTR}|//" | sed 's/ /_/g') ATTR="deviceFamily" FAMILY=$(grep ${ATTR}\| ${DEVTXT} | sed "s/^.*${ATTR}|//" | sed 's/ /_/g') IDX=0 for N in $NAME ; do C=0 for F in $FAMILY ; do if [ $C -eq $IDX ] ; then if [ "$F" = "WHA" -o "$F" = "ECHO" -o "$F" = "KNIGHT" -o "$F" = "ROOK" ] ; then echo ${N} >> ${DEVALL} fi break fi C=$((C+1)) done IDX=$((IDX+1)) done } check_status() { # # bootstrap with GUI-Version writes GUI version to cookie # returns among other the current authentication state # AUTHSTATUS=$(${CURL} ${OPTS} -s -b ${COOKIE} -A "${BROWSER}" -H "DNT: 1" -H "Connection: keep-alive" -L https://${ALEXA}/api/bootstrap?version=${GUIVERSION}) MEDIAOWNERCUSTOMERID=$(echo $AUTHSTATUS | sed -r 's/^.*"customerId":"([^,]+)",.*$/\1/g') AUTHSTATUS=$(echo $AUTHSTATUS | 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') ATTR="online" ONLINE=$(grep ${ATTR}\: ${DEVTXT} | sed "s/^.*${ATTR}://") if [ -z "${DEVICE}" ] ; then # if no device was supplied, use the first Echo(dot) in device list IDX=0 for F in $FAMILY ; do if [ "$F" = "ECHO" -o "$F" = "KNIGHT" -o "$F" = "ROOK" ] ; then break; fi IDX=$((IDX+1)) done C=0 for N in $NAME ; do if [ $C -eq $IDX ] ; then DEVICE=$N break fi C=$((C+1)) done echo "setting default device to:" echo ${DEVICE} else DEVICE=`echo $DEVICE | sed 's/ /_/g'` IDX=0 for N in $NAME ; do if [ "$N" = "$DEVICE" ] ; then break; fi IDX=$((IDX+1)) done fi # customerId is now retrieved from the logged in user # the customerId in the device list is always from the user registering the device initially # C=0 # for I in $MEDIAID ; do # if [ $C -eq $IDX ] ; then # MEDIAOWNERCUSTOMERID=$I # break # fi # C=$((C+1)) # done C=0 for T in $TYPE ; do if [ $C -eq $IDX ] ; then DEVICETYPE=$T break fi C=$((C+1)) done C=0 for S in $SERIAL ; do if [ $C -eq $IDX ] ; then DEVICESERIALNUMBER=$S break fi C=$((C+1)) done C=0 for F in $FAMILY ; do if [ $C -eq $IDX ] ; then DEVICEFAMILY=$F break fi C=$((C+1)) done C=0 for O in $ONLINE ; do if [ $C -eq $IDX ] ; then DEVICESTATE=$O break fi C=$((C+1)) done if [ -z "${DEVICESERIALNUMBER}" ] ; then echo "ERROR: unkown device dev:${DEVICE}" exit 1 fi } # # execute command # (SequenceCommands by Michael Geramb and Ralf Otto) # run_cmd() { if [ -n "${SEQUENCECMD}" ] ; then if echo $COMMAND | grep -q -E "weather|traffic|flashbriefing|goodmorning|singasong|tellstory|speak|sound|textcommand" ; then if [ "${DEVICEFAMILY}" = "WHA" ] ; then echo "Skipping unsupported command: ${COMMAND} on dev:${DEVICE} type:${DEVICETYPE} serial:${DEVICESERIALNUMBER} family:${DEVICEFAMILY}" return fi fi # the speak command is treated differently if $SPEAKVOL > 0 if [ -n "${TTS}" -a $SPEAKVOL -gt 0 ] || [ "${COMMAND%%:*}" = 'sound' -a $SPEAKVOL -gt 0 ] ; then SVOL=$SPEAKVOL # Not using arrays here in order to be compatible with non-Bash # Get the list position of the current device type IDX=0 for D in $DEVICEVOLNAME ; do if [ "${D}" = "${DEVICE}" ] ; then break; fi IDX=$((IDX+1)) done # get the speak volume at that position C=0 for D in $DEVICEVOLSPEAK ; do if [ $C -eq $IDX ] ; then if [ -n "${D}" ] ; then SVOL=$D ; fi break fi C=$((C+1)) done # try to retrieve the "currently playing" volume VOL=$(${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/media/state?deviceSerialNumber=${DEVICESERIALNUMBER}&deviceType=${DEVICETYPE}" | grep 'volume' | sed -r 's/^.*"volume":\s*([0-9]+)[^0-9]*$/\1/g') # in order to prevent a "Rate exceeded" we need to delay the command sleep 1 if [ -z "${VOL}" ] ; then # get the normal volume of the current device type C=0 for D in $DEVICEVOLNORMAL; do if [ $C -eq $IDX ] ; then VOL=$D break fi C=$((C+1)) done # if the volume is still undefined, use $NORMALVOL if [ -z "${VOL}" ] ; then VOL=$NORMALVOL fi fi ALEXACMD='{"behaviorId":"PREVIEW","sequenceJson":"{\"@type\":\"com.amazon.alexa.behaviors.model.Sequence\",\"startNode\":{\"@type\":\"com.amazon.alexa.behaviors.model.SerialNode\",\"nodesToExecute\":[{\"@type\":\"com.amazon.alexa.behaviors.model.OpaquePayloadOperationNode\",\"type\":\"Alexa.DeviceControls.Volume\",\"operationPayload\":{\"deviceType\":\"'${DEVICETYPE}'\",\"deviceSerialNumber\":\"'${DEVICESERIALNUMBER}'\",\"customerId\":\"'${MEDIAOWNERCUSTOMERID}'\",\"locale\":\"'${TTS_LOCALE}'\",\"value\":\"'${SVOL}'\"}},{\"@type\":\"com.amazon.alexa.behaviors.model.OpaquePayloadOperationNode\",\"type\":\"'${SEQUENCECMD}'\",\"operationPayload\":{\"deviceType\":\"'${DEVICETYPE}'\",\"deviceSerialNumber\":\"'${DEVICESERIALNUMBER}'\",\"customerId\":\"'${MEDIAOWNERCUSTOMERID}'\",\"locale\":\"'${TTS_LOCALE}'\"'${SEQUENCEVAL}'}},{\"@type\":\"com.amazon.alexa.behaviors.model.OpaquePayloadOperationNode\",\"type\":\"Alexa.DeviceControls.Volume\",\"operationPayload\":{\"deviceType\":\"'${DEVICETYPE}'\",\"deviceSerialNumber\":\"'${DEVICESERIALNUMBER}'\",\"customerId\":\"'${MEDIAOWNERCUSTOMERID}'\",\"locale\":\"'${TTS_LOCALE}'\",\"value\":\"'${VOL}'\"}}]}}","status":"ENABLED"}' else ALEXACMD='{"behaviorId":"PREVIEW","sequenceJson":"{\"@type\":\"com.amazon.alexa.behaviors.model.Sequence\",\"startNode\":{\"@type\":\"com.amazon.alexa.behaviors.model.OpaquePayloadOperationNode\",\"type\":\"'${SEQUENCECMD}'\",\"operationPayload\":{\"deviceType\":\"'${DEVICETYPE}'\",\"deviceSerialNumber\":\"'${DEVICESERIALNUMBER}'\",\"customerId\":\"'${MEDIAOWNERCUSTOMERID}'\",\"locale\":\"'${TTS_LOCALE}'\"'${SEQUENCEVAL}'}}}","status":"ENABLED"}' fi # Due to some weird shell-escape-behavior the command has to be written to a file before POSTing it echo $ALEXACMD > "${TMP}/.alexa.cmd" ${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 @"${TMP}/.alexa.cmd"\ "https://${ALEXA}/api/behaviors/preview" 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}"\ -H "csrf: $(awk "\$0 ~/.${AMAZON}.*csrf[ \\s\\t]+/ {print \$7}" ${COOKIE})" -X POST -d ${COMMAND}\ "https://${ALEXA}/api/np/command?deviceSerialNumber=${DEVICESERIALNUMBER}&deviceType=${DEVICETYPE}" fi } # # play TuneIn radio station # play_radio() { 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 PUT -d "${JSON}" \ "https://${ALEXA}/api/entertainment/v1/player/queue?deviceSerialNumber=${DEVICESERIALNUMBER}&deviceType=${DEVICETYPE}" } # # play library track # play_song() { if [ -z "${ALBUM}" ] ; then JSON="{\"trackId\":\"${SONG}\",\"playQueuePrime\":true}" else JSON="{\"albumArtistName\":\"${ARTIST}\",\"albumName\":\"${ALBUM}\"}" 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/cloudplayer/queue-and-play?deviceSerialNumber=${DEVICESERIALNUMBER}&deviceType=${DEVICETYPE}&mediaOwnerCustomerId=${MEDIAOWNERCUSTOMERID}&shuffle=false" } # # play library playlist # play_playlist() { ${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 "{\"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 "${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 "{\"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 "${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 "{\"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 "${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 "{\"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 "${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/np/player?deviceSerialNumber=${DEVICESERIALNUMBER}&deviceType=${DEVICETYPE}" echo echo "/api/np/queue" ${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/np/queue?deviceSerialNumber=${DEVICESERIALNUMBER}&deviceType=${DEVICETYPE}" echo echo "/api/media/state" ${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/media/state?deviceSerialNumber=${DEVICESERIALNUMBER}&deviceType=${DEVICETYPE}" echo } # # show notifications and alarms # show_notifications() { echo "/api/notifications" ${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/notifications?deviceSerialNumber=${DEVICESERIALNUMBER}&deviceType=${DEVICETYPE}" echo } # # deletes a multiroom device # delete_multiroom() { ${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 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 "${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/lemur/tail" } # # list bluetooth devices # list_bluetooth() { ${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/bluetooth?cached=false" } # # connect bluetooth device # connect_bluetooth() { ${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 "{\"bluetoothDeviceAddress\":\"${BLUETOOTH}\"}"\ "https://${ALEXA}/api/bluetooth/pair-sink/${DEVICETYPE}/${DEVICESERIALNUMBER}" } # # disconnect bluetooth device # disconnect_bluetooth() { ${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/bluetooth/disconnect-sink/${DEVICETYPE}/${DEVICESERIALNUMBER}" } # # device that sent the last command # (by Markus Wennesheimer) # last_alexa() { ${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/activities?startTime=&size=1&offset=1" | sed -r 's/^.*serialNumber":"([^"]+)".*$/\1/' } # # logout # log_off() { ${CURL} ${OPTS} -s -c ${COOKIE} -b ${COOKIE} -A "${BROWSER}" -H "DNT: 1" -H "Connection: keep-alive" -L\ https://${ALEXA}/logout > /dev/null rm -f ${DEVLIST} rm -f ${DEVTXT} rm -f ${DEVALL} rm -f ${COOKIE} } if [ -z "$LASTALEXA" -a -z "$BLUETOOTH" -a -z "$LEMUR" -a -z "$PLIST" -a -z "$HIST" -a -z "$SEEDID" -a -z "$ASIN" -a -z "$QUEUE" -a -z "$NOTIFICATIONS" -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 does not exist. logging in ..." log_in fi check_status if [ $? -eq 0 ] ; then echo "cookie expired, logging in again ..." log_in check_status if [ $? -eq 0 ] ; then echo "log in failed, aborting" exit 1 fi fi if [ ! -f ${DEVTXT} -o ! -f ${DEVALL} ] ; then echo "device list does not exist. downloading ..." get_devlist if [ ! -f ${DEVTXT} ] ; then echo "failed to download device list, aborting" exit 1 fi fi if [ -n "$LOGIN" ] ; then echo "logged in" exit 0 fi if [ -n "$COMMAND" -o -n "$QUEUE" -o -n "$NOTIFICATIONS" ] ; then if [ "${DEVICE}" = "ALL" ] ; then while IFS= read -r DEVICE ; do set_var if [ "$DEVICESTATE" = "true" ] ; then if [ -n "$COMMAND" ] ; then echo "sending cmd:${COMMAND} to dev:${DEVICE} type:${DEVICETYPE} serial:${DEVICESERIALNUMBER} customerid:${MEDIAOWNERCUSTOMERID}" run_cmd # in order to prevent a "Rate exceeded" we need to delay the command sleep 1 echo elif [ -n "$NOTIFICATIONS" ] ; then echo "notifications info for dev:${DEVICE} type:${DEVICETYPE} serial:${DEVICESERIALNUMBER}" show_notifications else echo "queue info for dev:${DEVICE} type:${DEVICETYPE} serial:${DEVICESERIALNUMBER}" show_queue echo fi fi done < ${DEVALL} else set_var if [ -n "$COMMAND" ] ; then echo "sending cmd:${COMMAND} to dev:${DEVICE} type:${DEVICETYPE} serial:${DEVICESERIALNUMBER} customerid:${MEDIAOWNERCUSTOMERID}" run_cmd echo elif [ -n "$NOTIFICATIONS" ] ; then echo "notifications info for dev:${DEVICE} type:${DEVICETYPE} serial:${DEVICESERIALNUMBER}" show_notifications else echo "queue info for dev:${DEVICE} type:${DEVICETYPE} serial:${DEVICESERIALNUMBER}" show_queue echo fi 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 echo fi rm -f ${DEVLIST} rm -f ${DEVALL} rm -f ${DEVTXT} get_devlist elif [ -n "$BLUETOOTH" ] ; then if [ "$BLUETOOTH" = "list" -o "$BLUETOOTH" = "List" -o "$BLUETOOTH" = "LIST" ] ; then if [ "${DEVICE}" = "ALL" ] ; then while IFS= read -r DEVICE ; do set_var echo "bluetooth api list:" list_bluetooth echo done < ${DEVALL} else set_var echo "bluetooth api list:" list_bluetooth echo fi elif [ "$BLUETOOTH" = "null" ] ; then set_var echo "disconnecting dev:${DEVICE} type:${DEVICETYPE} serial:${DEVICESERIALNUMBER} from bluetooth" disconnect_bluetooth echo else set_var echo "connecting dev:${DEVICE} type:${DEVICETYPE} serial:${DEVICESERIALNUMBER} to bluetooth device:${BLUETOOTH}" connect_bluetooth echo 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 "$LIST" ] ; then ATTR="accountName" echo "the following devices exist in your account:" grep ${ATTR}\| ${DEVTXT} | sed "s/^.*${ATTR}|//" | sed 's/ /_/g' elif [ -n "$LASTALEXA" ] ; then last_alexa else echo "no alexa command received" fi if [ -n "$LOGOFF" ] ; then echo "logout option present, logging off ..." log_off fi