├── README.md ├── LICENSE ├── alexa_remote_control_plain.sh └── alexa_remote_control.sh /README.md: -------------------------------------------------------------------------------- 1 | 2 | # alexa-remote-control 3 | control Amazon Alexa from command Line 4 | 5 | The settings can now be controlled via environment variables. 6 | ``` 7 | BROWSER - the User-Agent your browser sends in the request header 8 | AMAZON - your Amazon domain 9 | ALEXA - the URL you would use for the Alexa Web App 10 | CURL - location of your cURL binary 11 | OPTS - any cURL options you require 12 | TMP - location of the temp dir 13 | SPEAKVOL - the volume for speak messages ( if set to 0, volume levels are left untouched) 14 | NORMALVOL - if no current playing volume can be determined, fall back to normal volume 15 | VOLMAXAGE - max. age in minutes before volume is re-read from API 16 | DEVICEVOLNAME - a list of device names with specific volume settings (space separated) 17 | DEVICEVOLSPEAK - a list of speak volume levels - matching the devices above 18 | DEVICEVOLNORMAL - a list of normal volume levels- matching the devices above 19 | (current playing volume takes precedence for normal volume) 20 | REFRESH_TOKEN - the new preference over EMAIL/PASSWORD can be obtained here: https://github.com/adn77/alexa-cookie-cli 21 | ``` 22 | 23 | ``` 24 | alexa-remote-control [-d |ALL] -e > | 25 | -b [list|<"AA:BB:CC:DD:EE:FF">] | -q | -n | -r <"station name"|stationid> | 26 | -s | -t | -u | -v | 27 | -w | -i | -p | -P | -S | -a | -z | -l | -h | 28 | -m [device_1 .. device_X] | -lastalexa | -lastcommand 29 | 30 | -e : run command, additional SEQUENCECMDs: 31 | weather,traffic,flashbriefing,goodmorning,singasong,tellstory, 32 | speak:'',automation:'',sound:, 33 | textcommand:'', 34 | playmusic::'' 35 | 36 | -b : connect/disconnect/list bluetooth device 37 | -c : list 'playmusic' channels 38 | -q : query queue 39 | -n : query notifications 40 | -r : play tunein radio 41 | -s : play library track/library album 42 | -t : play Prime playlist 43 | -u : play Prime station 44 | -v : play Prime historical queue 45 | -w : play library playlist 46 | -i : list imported library tracks 47 | -p : list purchased library tracks 48 | -P : list Prime playlists 49 | -S : list Prime stations 50 | -a : list available devices 51 | -m : delete multiroom and/or create new multiroom containing devices 52 | -lastalexa : print device that received the last voice command 53 | -lastcommand : print last voice command or last voice command of specific device 54 | -login : Logs in, without further command (downloads cookie) 55 | -z : print current volume level 56 | -l : logoff 57 | -h : help 58 | ``` 59 | 60 | Login via REFRESH_TOKEN 61 | ---- 62 | 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 63 | 64 | https://blog.loetzimmer.de/2021/09/alexa-remote-control-shell-script.html 65 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /alexa_remote_control_plain.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Amazon Alexa Remote Control (PLAIN shell) 4 | # alex(at)loetzimmer.de 5 | # 6 | # 2021-01-28: v0.17c (for updates see http://blog.loetzimmer.de/2017/10/amazon-alexa-hort-auf-die-shell-echo.html) 7 | # 2021-09-02: v0.17d includes fixes for playing tunein (base64 required) 8 | # 9 | # !!! THIS IS THE FINAL VERSION !!! 10 | # 11 | # Due to JQ being widely available across platforms there is no need to expose oneself to the hacks 12 | # required when parsing JSON with BASH. 13 | # 14 | ### 15 | # 16 | # (no BASHisms were used, should run with any shell) 17 | # - requires cURL for web communication 18 | # - (GNU) sed and awk for extraction 19 | # - base64 for B64 encoding (make sure "-w 0" option is available on your platform) 20 | # - oathtool as OATH one-time password tool (optional for two-factor authentication) 21 | # 22 | ########################################## 23 | 24 | SET_EMAIL='amazon_account@email.address' 25 | SET_PASSWORD='Very_Secret_Amazon_Account_Password' 26 | SET_MFA_SECRET='' 27 | # something like: 28 | # 1234 5678 9ABC DEFG HIJK LMNO PQRS TUVW XYZ0 1234 5678 9ABC DEFG 29 | 30 | SET_LANGUAGE='de,en-US;q=0.7,en;q=0.3' 31 | #SET_LANGUAGE='en-US' 32 | 33 | SET_TTS_LOCALE='de-DE' 34 | 35 | SET_AMAZON='amazon.de' 36 | #SET_AMAZON='amazon.com' 37 | 38 | SET_ALEXA='alexa.amazon.de' 39 | #SET_ALEXA='pitangui.amazon.com' 40 | 41 | # cURL binary 42 | SET_CURL='/usr/bin/curl' 43 | 44 | # cURL options 45 | # -k : if your cURL cannot verify CA certificates, you'll have to trust any 46 | # --compressed : if your cURL was compiled with libz you may use compression 47 | # --http1.1 : cURL defaults to HTTP/2 on HTTPS connections if available 48 | SET_OPTS='--compressed --http1.1' 49 | #SET_OPTS='-k --compressed --http1.1' 50 | 51 | # browser identity 52 | SET_BROWSER='Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:1.0) bash-script/1.0' 53 | #SET_BROWSER='Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:65.0) Gecko/20100101 Firefox/65.0' 54 | 55 | # oathtool command line tool 56 | SET_OATHTOOL='/usr/bin/oathtool' 57 | 58 | # tmp path 59 | SET_TMP="/tmp" 60 | 61 | # Volume for speak commands (a SPEAKVOL of 0 leaves the volume settings untouched) 62 | SET_SPEAKVOL="0" 63 | # if no current playing volume can be determined, fall back to normal volume 64 | SET_NORMALVOL="10" 65 | 66 | # Device specific volumes (overriding the above) 67 | SET_DEVICEVOLNAME="EchoDot2ndGen Echo1stGen" 68 | SET_DEVICEVOLSPEAK="100 30" 69 | SET_DEVICEVOLNORMAL="100 20" 70 | 71 | ########################################### 72 | # nothing to configure below here 73 | # 74 | 75 | # retrieving environment variables if any are set 76 | EMAIL=${EMAIL:-$SET_EMAIL} 77 | PASSWORD=${PASSWORD:-$SET_PASSWORD} 78 | MFA_SECRET=${MFA_SECRET:-$SET_MFA_SECRET} 79 | AMAZON=${AMAZON:-$SET_AMAZON} 80 | ALEXA=${ALEXA:-$SET_ALEXA} 81 | LANGUAGE=${LANGUAGE:-$SET_LANGUAGE} 82 | BROWSER=${BROWSER:-$SET_BROWSER} 83 | CURL=${CURL:-$SET_CURL} 84 | OPTS=${OPTS:-$SET_OPTS} 85 | TTS_LOCALE=${TTS_LOCALE:-$SET_TTS_LOCALE} 86 | TMP=${TMP:-$SET_TMP} 87 | OATHTOOL=${OATHTOOL:-$SET_OATHTOOL} 88 | SPEAKVOL=${SPEAKVOL:-$SET_SPEAKVOL} 89 | NORMALVOL=${NORMALVOL:-$SET_NORMALVOL} 90 | DEVICEVOLNAME=${DEVICEVOLNAME:-$SET_DEVICEVOLNAME} 91 | DEVICEVOLSPEAK=${DEVICEVOLSPEAK:-$SET_DEVICEVOLSPEAK} 92 | DEVICEVOLNORMAL=${DEVICEVOLNORMAL:-$SET_DEVICEVOLNORMAL} 93 | 94 | COOKIE="${TMP}/.alexa.cookie" 95 | DEVLIST="${TMP}/.alexa.devicelist.json" 96 | DEVTXT="${TMP}/.alexa.devicelist.txt" 97 | DEVALL="${TMP}/.alexa.devicelist.all" 98 | 99 | GUIVERSION=0 100 | 101 | LIST="" 102 | LOGOFF="" 103 | COMMAND="" 104 | TTS="" 105 | SEQUENCECMD="" 106 | SEQUENCEVAL="" 107 | STATIONID="" 108 | QUEUE="" 109 | SONG="" 110 | ALBUM="" 111 | ARTIST="" 112 | ASIN="" 113 | SEEDID="" 114 | HIST="" 115 | LEMUR="" 116 | CHILD="" 117 | PLIST="" 118 | BLUETOOTH="" 119 | LASTALEXA="" 120 | NOTIFICATIONS="" 121 | 122 | usage() 123 | { 124 | echo "$0 [-d |ALL] -e > |" 125 | echo " -b [list|<\"AA:BB:CC:DD:EE:FF\">] | -q | -n | -r <\"station name\"|stationid> |" 126 | echo " -s | -t | -u | -v | -w |" 127 | echo " -a | -m [device_1 .. device_X] | -lastalexa | -l | -h" 128 | echo 129 | echo " -e : run command, additional SEQUENCECMDs:" 130 | echo " weather,traffic,flashbriefing,goodmorning,singasong,tellstory," 131 | echo " speak:'',sound:," 132 | echo " textcommand:''" 133 | echo " -b : connect/disconnect/list bluetooth device" 134 | echo " -q : query queue" 135 | echo " -n : query notifications" 136 | echo " -r : play tunein radio" 137 | echo " -s : play library track/library album" 138 | echo " -t : play Prime playlist" 139 | echo " -u : play Prime station" 140 | echo " -v : play Prime historical queue" 141 | echo " -w : play library playlist" 142 | echo " -a : list available devices" 143 | echo " -m : delete multiroom and/or create new multiroom containing devices" 144 | echo " -lastalexa : print serial number that received the last voice command" 145 | echo " -login : Logs in, without further command" 146 | echo " -l : logoff" 147 | echo " -h : help" 148 | } 149 | 150 | while [ "$#" -gt 0 ] ; do 151 | case "$1" in 152 | --version) 153 | echo "v0.17d" 154 | exit 0 155 | ;; 156 | -d) 157 | if [ "${2#-}" != "${2}" -o -z "$2" ] ; then 158 | echo "ERROR: missing argument for ${1}" 159 | usage 160 | exit 1 161 | fi 162 | DEVICE=$2 163 | shift 164 | ;; 165 | -e) 166 | if [ "${2#-}" != "${2}" -o -z "$2" ] ; then 167 | echo "ERROR: missing argument for ${1}" 168 | usage 169 | exit 1 170 | fi 171 | COMMAND=$2 172 | shift 173 | ;; 174 | -b) 175 | if [ "${2#-}" = "${2}" -a -n "$2" ] ; then 176 | BLUETOOTH=$2 177 | shift 178 | else 179 | BLUETOOTH="null" 180 | fi 181 | ;; 182 | -m) 183 | if [ "${2#-}" != "${2}" -o -z "$2" ] ; then 184 | echo "ERROR: missing argument for ${1}" 185 | usage 186 | exit 1 187 | fi 188 | LEMUR=$2 189 | shift 190 | while [ "${2#-}" = "${2}" -a -n "$2" ] ; do 191 | CHILD="${CHILD} ${2}" 192 | shift 193 | done 194 | ;; 195 | -r) 196 | if [ "${2#-}" != "${2}" -o -z "$2" ] ; then 197 | echo "ERROR: missing argument for ${1}" 198 | usage 199 | exit 1 200 | fi 201 | STATIONID=$2 202 | shift 203 | ;; 204 | -s) 205 | if [ "${2#-}" != "${2}" -o -z "$2" ] ; then 206 | echo "ERROR: missing argument for ${1}" 207 | usage 208 | exit 1 209 | fi 210 | SONG=$2 211 | shift 212 | if [ "${2#-}" = "${2}" -a -n "$2" ] ; then 213 | ALBUM=$2 214 | ARTIST=$SONG 215 | shift 216 | fi 217 | ;; 218 | -t) 219 | if [ "${2#-}" != "${2}" -o -z "$2" ] ; then 220 | echo "ERROR: missing argument for ${1}" 221 | usage 222 | exit 1 223 | fi 224 | ASIN=$2 225 | shift 226 | ;; 227 | -u) 228 | if [ "${2#-}" != "${2}" -o -z "$2" ] ; then 229 | echo "ERROR: missing argument for ${1}" 230 | usage 231 | exit 1 232 | fi 233 | SEEDID=$2 234 | shift 235 | ;; 236 | -v) 237 | if [ "${2#-}" != "${2}" -o -z "$2" ] ; then 238 | echo "ERROR: missing argument for ${1}" 239 | usage 240 | exit 1 241 | fi 242 | HIST=$2 243 | shift 244 | ;; 245 | -w) 246 | if [ "${2#-}" != "${2}" -o -z "$2" ] ; then 247 | echo "ERROR: missing argument for ${1}" 248 | usage 249 | exit 1 250 | fi 251 | PLIST=$2 252 | shift 253 | ;; 254 | -d) 255 | if [ "${2#-}" != "${2}" -o -z "$2" ] ; then 256 | echo "ERROR: missing argument for ${1}" 257 | usage 258 | exit 1 259 | fi 260 | DEVICE=$2 261 | shift 262 | ;; 263 | -login) 264 | LOGIN="true" 265 | ;; 266 | -l) 267 | LOGOFF="true" 268 | ;; 269 | -a) 270 | LIST="true" 271 | ;; 272 | -q) 273 | QUEUE="true" 274 | ;; 275 | -n) 276 | NOTIFICATIONS="true" 277 | ;; 278 | -lastalexa) 279 | LASTALEXA="true" 280 | ;; 281 | -h|-\?|--help) 282 | usage 283 | exit 0 284 | ;; 285 | *) 286 | echo "ERROR: unknown option ${1}" 287 | usage 288 | exit 1 289 | ;; 290 | esac 291 | shift 292 | done 293 | 294 | case "$COMMAND" in 295 | pause) 296 | COMMAND='{"type":"PauseCommand"}' 297 | ;; 298 | play) 299 | COMMAND='{"type":"PlayCommand"}' 300 | ;; 301 | next) 302 | COMMAND='{"type":"NextCommand"}' 303 | ;; 304 | prev) 305 | COMMAND='{"type":"PreviousCommand"}' 306 | ;; 307 | fwd) 308 | COMMAND='{"type":"ForwardCommand"}' 309 | ;; 310 | rwd) 311 | COMMAND='{"type":"RewindCommand"}' 312 | ;; 313 | shuffle) 314 | COMMAND='{"type":"ShuffleCommand","shuffle":"true"}' 315 | ;; 316 | repeat) 317 | COMMAND='{"type":"RepeatCommand","repeat":true}' 318 | ;; 319 | vol:*) 320 | VOL=${COMMAND##*:} 321 | # volume as integer! 322 | if [ $VOL -le 100 -a $VOL -ge 0 ] ; then 323 | # COMMAND='{"type":"VolumeLevelCommand","volumeLevel":'${VOL}'}' 324 | SEQUENCECMD='Alexa.DeviceControls.Volume' 325 | SEQUENCEVAL=',\"value\":\"'${VOL}'\"' 326 | else 327 | echo "ERROR: volume should be an integer between 0 and 100" 328 | usage 329 | exit 1 330 | fi 331 | ;; 332 | textcommand:*) 333 | SEQUENCECMD='Alexa.TextCommand\",\"skillId\":\"amzn1.ask.1p.tellalexa' 334 | SEQUENCEVAL=$(echo ${COMMAND##textcommand:} | sed -r s/\"/\'/g) 335 | SEQUENCEVAL=',\"text\":\"'${SEQUENCEVAL}'\"' 336 | ;; 337 | speak:*) 338 | TTS=$(echo ${COMMAND##*:} | sed -r 's/["\\]/ /g') 339 | TTS=',\"textToSpeak\":\"'${TTS}'\"' 340 | SEQUENCECMD='Alexa.Speak' 341 | SEQUENCEVAL=$TTS 342 | ;; 343 | sound:*) 344 | SEQUENCECMD='Alexa.Sound' 345 | SEQUENCEVAL=',\"soundStringId\":\"'${COMMAND##sound:}'\"' 346 | ;; 347 | weather) 348 | SEQUENCECMD='Alexa.Weather.Play' 349 | ;; 350 | traffic) 351 | SEQUENCECMD='Alexa.Traffic.Play' 352 | ;; 353 | flashbriefing) 354 | SEQUENCECMD='Alexa.FlashBriefing.Play' 355 | ;; 356 | goodmorning) 357 | SEQUENCECMD='Alexa.GoodMorning.Play' 358 | ;; 359 | singasong) 360 | SEQUENCECMD='Alexa.SingASong.Play' 361 | ;; 362 | tellstory) 363 | SEQUENCECMD='Alexa.TellStory.Play' 364 | ;; 365 | "") 366 | ;; 367 | *) 368 | echo "ERROR: unknown command \"${COMMAND}\"!" 369 | usage 370 | exit 1 371 | ;; 372 | esac 373 | 374 | # 375 | # Amazon Login 376 | # 377 | log_in() 378 | { 379 | ################################################################ 380 | # 381 | # following headers are required: 382 | # Accept-Language (possibly for determining login region) 383 | # User-Agent (CURL wouldn't store cookies without) 384 | # 385 | ################################################################ 386 | 387 | rm -f ${DEVLIST} 388 | rm -f ${DEVTXT} 389 | rm -f ${DEVALL} 390 | rm -f ${COOKIE} 391 | 392 | # 393 | # get first cookie and write redirection target into referer 394 | # 395 | ${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\ 396 | https://alexa.${AMAZON} | grep "hidden" | sed 's/hidden/\n/g' | grep "value=\"" | sed -r 's/^.*name="([^"]+)".*value="([^"]+)".*/\1=\2\&/g' > "${TMP}/.alexa.postdata" 397 | 398 | # 399 | # login empty to generate session 400 | # 401 | ${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\ 402 | -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" 403 | 404 | # 405 | # add OTP if using MFA 406 | # 407 | if [ -n "${MFA_SECRET}" ] ; then 408 | OTP=$(${OATHTOOL} -b --totp "${MFA_SECRET}") 409 | PASSWORD="${PASSWORD}${OTP}" 410 | fi 411 | 412 | # 413 | # login with filled out form 414 | # !!! referer now contains session in URL 415 | # 416 | ${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\ 417 | -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" 418 | 419 | # check whether the login has been successful or exit otherwise 420 | if [ -z "$(grep 'Location: https://alexa.*html' ${TMP}/.alexa.header2)" ] ; then 421 | echo "ERROR: Amazon Login was unsuccessful. Possibly you get a captcha login screen." 422 | echo " Try logging in to https://alexa.${AMAZON} with your browser. In your browser" 423 | echo " make sure to have all Amazon related cookies deleted and Javascript disabled!" 424 | echo 425 | echo " (For more information have a look at ${TMP}/.alexa.login)" 426 | echo 427 | echo " To avoid issues with captcha, try using Multi-Factor Authentication." 428 | echo " To do so, first set up Two-Step Verification on your Amazon account, then" 429 | echo " configure this script (or the environment) with your MFA secret." 430 | echo " Support for Multi-Factor Authentication requires 'oathtool' to be installed." 431 | 432 | rm -f ${COOKIE} 433 | rm -f "${TMP}/.alexa.header" 434 | rm -f "${TMP}/.alexa.header2" 435 | rm -f "${TMP}/.alexa.postdata" 436 | rm -f "${TMP}/.alexa.postdata2" 437 | exit 1 438 | fi 439 | 440 | # 441 | # get CSRF 442 | # 443 | ${CURL} ${OPTS} -s -c ${COOKIE} -b ${COOKIE} -A "${BROWSER}" -H "DNT: 1" -H "Connection: keep-alive" -L\ 444 | -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ 445 | https://${ALEXA}/api/language > /dev/null 446 | 447 | if [ -z "$(grep ".${AMAZON}.*csrf" ${COOKIE})" ] ; then 448 | echo "trying to get CSRF from handlebars" 449 | ${CURL} ${OPTS} -s -c ${COOKIE} -b ${COOKIE} -A "${BROWSER}" -H "DNT: 1" -H "Connection: keep-alive" -L\ 450 | -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ 451 | https://${ALEXA}/templates/oobe/d-device-pick.handlebars > /dev/null 452 | fi 453 | 454 | if [ -z "$(grep ".${AMAZON}.*csrf" ${COOKIE})" ] ; then 455 | echo "trying to get CSRF from devices-v2" 456 | ${CURL} ${OPTS} -s -c ${COOKIE} -b ${COOKIE} -A "${BROWSER}" -H "DNT: 1" -H "Connection: keep-alive" -L\ 457 | -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ 458 | https://${ALEXA}/api/devices-v2/device?cached=false > /dev/null 459 | fi 460 | 461 | rm -f "${TMP}/.alexa.login" 462 | rm -f "${TMP}/.alexa.header" 463 | rm -f "${TMP}/.alexa.header2" 464 | rm -f "${TMP}/.alexa.postdata" 465 | rm -f "${TMP}/.alexa.postdata2" 466 | 467 | if [ -z "$(grep ".${AMAZON}.*csrf" ${COOKIE})" ] ; then 468 | echo "ERROR: no CSRF cookie received" 469 | exit 1 470 | fi 471 | } 472 | 473 | # 474 | # get JSON device list 475 | # 476 | get_devlist() 477 | { 478 | ${CURL} ${OPTS} -s -b ${COOKIE} -A "${BROWSER}" -H "DNT: 1" -H "Connection: keep-alive" -L\ 479 | -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ 480 | -H "csrf: $(awk "\$0 ~/.${AMAZON}.*csrf[ \\s\\t]+/ {print \$7}" ${COOKIE})"\ 481 | "https://${ALEXA}/api/devices-v2/device?cached=false" > ${DEVLIST} 482 | 483 | if [ ! -f ${DEVTXT} ] ; then 484 | 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} 485 | fi 486 | 487 | # create a file that contains valid device names for the "ALL" device 488 | ATTR="accountName" 489 | NAME=$(grep ${ATTR}\| ${DEVTXT} | sed "s/^.*${ATTR}|//" | sed 's/ /_/g') 490 | 491 | ATTR="deviceFamily" 492 | FAMILY=$(grep ${ATTR}\| ${DEVTXT} | sed "s/^.*${ATTR}|//" | sed 's/ /_/g') 493 | 494 | IDX=0 495 | for N in $NAME ; do 496 | C=0 497 | for F in $FAMILY ; do 498 | if [ $C -eq $IDX ] ; then 499 | if [ "$F" = "WHA" -o "$F" = "ECHO" -o "$F" = "KNIGHT" -o "$F" = "ROOK" ] ; then 500 | echo ${N} >> ${DEVALL} 501 | fi 502 | break 503 | fi 504 | C=$((C+1)) 505 | done 506 | IDX=$((IDX+1)) 507 | done 508 | } 509 | 510 | check_status() 511 | { 512 | # 513 | # bootstrap with GUI-Version writes GUI version to cookie 514 | # returns among other the current authentication state 515 | # 516 | AUTHSTATUS=$(${CURL} ${OPTS} -s -b ${COOKIE} -A "${BROWSER}" -H "DNT: 1" -H "Connection: keep-alive" -L https://${ALEXA}/api/bootstrap?version=${GUIVERSION}) 517 | MEDIAOWNERCUSTOMERID=$(echo $AUTHSTATUS | sed -r 's/^.*"customerId":"([^,]+)",.*$/\1/g') 518 | AUTHSTATUS=$(echo $AUTHSTATUS | sed -r 's/^.*"authenticated":([^,]+),.*$/\1/g') 519 | 520 | if [ "$AUTHSTATUS" = "true" ] ; then 521 | return 1 522 | fi 523 | 524 | return 0 525 | } 526 | 527 | # 528 | # set device specific variables from JSON device list 529 | # 530 | set_var() 531 | { 532 | ATTR="accountName" 533 | NAME=$(grep ${ATTR}\| ${DEVTXT} | sed "s/^.*${ATTR}|//" | sed 's/ /_/g') 534 | 535 | ATTR="deviceType" 536 | TYPE=$(grep ${ATTR}\| ${DEVTXT} | sed "s/^.*${ATTR}|//" | sed 's/ /_/g') 537 | 538 | ATTR="serialNumber" 539 | SERIAL=$(grep ${ATTR}\| ${DEVTXT} | sed "s/^.*${ATTR}|//" | sed 's/ /_/g') 540 | 541 | # ATTR="deviceOwnerCustomerId" 542 | # MEDIAID=`grep ${ATTR}\| ${DEVTXT} | sed "s/^.*${ATTR}|//" | sed 's/ /_/g') 543 | 544 | ATTR="deviceFamily" 545 | FAMILY=$(grep ${ATTR}\| ${DEVTXT} | sed "s/^.*${ATTR}|//" | sed 's/ /_/g') 546 | 547 | ATTR="online" 548 | ONLINE=$(grep ${ATTR}\: ${DEVTXT} | sed "s/^.*${ATTR}://") 549 | 550 | if [ -z "${DEVICE}" ] ; then 551 | # if no device was supplied, use the first Echo(dot) in device list 552 | IDX=0 553 | for F in $FAMILY ; do 554 | if [ "$F" = "ECHO" -o "$F" = "KNIGHT" -o "$F" = "ROOK" ] ; then 555 | break; 556 | fi 557 | IDX=$((IDX+1)) 558 | done 559 | 560 | C=0 561 | for N in $NAME ; do 562 | if [ $C -eq $IDX ] ; then 563 | DEVICE=$N 564 | break 565 | fi 566 | C=$((C+1)) 567 | done 568 | echo "setting default device to:" 569 | echo ${DEVICE} 570 | else 571 | DEVICE=`echo $DEVICE | sed 's/ /_/g'` 572 | IDX=0 573 | for N in $NAME ; do 574 | if [ "$N" = "$DEVICE" ] ; then 575 | break; 576 | fi 577 | IDX=$((IDX+1)) 578 | done 579 | fi 580 | 581 | # customerId is now retrieved from the logged in user 582 | # the customerId in the device list is always from the user registering the device initially 583 | # C=0 584 | # for I in $MEDIAID ; do 585 | # if [ $C -eq $IDX ] ; then 586 | # MEDIAOWNERCUSTOMERID=$I 587 | # break 588 | # fi 589 | # C=$((C+1)) 590 | # done 591 | 592 | C=0 593 | for T in $TYPE ; do 594 | if [ $C -eq $IDX ] ; then 595 | DEVICETYPE=$T 596 | break 597 | fi 598 | C=$((C+1)) 599 | done 600 | 601 | C=0 602 | for S in $SERIAL ; do 603 | if [ $C -eq $IDX ] ; then 604 | DEVICESERIALNUMBER=$S 605 | break 606 | fi 607 | C=$((C+1)) 608 | done 609 | 610 | C=0 611 | for F in $FAMILY ; do 612 | if [ $C -eq $IDX ] ; then 613 | DEVICEFAMILY=$F 614 | break 615 | fi 616 | C=$((C+1)) 617 | done 618 | 619 | C=0 620 | for O in $ONLINE ; do 621 | if [ $C -eq $IDX ] ; then 622 | DEVICESTATE=$O 623 | break 624 | fi 625 | C=$((C+1)) 626 | done 627 | 628 | if [ -z "${DEVICESERIALNUMBER}" ] ; then 629 | echo "ERROR: unkown device dev:${DEVICE}" 630 | exit 1 631 | fi 632 | } 633 | 634 | # 635 | # execute command 636 | # (SequenceCommands by Michael Geramb and Ralf Otto) 637 | # 638 | run_cmd() 639 | { 640 | if [ -n "${SEQUENCECMD}" ] ; then 641 | if echo $COMMAND | grep -q -E "weather|traffic|flashbriefing|goodmorning|singasong|tellstory|speak|sound|textcommand" ; then 642 | if [ "${DEVICEFAMILY}" = "WHA" ] ; then 643 | echo "Skipping unsupported command: ${COMMAND} on dev:${DEVICE} type:${DEVICETYPE} serial:${DEVICESERIALNUMBER} family:${DEVICEFAMILY}" 644 | return 645 | fi 646 | fi 647 | # the speak command is treated differently if $SPEAKVOL > 0 648 | if [ -n "${TTS}" -a $SPEAKVOL -gt 0 ] || [ "${COMMAND%%:*}" = 'sound' -a $SPEAKVOL -gt 0 ] ; then 649 | SVOL=$SPEAKVOL 650 | 651 | # Not using arrays here in order to be compatible with non-Bash 652 | # Get the list position of the current device type 653 | IDX=0 654 | for D in $DEVICEVOLNAME ; do 655 | if [ "${D}" = "${DEVICE}" ] ; then 656 | break; 657 | fi 658 | IDX=$((IDX+1)) 659 | done 660 | 661 | # get the speak volume at that position 662 | C=0 663 | for D in $DEVICEVOLSPEAK ; do 664 | if [ $C -eq $IDX ] ; then 665 | if [ -n "${D}" ] ; then SVOL=$D ; fi 666 | break 667 | fi 668 | C=$((C+1)) 669 | done 670 | 671 | # try to retrieve the "currently playing" volume 672 | VOL=$(${CURL} ${OPTS} -s -b ${COOKIE} -A "${BROWSER}" -H "DNT: 1" -H "Connection: keep-alive" -L\ 673 | -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ 674 | -H "csrf: $(awk "\$0 ~/.${AMAZON}.*csrf[ \\s\\t]+/ {print \$7}" ${COOKIE})" -X GET \ 675 | "https://${ALEXA}/api/media/state?deviceSerialNumber=${DEVICESERIALNUMBER}&deviceType=${DEVICETYPE}" | grep 'volume' | sed -r 's/^.*"volume":\s*([0-9]+)[^0-9]*$/\1/g') 676 | 677 | # in order to prevent a "Rate exceeded" we need to delay the command 678 | sleep 1 679 | 680 | if [ -z "${VOL}" ] ; then 681 | # get the normal volume of the current device type 682 | C=0 683 | for D in $DEVICEVOLNORMAL; do 684 | if [ $C -eq $IDX ] ; then 685 | VOL=$D 686 | break 687 | fi 688 | C=$((C+1)) 689 | done 690 | # if the volume is still undefined, use $NORMALVOL 691 | if [ -z "${VOL}" ] ; then 692 | VOL=$NORMALVOL 693 | fi 694 | fi 695 | 696 | 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"}' 697 | else 698 | 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"}' 699 | fi 700 | 701 | # Due to some weird shell-escape-behavior the command has to be written to a file before POSTing it 702 | echo $ALEXACMD > "${TMP}/.alexa.cmd" 703 | 704 | ${CURL} ${OPTS} -s -b ${COOKIE} -A "${BROWSER}" -H "DNT: 1" -H "Connection: keep-alive" -L\ 705 | -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ 706 | -H "csrf: $(awk "\$0 ~/.${AMAZON}.*csrf[ \\s\\t]+/ {print \$7}" ${COOKIE})" -X POST -d @"${TMP}/.alexa.cmd"\ 707 | "https://${ALEXA}/api/behaviors/preview" 708 | 709 | rm -f "${TMP}/.alexa.cmd" 710 | 711 | else 712 | ${CURL} ${OPTS} -s -b ${COOKIE} -A "${BROWSER}" -H "DNT: 1" -H "Connection: keep-alive" -L\ 713 | -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ 714 | -H "csrf: $(awk "\$0 ~/.${AMAZON}.*csrf[ \\s\\t]+/ {print \$7}" ${COOKIE})" -X POST -d ${COMMAND}\ 715 | "https://${ALEXA}/api/np/command?deviceSerialNumber=${DEVICESERIALNUMBER}&deviceType=${DEVICETYPE}" 716 | fi 717 | } 718 | 719 | # 720 | # play TuneIn radio station 721 | # 722 | play_radio() 723 | { 724 | JSON='{"contentToken":"music:'$(echo '["music/tuneIn/stationId","'${STATIONID}'"]|{"previousPageId":"TuneIn_SEARCH"}'| base64 -w 0| base64 -w 0 )'"}' 725 | 726 | ${CURL} ${OPTS} -s -b ${COOKIE} -A "${BROWSER}" -H "DNT: 1" -H "Connection: keep-alive" -L\ 727 | -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ 728 | -H "csrf: $(awk "\$0 ~/.${AMAZON}.*csrf[ \\s\\t]+/ {print \$7}" ${COOKIE})" -X PUT -d "${JSON}" \ 729 | "https://${ALEXA}/api/entertainment/v1/player/queue?deviceSerialNumber=${DEVICESERIALNUMBER}&deviceType=${DEVICETYPE}" 730 | } 731 | 732 | # 733 | # play library track 734 | # 735 | play_song() 736 | { 737 | if [ -z "${ALBUM}" ] ; then 738 | JSON="{\"trackId\":\"${SONG}\",\"playQueuePrime\":true}" 739 | else 740 | JSON="{\"albumArtistName\":\"${ARTIST}\",\"albumName\":\"${ALBUM}\"}" 741 | fi 742 | 743 | ${CURL} ${OPTS} -s -b ${COOKIE} -A "${BROWSER}" -H "DNT: 1" -H "Connection: keep-alive" -L\ 744 | -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ 745 | -H "csrf: $(awk "\$0 ~/.${AMAZON}.*csrf[ \\s\\t]+/ {print \$7}" ${COOKIE})" -X POST -d "${JSON}"\ 746 | "https://${ALEXA}/api/cloudplayer/queue-and-play?deviceSerialNumber=${DEVICESERIALNUMBER}&deviceType=${DEVICETYPE}&mediaOwnerCustomerId=${MEDIAOWNERCUSTOMERID}&shuffle=false" 747 | } 748 | 749 | # 750 | # play library playlist 751 | # 752 | play_playlist() 753 | { 754 | ${CURL} ${OPTS} -s -b ${COOKIE} -A "${BROWSER}" -H "DNT: 1" -H "Connection: keep-alive" -L\ 755 | -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ 756 | -H "csrf: $(awk "\$0 ~/.${AMAZON}.*csrf[ \\s\\t]+/ {print \$7}" ${COOKIE})" -X POST -d "{\"playlistId\":\"${PLIST}\",\"playQueuePrime\":true}"\ 757 | "https://${ALEXA}/api/cloudplayer/queue-and-play?deviceSerialNumber=${DEVICESERIALNUMBER}&deviceType=${DEVICETYPE}&mediaOwnerCustomerId=${MEDIAOWNERCUSTOMERID}&shuffle=false" 758 | } 759 | 760 | # 761 | # play PRIME playlist 762 | # 763 | play_prime_playlist() 764 | { 765 | ${CURL} ${OPTS} -s -b ${COOKIE} -A "${BROWSER}" -H "DNT: 1" -H "Connection: keep-alive" -L\ 766 | -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ 767 | -H "csrf: $(awk "\$0 ~/.${AMAZON}.*csrf[ \\s\\t]+/ {print \$7}" ${COOKIE})" -X POST -d "{\"asin\":\"${ASIN}\"}"\ 768 | "https://${ALEXA}/api/prime/prime-playlist-queue-and-play?deviceSerialNumber=${DEVICESERIALNUMBER}&deviceType=${DEVICETYPE}&mediaOwnerCustomerId=${MEDIAOWNERCUSTOMERID}" 769 | } 770 | 771 | # 772 | # play PRIME station 773 | # 774 | play_prime_station() 775 | { 776 | ${CURL} ${OPTS} -s -b ${COOKIE} -A "${BROWSER}" -H "DNT: 1" -H "Connection: keep-alive" -L\ 777 | -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ 778 | -H "csrf: $(awk "\$0 ~/.${AMAZON}.*csrf[ \\s\\t]+/ {print \$7}" ${COOKIE})" -X POST -d "{\"seed\":\"{\\\"type\\\":\\\"KEY\\\",\\\"seedId\\\":\\\"${SEEDID}\\\"}\",\"stationName\":\"none\",\"seedType\":\"KEY\"}"\ 779 | "https://${ALEXA}/api/gotham/queue-and-play?deviceSerialNumber=${DEVICESERIALNUMBER}&deviceType=${DEVICETYPE}&mediaOwnerCustomerId=${MEDIAOWNERCUSTOMERID}" 780 | } 781 | 782 | # 783 | # play PRIME historical queue 784 | # 785 | play_prime_hist_queue() 786 | { 787 | ${CURL} ${OPTS} -s -b ${COOKIE} -A "${BROWSER}" -H "DNT: 1" -H "Connection: keep-alive" -L\ 788 | -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ 789 | -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\"}"\ 790 | "https://${ALEXA}/api/media/play-historical-queue" 791 | } 792 | 793 | # 794 | # current queue 795 | # 796 | show_queue() 797 | { 798 | echo "/api/np/player" 799 | ${CURL} ${OPTS} -s -b ${COOKIE} -A "${BROWSER}" -H "DNT: 1" -H "Connection: keep-alive" -L\ 800 | -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ 801 | -H "csrf: $(awk "\$0 ~/.${AMAZON}.*csrf[ \\s\\t]+/ {print \$7}" ${COOKIE})" -X GET \ 802 | "https://${ALEXA}/api/np/player?deviceSerialNumber=${DEVICESERIALNUMBER}&deviceType=${DEVICETYPE}" 803 | echo 804 | echo "/api/np/queue" 805 | ${CURL} ${OPTS} -s -b ${COOKIE} -A "${BROWSER}" -H "DNT: 1" -H "Connection: keep-alive" -L\ 806 | -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ 807 | -H "csrf: $(awk "\$0 ~/.${AMAZON}.*csrf[ \\s\\t]+/ {print \$7}" ${COOKIE})" -X GET \ 808 | "https://${ALEXA}/api/np/queue?deviceSerialNumber=${DEVICESERIALNUMBER}&deviceType=${DEVICETYPE}" 809 | echo 810 | echo "/api/media/state" 811 | ${CURL} ${OPTS} -s -b ${COOKIE} -A "${BROWSER}" -H "DNT: 1" -H "Connection: keep-alive" -L\ 812 | -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ 813 | -H "csrf: $(awk "\$0 ~/.${AMAZON}.*csrf[ \\s\\t]+/ {print \$7}" ${COOKIE})" -X GET \ 814 | "https://${ALEXA}/api/media/state?deviceSerialNumber=${DEVICESERIALNUMBER}&deviceType=${DEVICETYPE}" 815 | echo 816 | } 817 | 818 | # 819 | # show notifications and alarms 820 | # 821 | show_notifications() 822 | { 823 | echo "/api/notifications" 824 | ${CURL} ${OPTS} -s -b ${COOKIE} -A "${BROWSER}" -H "DNT: 1" -H "Connection: keep-alive" -L\ 825 | -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ 826 | -H "csrf: $(awk "\$0 ~/.${AMAZON}.*csrf[ \\s\\t]+/ {print \$7}" ${COOKIE})" -X GET \ 827 | "https://${ALEXA}/api/notifications?deviceSerialNumber=${DEVICESERIALNUMBER}&deviceType=${DEVICETYPE}" 828 | echo 829 | } 830 | 831 | # 832 | # deletes a multiroom device 833 | # 834 | delete_multiroom() 835 | { 836 | ${CURL} ${OPTS} -s -b ${COOKIE} -A "${BROWSER}" -H "DNT: 1" -H "Connection: keep-alive" -L\ 837 | -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ 838 | -H "csrf: $(awk "\$0 ~/.${AMAZON}.*csrf[ \\s\\t]+/ {print \$7}" ${COOKIE})" -X DELETE \ 839 | "https://${ALEXA}/api/lemur/tail/${DEVICESERIALNUMBER}" 840 | } 841 | 842 | # 843 | # creates a multiroom device 844 | # 845 | create_multiroom() 846 | { 847 | JSON="{\"id\":null,\"name\":\"${LEMUR}\",\"members\":[" 848 | for DEVICE in $CHILD ; do 849 | set_var 850 | JSON="${JSON}{\"dsn\":\"${DEVICESERIALNUMBER}\",\"deviceType\":\"${DEVICETYPE}\"}," 851 | done 852 | JSON="${JSON%,}]}" 853 | 854 | ${CURL} ${OPTS} -s -b ${COOKIE} -A "${BROWSER}" -H "DNT: 1" -H "Connection: keep-alive" -L\ 855 | -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ 856 | -H "csrf: $(awk "\$0 ~/.${AMAZON}.*csrf[ \\s\\t]+/ {print \$7}" ${COOKIE})" -X POST -d "${JSON}" \ 857 | "https://${ALEXA}/api/lemur/tail" 858 | } 859 | 860 | # 861 | # list bluetooth devices 862 | # 863 | list_bluetooth() 864 | { 865 | ${CURL} ${OPTS} -s -b ${COOKIE} -A "${BROWSER}" -H "DNT: 1" -H "Connection: keep-alive" -L\ 866 | -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ 867 | -H "csrf: $(awk "\$0 ~/.${AMAZON}.*csrf[ \\s\\t]+/ {print \$7}" ${COOKIE})" -X GET \ 868 | "https://${ALEXA}/api/bluetooth?cached=false" 869 | } 870 | 871 | # 872 | # connect bluetooth device 873 | # 874 | connect_bluetooth() 875 | { 876 | ${CURL} ${OPTS} -s -b ${COOKIE} -A "${BROWSER}" -H "DNT: 1" -H "Connection: keep-alive" -L\ 877 | -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ 878 | -H "csrf: $(awk "\$0 ~/.${AMAZON}.*csrf[ \\s\\t]+/ {print \$7}" ${COOKIE})" -X POST -d "{\"bluetoothDeviceAddress\":\"${BLUETOOTH}\"}"\ 879 | "https://${ALEXA}/api/bluetooth/pair-sink/${DEVICETYPE}/${DEVICESERIALNUMBER}" 880 | } 881 | 882 | # 883 | # disconnect bluetooth device 884 | # 885 | disconnect_bluetooth() 886 | { 887 | ${CURL} ${OPTS} -s -b ${COOKIE} -A "${BROWSER}" -H "DNT: 1" -H "Connection: keep-alive" -L\ 888 | -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ 889 | -H "csrf: $(awk "\$0 ~/.${AMAZON}.*csrf[ \\s\\t]+/ {print \$7}" ${COOKIE})" -X POST \ 890 | "https://${ALEXA}/api/bluetooth/disconnect-sink/${DEVICETYPE}/${DEVICESERIALNUMBER}" 891 | } 892 | 893 | # 894 | # device that sent the last command 895 | # (by Markus Wennesheimer) 896 | # 897 | last_alexa() 898 | { 899 | ${CURL} ${OPTS} -s -b ${COOKIE} -A "${BROWSER}" -H "DNT: 1" -H "Connection: keep-alive" -L\ 900 | -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ 901 | -H "csrf: $(awk "\$0 ~/.${AMAZON}.*csrf[ \\s\\t]+/ {print \$7}" ${COOKIE})" -X GET \ 902 | "https://${ALEXA}/api/activities?startTime=&size=1&offset=1" | sed -r 's/^.*serialNumber":"([^"]+)".*$/\1/' 903 | } 904 | 905 | # 906 | # logout 907 | # 908 | log_off() 909 | { 910 | ${CURL} ${OPTS} -s -c ${COOKIE} -b ${COOKIE} -A "${BROWSER}" -H "DNT: 1" -H "Connection: keep-alive" -L\ 911 | https://${ALEXA}/logout > /dev/null 912 | 913 | rm -f ${DEVLIST} 914 | rm -f ${DEVTXT} 915 | rm -f ${DEVALL} 916 | rm -f ${COOKIE} 917 | } 918 | 919 | 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 920 | echo "only logout option present, logging off ..." 921 | log_off 922 | exit 0 923 | fi 924 | 925 | if [ ! -f ${COOKIE} ] ; then 926 | echo "cookie does not exist. logging in ..." 927 | log_in 928 | fi 929 | 930 | check_status 931 | if [ $? -eq 0 ] ; then 932 | echo "cookie expired, logging in again ..." 933 | log_in 934 | check_status 935 | if [ $? -eq 0 ] ; then 936 | echo "log in failed, aborting" 937 | exit 1 938 | fi 939 | fi 940 | 941 | if [ ! -f ${DEVTXT} -o ! -f ${DEVALL} ] ; then 942 | echo "device list does not exist. downloading ..." 943 | get_devlist 944 | if [ ! -f ${DEVTXT} ] ; then 945 | echo "failed to download device list, aborting" 946 | exit 1 947 | fi 948 | fi 949 | 950 | if [ -n "$LOGIN" ] ; then 951 | echo "logged in" 952 | exit 0 953 | fi 954 | 955 | if [ -n "$COMMAND" -o -n "$QUEUE" -o -n "$NOTIFICATIONS" ] ; then 956 | if [ "${DEVICE}" = "ALL" ] ; then 957 | while IFS= read -r DEVICE ; do 958 | set_var 959 | if [ "$DEVICESTATE" = "true" ] ; then 960 | if [ -n "$COMMAND" ] ; then 961 | echo "sending cmd:${COMMAND} to dev:${DEVICE} type:${DEVICETYPE} serial:${DEVICESERIALNUMBER} customerid:${MEDIAOWNERCUSTOMERID}" 962 | run_cmd 963 | # in order to prevent a "Rate exceeded" we need to delay the command 964 | sleep 1 965 | echo 966 | elif [ -n "$NOTIFICATIONS" ] ; then 967 | echo "notifications info for dev:${DEVICE} type:${DEVICETYPE} serial:${DEVICESERIALNUMBER}" 968 | show_notifications 969 | else 970 | echo "queue info for dev:${DEVICE} type:${DEVICETYPE} serial:${DEVICESERIALNUMBER}" 971 | show_queue 972 | echo 973 | fi 974 | fi 975 | done < ${DEVALL} 976 | else 977 | set_var 978 | if [ -n "$COMMAND" ] ; then 979 | echo "sending cmd:${COMMAND} to dev:${DEVICE} type:${DEVICETYPE} serial:${DEVICESERIALNUMBER} customerid:${MEDIAOWNERCUSTOMERID}" 980 | run_cmd 981 | echo 982 | elif [ -n "$NOTIFICATIONS" ] ; then 983 | echo "notifications info for dev:${DEVICE} type:${DEVICETYPE} serial:${DEVICESERIALNUMBER}" 984 | show_notifications 985 | else 986 | echo "queue info for dev:${DEVICE} type:${DEVICETYPE} serial:${DEVICESERIALNUMBER}" 987 | show_queue 988 | echo 989 | fi 990 | fi 991 | elif [ -n "$LEMUR" ] ; then 992 | DEVICE="${LEMUR}" 993 | set_var 994 | if [ -n "$DEVICESERIALNUMBER" ] ; then 995 | delete_multiroom 996 | fi 997 | if [ -z "$CHILD" ] ; then 998 | echo "Deleted multi room dev:${LEMUR} serial:${DEVICESERIALNUMBER}" 999 | else 1000 | echo "Creating multi room dev:${LEMUR} member_dev(s):${CHILD}" 1001 | create_multiroom 1002 | echo 1003 | fi 1004 | rm -f ${DEVLIST} 1005 | rm -f ${DEVALL} 1006 | rm -f ${DEVTXT} 1007 | get_devlist 1008 | elif [ -n "$BLUETOOTH" ] ; then 1009 | if [ "$BLUETOOTH" = "list" -o "$BLUETOOTH" = "List" -o "$BLUETOOTH" = "LIST" ] ; then 1010 | if [ "${DEVICE}" = "ALL" ] ; then 1011 | while IFS= read -r DEVICE ; do 1012 | set_var 1013 | echo "bluetooth api list:" 1014 | list_bluetooth 1015 | echo 1016 | done < ${DEVALL} 1017 | else 1018 | set_var 1019 | echo "bluetooth api list:" 1020 | list_bluetooth 1021 | echo 1022 | fi 1023 | elif [ "$BLUETOOTH" = "null" ] ; then 1024 | set_var 1025 | echo "disconnecting dev:${DEVICE} type:${DEVICETYPE} serial:${DEVICESERIALNUMBER} from bluetooth" 1026 | disconnect_bluetooth 1027 | echo 1028 | else 1029 | set_var 1030 | echo "connecting dev:${DEVICE} type:${DEVICETYPE} serial:${DEVICESERIALNUMBER} to bluetooth device:${BLUETOOTH}" 1031 | connect_bluetooth 1032 | echo 1033 | fi 1034 | elif [ -n "$STATIONID" ] ; then 1035 | set_var 1036 | echo "playing stationID:${STATIONID} on dev:${DEVICE} type:${DEVICETYPE} serial:${DEVICESERIALNUMBER} mediaownerid:${MEDIAOWNERCUSTOMERID}" 1037 | play_radio 1038 | elif [ -n "$SONG" ] ; then 1039 | set_var 1040 | echo "playing library track:${SONG} on dev:${DEVICE} type:${DEVICETYPE} serial:${DEVICESERIALNUMBER} mediaownerid:${MEDIAOWNERCUSTOMERID}" 1041 | play_song 1042 | elif [ -n "$PLIST" ] ; then 1043 | set_var 1044 | echo "playing library playlist:${PLIST} on dev:${DEVICE} type:${DEVICETYPE} serial:${DEVICESERIALNUMBER} mediaownerid:${MEDIAOWNERCUSTOMERID}" 1045 | play_playlist 1046 | elif [ -n "$ASIN" ] ; then 1047 | set_var 1048 | echo "playing PRIME playlist ${ASIN}" 1049 | play_prime_playlist 1050 | elif [ -n "$SEEDID" ] ; then 1051 | set_var 1052 | echo "playing PRIME station ${SEEDID}" 1053 | play_prime_station 1054 | elif [ -n "$HIST" ] ; then 1055 | set_var 1056 | echo "playing PRIME historical queue ${HIST}" 1057 | play_prime_hist_queue 1058 | elif [ -n "$LIST" ] ; then 1059 | ATTR="accountName" 1060 | echo "the following devices exist in your account:" 1061 | grep ${ATTR}\| ${DEVTXT} | sed "s/^.*${ATTR}|//" | sed 's/ /_/g' 1062 | elif [ -n "$LASTALEXA" ] ; then 1063 | last_alexa 1064 | else 1065 | echo "no alexa command received" 1066 | fi 1067 | 1068 | if [ -n "$LOGOFF" ] ; then 1069 | echo "logout option present, logging off ..." 1070 | log_off 1071 | fi 1072 | -------------------------------------------------------------------------------- /alexa_remote_control.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Amazon Alexa Remote Control 4 | # alex(at)loetzimmer.de 5 | # 6 | # 2017-10-10: v0.1 initial release 7 | # 2017-10-11: v0.2 TuneIn Station Search 8 | # 2017-10-11: v0.2a commands on special device "ALL" are executed on all ECHO+WHA 9 | # 2017-10-16: v0.3 added playback of library tracks 10 | # 2017-10-24: v0.4 added playback information 11 | # 2017-11-21: v0.5 added Prime station and playlist 12 | # 2017-11-22: v0.6 added Prime historical queue and replaced getopts 13 | # 2017-11-25: v0.6a cURL is now configurable 14 | # 2017-11-25: v0.7 added multiroom create/delete, playback of library playlist 15 | # 2017-11-30: v0.7a added US config, fixed device names containing spaces 16 | # 2017-12-07: v0.7b added Bluetooth connect/disconnect 17 | # 2017-12-18: v0.7c fixed US version 18 | # 2017-12-19: v0.7d fixed AWK csrf extraction on some systems 19 | # 2017-12-20: v0.7e moved get_devlist after check_status 20 | # 2018-01-08: v0.7f added echo-show to ALL group, TuneIn station can now be up to 6 digits 21 | # 2018-01-08: v0.8 added bluetooth list function 22 | # 2018-01-10: v0.8a abort when login was unsuccessful 23 | # 2018-01-25: v0.8b added echo-spot to ALL group 24 | # 2018-01-28: v0.8c added configurable browser string 25 | # 2018-02-17: v0.8d no need to write the cookie file on every "check_status" 26 | # 2018-02-27: v0.8e added "lastalexa" option for HA-Bridge to send its command to a specific device 27 | # (Markus Wennesheimer: https://wennez.wordpress.com/light-on-with-alexa-for-each-room/) 28 | # 2018-02-27: v0.9 unsuccessful logins will now give a short info how to debug the login 29 | # 2018-03-09: v0.9a workaround for login problem, force curl to use http1.1 30 | # 2018-05-17: v0.9b update browser string and accept language 31 | # 2018-05-23: v0.9c update accept language (again) 32 | # 2018-06-12: v0.10 introducing TTS and more 33 | # (thanks to Michael Geramb and his openHAB2 Amazon Echo Control binding) 34 | # https://github.com/openhab/openhab2-addons/tree/master/addons/binding/org.openhab.binding.amazonechocontrol 35 | # (thanks to Ralf Otto for implementing this feature in this script) 36 | # 2018-06-13: v0.10a added album play of imported library 37 | # 2018-06-18: v0.10b added Alexa routine execution 38 | # 2019-01-22: v0.11 added repeat command, added environment variable parsing 39 | # 2019-02-03: v0.11a fixed string escape for automation and speak commands 40 | # 2019-02-10: v0.12 added "-d ALL" to the plain version, lastalexa now checks for SUCCESS activityStatus 41 | # 2019-02-14: v0.12a reduced the number of replaced characters for TTS and automation 42 | # 2019-06-18: v0.12b fixed CSRF 43 | # 2019-06-28: v0.12c properly fixed CSRF 44 | # 2019-07-08: v0.13 added support for Multi-Factor Authentication 45 | # (thanks to rich-gepp https://github.com/rich-gepp) 46 | # 2019-08-05: v0.14 added Volume setting via routine, and $SPEAKVOL 47 | # 2019-11-18: v0.14a download 200 routines instead of only the first 20 48 | # 2019-12-23: v0.14b Trigger routines by either utterance or routine name 49 | # 2019-12-30: v0.15 re-worked the volume setting for TTS commands 50 | # 2020-01-03: v0.15a introduce some proper "get_volume" 51 | # 2020-01-08: v0.15b cleaned merge errors 52 | # 2020-02-03: v0.15c SPEAKVOL of 0 leaves the volume setting untouched 53 | # 2020-02-09: v0.16 TTS to Multiroom groups via USE_ANNOUNCEMENT_FOR_SPEAK + SSML for TTS 54 | # (!!! requires Announcement feature to be enabled in each device !!!) 55 | # 2020-02-09: v0.16a added sound library - only very few sounds are actually supported 56 | # ( https://developer.amazon.com/en-US/docs/alexa/custom-skills/ask-soundlibrary.html ) 57 | # 2020-06-15: v0.16b added "lastcommand" option 58 | # (thanks to Trinitus01 https://github.com/trinitus01) 59 | # 2020-07-07: v0.16c fixed NORMALVOL if USE_ANNOUNCEMENT_FOR_SPEAK is set 60 | # 2020-12-12: v0.17 added textcommand which lets you send anything via CLI you would otherwise say to Alexa 61 | # ( https://github.com/thorsten-gehrig/alexa-remote-control/issues/108 ) 62 | # 2020-12-12: v0.17a sounds now benefit from SPEAKVOL 63 | # fixed TuneIn IDs to also play podcasts 64 | # 2021-01-28: v0.17b fixed new API endpoint for automations 65 | # (thanks to Michael Winkler) 66 | # 2021-01-28: v0.17c simplified volume detection using new DeviceVolumes endpoint 67 | # (thanks to Ingo Fischer) 68 | # 2021-05-27: v0.18 complete rework of sequence commands especially for TTS 69 | # Announcement feature is no longer required due to inconsistent SSML handling 70 | # 2021-09-02: v0.19 Playing TuneIn works again using new entertainment API endpoint 71 | # Added playmusic (Alexa.Music.PlaySearchPhrase) as command, for available channels use "-c" 72 | # Note: playmusic is not multi-room capable, doing so might lead to unexpected results 73 | # 2021-09-13: v0.20 implemented device registration refresh_token cookie exchange flow as an alternative 74 | # to logging in 75 | # 2021-09-15: v0.20a optimized speak commands to use less JQ. This is useful in low-resource environments 76 | # 2021-10-07: v0.20b fixed different cookie naming for amazon.com 77 | # 2021-11-16: v0.20c fixed AlexaApp device selection: since they're all called "This Device" use corresponding 78 | # line in /tmp/.alexa.devicelist.txt, e.g.: -d "This Device=A2TF17PFR55MTB=ce0123456789abcdef01=VOX" 79 | # -lastalexa now returns this string. Make sure to put the device in double quotes! 80 | # 2022-02-04: v0.20d minor volume fix (write volume to volume cache when volume is changed) 81 | # 2022-06-29: v0.20e removed call to jq's strptime function, replaced with bash function using 'date' to convert to epoch 82 | # 2024-01-29: v0.21 removed legacy login methods as they were no longer working 83 | # implemented new API calls for -lastalexa and -lastcommand 84 | # there is now an OS-type switch that hopefully handles OSX and BSD date creation 85 | # 2024-01-31: v0.21a trying all different date options which come to mind (first working wins) 86 | # 2024-02-01: v0.21b changed the output of -lastalexa back to the output of devicelist.txt 87 | # 2024-04-06: v0.22 changed the date calculation once again, now the date processing ignores the actual cookie validity 88 | # and simply sets it to "now + COOKIE_LIFETIME" 89 | # 2025-11-07: v0.23 /api/bootstrap is gone, switched to /api/customer-status 90 | # (thanks once again to Ingo Fischer) 91 | # 92 | ### 93 | # 94 | # (no BASHisms were used, should run with any shell) 95 | # - requires cURL for web communication 96 | # - (GNU) sed and awk for extraction 97 | # - jq as command line JSON parser (optional for the fancy bits) 98 | # - base64 for B64 encoding (make sure "-w 0" option is available on your platform) 99 | # 100 | ########################################## 101 | 102 | # this can be obtained by doing the device registration login flow 103 | # e.g. from here: https://github.com/adn77/alexa-cookie-cli/ 104 | SET_REFRESH_TOKEN='' 105 | 106 | SET_TTS_LOCALE='de-DE' 107 | 108 | SET_AMAZON='amazon.de' 109 | #SET_AMAZON='amazon.com' 110 | 111 | SET_ALEXA='alexa.amazon.de' 112 | #SET_ALEXA='pitangui.amazon.com' 113 | 114 | # cURL binary 115 | SET_CURL='/usr/bin/curl' 116 | 117 | # cURL options 118 | # -k : if your cURL cannot verify CA certificates, you'll have to trust any 119 | # --compressed : if your cURL was compiled with libz you may use compression 120 | # --http1.1 : cURL defaults to HTTP/2 on HTTPS connections if available 121 | SET_OPTS='--compressed --http1.1' 122 | #SET_OPTS='-k --compressed --http1.1' 123 | 124 | # browser identity 125 | SET_BROWSER='Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:1.0) bash-script/1.0' 126 | #SET_BROWSER='Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:65.0) Gecko/20100101 Firefox/65.0' 127 | 128 | # jq binary 129 | SET_JQ='/usr/bin/jq' 130 | 131 | # tmp path 132 | SET_TMP="/tmp" 133 | 134 | # Volume for speak commands (a SPEAKVOL of 0 leaves the volume settings untouched) 135 | SET_SPEAKVOL="0" 136 | # if no current playing volume can be determined, fall back to normal volume 137 | SET_NORMALVOL="10" 138 | 139 | # Device specific volumes (overriding the above) 140 | # SET_DEVICEVOLNAME="EchoDot2ndGen Echo1stGen" 141 | # SET_DEVICEVOLSPEAK="100 30" 142 | # SET_DEVICEVOLNORMAL="100 20" 143 | SET_DEVICEVOLNAME="" 144 | SET_DEVICEVOLSPEAK="" 145 | SET_DEVICEVOLNORMAL="" 146 | 147 | # max. age in minutes before volume is read from API (local cache time) 148 | SET_VOLMAXAGE="1" 149 | 150 | ########################################### 151 | # nothing to configure below here 152 | # 153 | 154 | # retrieving environment variables if any are set 155 | REFRESH_TOKEN=${REFRESH_TOKEN:-$SET_REFRESH_TOKEN} 156 | AMAZON=${AMAZON:-$SET_AMAZON} 157 | ALEXA=${ALEXA:-$SET_ALEXA} 158 | BROWSER=${BROWSER:-$SET_BROWSER} 159 | CURL=${CURL:-$SET_CURL} 160 | OPTS=${OPTS:-$SET_OPTS} 161 | TTS_LOCALE=${TTS_LOCALE:-$SET_TTS_LOCALE} 162 | TMP=${TMP:-$SET_TMP} 163 | JQ=${JQ:-$SET_JQ} 164 | SPEAKVOL=${SPEAKVOL:-$SET_SPEAKVOL} 165 | NORMALVOL=${NORMALVOL:-$SET_NORMALVOL} 166 | VOLMAXAGE=${VOLMAXAGE:-$SET_VOLMAXAGE} 167 | DEVICEVOLNAME=${DEVICEVOLNAME:-$SET_DEVICEVOLNAME} 168 | DEVICEVOLSPEAK=${DEVICEVOLSPEAK:-$SET_DEVICEVOLSPEAK} 169 | DEVICEVOLNORMAL=${DEVICEVOLNORMAL:-$SET_DEVICEVOLNORMAL} 170 | 171 | COOKIE="${TMP}/.alexa.cookie" 172 | DEVLIST="${TMP}/.alexa.devicelist" 173 | COOKIE_LIFETIME=$(( 24 * 60 * 60 )) # default lifetime of one day before revalidation 174 | 175 | LIST="" 176 | LOGOFF="" 177 | COMMAND="" 178 | TTS="" 179 | UTTERANCE="" 180 | SEQUENCECMD="" 181 | SEQUENCEVAL="" 182 | SEARCHPHRASE="" 183 | PROVIDERID="" 184 | STATIONID="" 185 | CHANNEL="" 186 | QUEUE="" 187 | SONG="" 188 | ALBUM="" 189 | ARTIST="" 190 | TYPE="" 191 | ASIN="" 192 | SEEDID="" 193 | HIST="" 194 | LEMUR="" 195 | CHILD="" 196 | PLIST="" 197 | BLUETOOTH="" 198 | LASTALEXA="" 199 | LASTCOMMAND="" 200 | GETVOL="" 201 | NOTIFICATIONS="" 202 | 203 | usage() 204 | { 205 | echo "$0 [-d |ALL] -e > |" 206 | echo " -b [list|<\"AA:BB:CC:DD:EE:FF\">] | -q | -n | -r <\"station name\"|stationId> |" 207 | echo " -s | -t | -u | -v | -w |" 208 | echo " -i | -p | -P | -S | -a | -m [device_1 .. device_X] | -lastalexa | -lastcommand | -z | -l | -h" 209 | echo 210 | echo " -e : run command, additional SEQUENCECMDs:" 211 | echo " weather,traffic,flashbriefing,goodmorning,singasong,tellstory," 212 | echo " speak:'',automation:'',sound:," 213 | echo " textcommand:''," 214 | echo " playmusic::''" 215 | echo " -b : connect/disconnect/list bluetooth device" 216 | echo " -c : list 'playmusic' channels" 217 | echo " -q : query queue" 218 | echo " -n : query notifications" 219 | echo " -r : play tunein radio" 220 | echo " -s : play library track/library album" 221 | echo " -t : play Prime playlist" 222 | echo " -u : play Prime station" 223 | echo " -v : play Prime historical queue" 224 | echo " -w : play library playlist" 225 | echo " -i : list imported library tracks" 226 | echo " -p : list purchased library tracks" 227 | echo " -P : list Prime playlists" 228 | echo " -S : list Prime stations" 229 | echo " -a : list available devices" 230 | echo " -m : delete multiroom and/or create new multiroom containing devices" 231 | echo " -lastalexa : print device that received the last voice command" 232 | echo " -lastcommand : print last voice command or last voice command of specific device" 233 | echo " -z : print current volume level" 234 | echo " -login : Logs in, without further command" 235 | echo " -l : logoff" 236 | echo " -h : help" 237 | } 238 | 239 | while [ "$#" -gt 0 ] ; do 240 | case "$1" in 241 | --version) 242 | echo "v0.23" 243 | exit 0 244 | ;; 245 | -d) 246 | if [ "${2#-}" != "${2}" -o -z "$2" ] ; then 247 | echo "ERROR: missing argument for ${1}" 248 | usage 249 | exit 1 250 | fi 251 | DEVICE=$2 252 | shift 253 | ;; 254 | -e) 255 | if [ "${2#-}" != "${2}" -o -z "$2" ] ; then 256 | echo "ERROR: missing argument for ${1}" 257 | usage 258 | exit 1 259 | fi 260 | COMMAND=$2 261 | shift 262 | ;; 263 | -b) 264 | if [ "${2#-}" = "${2}" -a -n "$2" ] ; then 265 | BLUETOOTH=$2 266 | shift 267 | else 268 | BLUETOOTH="null" 269 | fi 270 | ;; 271 | -m) 272 | if [ "${2#-}" != "${2}" -o -z "$2" ] ; then 273 | echo "ERROR: missing argument for ${1}" 274 | usage 275 | exit 1 276 | fi 277 | LEMUR=$2 278 | shift 279 | while [ "${2#-}" = "${2}" -a -n "$2" ] ; do 280 | CHILD="${CHILD} ${2}" 281 | shift 282 | done 283 | ;; 284 | -r) 285 | if [ "${2#-}" != "${2}" -o -z "$2" ] ; then 286 | echo "ERROR: missing argument for ${1}" 287 | usage 288 | exit 1 289 | fi 290 | STATIONID=$2 291 | shift 292 | # stationIDs are "s1234" or "s12345" 293 | 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 294 | # search for station name 295 | 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') 296 | if [ -z "$STATIONID" ] ; then 297 | echo "ERROR: no Station \"$2\" found on TuneIn" 298 | exit 1 299 | fi 300 | fi 301 | ;; 302 | -s) 303 | if [ "${2#-}" != "${2}" -o -z "$2" ] ; then 304 | echo "ERROR: missing argument for ${1}" 305 | usage 306 | exit 1 307 | fi 308 | SONG=$2 309 | shift 310 | if [ "${2#-}" = "${2}" -a -n "$2" ] ; then 311 | ALBUM=$2 312 | ARTIST=$SONG 313 | shift 314 | fi 315 | ;; 316 | -t) 317 | if [ "${2#-}" != "${2}" -o -z "$2" ] ; then 318 | echo "ERROR: missing argument for ${1}" 319 | usage 320 | exit 1 321 | fi 322 | ASIN=$2 323 | shift 324 | ;; 325 | -u) 326 | if [ "${2#-}" != "${2}" -o -z "$2" ] ; then 327 | echo "ERROR: missing argument for ${1}" 328 | usage 329 | exit 1 330 | fi 331 | SEEDID=$2 332 | shift 333 | ;; 334 | -v) 335 | if [ "${2#-}" != "${2}" -o -z "$2" ] ; then 336 | echo "ERROR: missing argument for ${1}" 337 | usage 338 | exit 1 339 | fi 340 | HIST=$2 341 | shift 342 | ;; 343 | -w) 344 | if [ "${2#-}" != "${2}" -o -z "$2" ] ; then 345 | echo "ERROR: missing argument for ${1}" 346 | usage 347 | exit 1 348 | fi 349 | PLIST=$2 350 | shift 351 | ;; 352 | -login) 353 | LOGIN="true" 354 | ;; 355 | -l) 356 | LOGOFF="true" 357 | ;; 358 | -a) 359 | LIST="true" 360 | ;; 361 | -c) 362 | CHANNEL="true" 363 | ;; 364 | -i) 365 | TYPE="IMPORTED" 366 | ;; 367 | -p) 368 | TYPE="PURCHASES" 369 | ;; 370 | -P) 371 | PRIME="prime-playlist-browse-nodes" 372 | ;; 373 | -S) 374 | PRIME="prime-sections" 375 | ;; 376 | -q) 377 | QUEUE="true" 378 | ;; 379 | -n) 380 | NOTIFICATIONS="true" 381 | ;; 382 | -lastalexa) 383 | LASTALEXA="true" 384 | ;; 385 | -lastcommand) 386 | LASTCOMMAND="true" 387 | ;; 388 | -z) 389 | GETVOL="true" 390 | ;; 391 | -h|-\?|--help) 392 | usage 393 | exit 0 394 | ;; 395 | *) 396 | echo "ERROR: unknown option ${1}" 397 | usage 398 | exit 1 399 | ;; 400 | esac 401 | shift 402 | done 403 | 404 | case "$COMMAND" in 405 | pause) 406 | COMMAND='{"type":"PauseCommand"}' 407 | ;; 408 | play) 409 | COMMAND='{"type":"PlayCommand"}' 410 | ;; 411 | next) 412 | COMMAND='{"type":"NextCommand"}' 413 | ;; 414 | prev) 415 | COMMAND='{"type":"PreviousCommand"}' 416 | ;; 417 | fwd) 418 | COMMAND='{"type":"ForwardCommand"}' 419 | ;; 420 | rwd) 421 | COMMAND='{"type":"RewindCommand"}' 422 | ;; 423 | shuffle) 424 | COMMAND='{"type":"ShuffleCommand","shuffle":"true"}' 425 | ;; 426 | repeat) 427 | COMMAND='{"type":"RepeatCommand","repeat":true}' 428 | ;; 429 | vol:*) 430 | VOL=${COMMAND##*:} 431 | # volume as integer! 432 | if [ $VOL -le 100 -a $VOL -ge 0 ] ; then 433 | SEQUENCECMD='Alexa.DeviceControls.Volume' 434 | SEQUENCEVAL=',\"value\":\"'${VOL}'\"' 435 | else 436 | echo "ERROR: volume should be an integer between 0 and 100" 437 | usage 438 | exit 1 439 | fi 440 | ;; 441 | textcommand:*) 442 | SEQUENCECMD='Alexa.TextCommand\",\"skillId\":\"amzn1.ask.1p.tellalexa' 443 | SEQUENCEVAL=$(echo ${COMMAND##textcommand:} | sed s/\"/\'/g) 444 | SEQUENCEVAL=',\"text\":\"'${SEQUENCEVAL}'\"' 445 | ;; 446 | speak:*) 447 | TTS=$(echo ${COMMAND##speak:} | sed s/\"/\'/g) 448 | TTS=',\"textToSpeak\":\"'${TTS}'\"' 449 | SEQUENCECMD='Alexa.Speak' 450 | SEQUENCEVAL=$TTS 451 | ;; 452 | sound:*) 453 | SEQUENCECMD='Alexa.Sound' 454 | SEQUENCEVAL=',\"soundStringId\":\"'${COMMAND##sound:}'\"' 455 | ;; 456 | automation:*) 457 | SEQUENCECMD='automation' 458 | UTTERANCE=$(echo ${COMMAND##automation:} | sed -r 's/["\\]/ /g') 459 | ;; 460 | weather) 461 | SEQUENCECMD='Alexa.Weather.Play' 462 | ;; 463 | traffic) 464 | SEQUENCECMD='Alexa.Traffic.Play' 465 | ;; 466 | flashbriefing) 467 | SEQUENCECMD='Alexa.FlashBriefing.Play' 468 | ;; 469 | goodmorning) 470 | SEQUENCECMD='Alexa.GoodMorning.Play' 471 | ;; 472 | singasong) 473 | SEQUENCECMD='Alexa.SingASong.Play' 474 | ;; 475 | tellstory) 476 | SEQUENCECMD='Alexa.TellStory.Play' 477 | ;; 478 | playmusic:*) 479 | SEQUENCECMD='Alexa.Music.PlaySearchPhrase' 480 | PROVIDERID=${COMMAND#*:} 481 | PROVIDERID=${PROVIDERID%:*} 482 | SEQUENCEVAL=',\"musicProviderId\":\"'${PROVIDERID}'\",' 483 | SEARCHPHRASE=$(echo ${COMMAND##*:} | sed s/\"/\'/g) 484 | ;; 485 | "") 486 | ;; 487 | *) 488 | echo "ERROR: unknown command \"${COMMAND}\"!" 489 | usage 490 | exit 1 491 | ;; 492 | esac 493 | 494 | # 495 | # Amazon Login 496 | # 497 | log_in() 498 | { 499 | rm -f ${DEVLIST}.json 500 | rm -f ${COOKIE} 501 | rm -f ${TMP}/.alexa.*.list 502 | 503 | if [ -z "${REFRESH_TOKEN}" ] ; then 504 | echo "Sorry, the very thing this project started with, namely the reverse engineered" 505 | echo " login to the Amazon web page does no longer work. The Alexa login page has" 506 | echo " been shut down in favor of a much more modern login process." 507 | echo 508 | echo "Please use the device login process https://github.com/adn77/alexa-cookie-cli" 509 | echo " all you need is the 'refreshToken' looking sth. like 'Atnr|...'" 510 | 511 | exit 1 512 | else 513 | now=$(date +%s) 514 | exp=$(( now + COOKIE_LIFETIME )) 515 | 516 | # the date processing ignores the actual cookie validity and simply sets it to "now + COOKIE_LIFETIME" 517 | ${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 |\ 518 | ${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} 519 | 520 | if [ -z "$(grep "\.${AMAZON}.*\sat-" ${COOKIE})" ] ; then 521 | echo "ERROR: cookie retrieval with refresh_token didn't work" 522 | exit 1 523 | fi 524 | fi 525 | 526 | # 527 | # get CSRF 528 | # 529 | ${CURL} ${OPTS} -s -c ${COOKIE} -b ${COOKIE} -A "${BROWSER}" -H "DNT: 1" -H "Connection: keep-alive" -L\ 530 | -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ 531 | https://${ALEXA}/api/language > /dev/null 532 | 533 | if [ -z "$(grep "\.${AMAZON}.*\scsrf" ${COOKIE})" ] ; then 534 | echo "trying to get CSRF from handlebars" 535 | ${CURL} ${OPTS} -s -c ${COOKIE} -b ${COOKIE} -A "${BROWSER}" -H "DNT: 1" -H "Connection: keep-alive" -L\ 536 | -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ 537 | https://${ALEXA}/templates/oobe/d-device-pick.handlebars > /dev/null 538 | fi 539 | 540 | if [ -z "$(grep "\.${AMAZON}.*\scsrf" ${COOKIE})" ] ; then 541 | echo "trying to get CSRF from devices-v2" 542 | ${CURL} ${OPTS} -s -c ${COOKIE} -b ${COOKIE} -A "${BROWSER}" -H "DNT: 1" -H "Connection: keep-alive" -L\ 543 | -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ 544 | https://${ALEXA}/api/devices-v2/device?cached=false > /dev/null 545 | fi 546 | 547 | if [ -z "$(grep "\.${AMAZON}.*\scsrf" ${COOKIE})" ] ; then 548 | echo "ERROR: no CSRF cookie received" 549 | exit 1 550 | fi 551 | } 552 | 553 | # 554 | # get JSON device list 555 | # 556 | get_devlist() 557 | { 558 | ${CURL} ${OPTS} -s -b ${COOKIE} -A "${BROWSER}" -H "DNT: 1" -H "Connection: keep-alive" -L\ 559 | -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ 560 | -H "csrf: $(awk "\$0 ~/.${AMAZON}.*csrf[ \\s\\t]+/ {print \$7}" ${COOKIE})"\ 561 | "https://${ALEXA}/api/devices-v2/device?cached=false" > ${DEVLIST}.json 562 | 563 | ${JQ} -r '.devices[] | "\(.accountName)=\(.deviceType)=\(.serialNumber)=\(.deviceFamily)"' ${DEVLIST}.json > ${DEVLIST}.txt 564 | ${JQ} -r '.devices[] | select( .appDeviceList | length >0 ) as $p | .appDeviceList[] | "\($p.accountName)=\(.deviceType)=\(.serialNumber)=\($p.deviceFamily)"' ${DEVLIST}.json >> ${DEVLIST}.txt 565 | ${JQ} -r '.devices[] | select(.deviceFamily == "WHA") | "\(.accountName)=\(.clusterMembers[])"' ${DEVLIST}.json > ${DEVLIST}_wha.txt 566 | } 567 | 568 | check_status() 569 | { 570 | # 571 | # returns the current authentication state (HTTP/200) 572 | # 573 | 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) 574 | 575 | case $AUTHSTATUS in 576 | 200) 577 | MEDIAOWNERCUSTOMERID=$(${CURL} ${OPTS} -s -b ${COOKIE} -A "${BROWSER}" -H "DNT: 1" -H "Connection: keep-alive" -L https://${ALEXA}/api/users/me | ${JQ} -r '.id') 578 | return 1 579 | ;; 580 | 401|403) 581 | return 0 582 | ;; 583 | *) 584 | ;; 585 | esac 586 | echo "ERROR: /api/customer-status returned unexpected HTTP/${AUTHSTATUS}" 587 | } 588 | 589 | # 590 | # set device specific variables from JSON device list 591 | # 592 | set_var() 593 | { 594 | DEVICE=$(echo ${DEVICE} | sed -r 's/%20/ /g') 595 | 596 | if [ -z "${DEVICE}" ] ; then 597 | # if no device was supplied, use the first Echo(dot) in device list 598 | echo -n "setting default device to: " 599 | DEVICE=$(grep -m 1 -E "ECHO|KNIGHT|ROOK" ${DEVLIST}.txt | cut -d'=' -f1) 600 | echo ${DEVICE} 601 | fi 602 | 603 | DEVICESERIALNUMBER=$(grep -m 1 "${DEVICE}" ${DEVLIST}.txt) 604 | DEVICESERIALNUMBER=${DEVICESERIALNUMBER#*=} 605 | 606 | DEVICEFAMILY=${DEVICESERIALNUMBER##*=} 607 | DEVICETYPE=${DEVICESERIALNUMBER%%=*} 608 | DEVICESERIALNUMBER=${DEVICESERIALNUMBER#*=} 609 | DEVICESERIALNUMBER=${DEVICESERIALNUMBER%=*} 610 | 611 | # customerId is now retrieved from the logged in user 612 | # the customerId in the device list is always from the user registering the device initially 613 | # MEDIAOWNERCUSTOMERID=$(${JQ} --arg device "${DEVICE}" -r '.devices[] | select(.accountName == $device) | .deviceOwnerCustomerId' ${DEVLIST}.json) 614 | 615 | if [ -z "${DEVICESERIALNUMBER}" ] ; then 616 | echo "ERROR: unkown device dev:${DEVICE}" 617 | exit 1 618 | fi 619 | } 620 | 621 | # 622 | # list available devices from JSON device list 623 | # 624 | list_devices() 625 | { 626 | ${JQ} -r '.devices[].accountName' ${DEVLIST}.json 627 | } 628 | 629 | # 630 | # sanitize search phrase 631 | # ARG1 - sequence command (e.g. Alexa.Music.PlaySearchPhrase) 632 | # ARG2 - musicProviderID ( TUNEIN, AMASON_MUSIC, CLOUDPLAYER, SPOTIFY, APPLE_MUSIC, DEEZER, I_HEART_RADIO ) 633 | # ARG3 - search phrase 634 | # 635 | sanitize_search() 636 | { 637 | if [ -n "$1" -a -n "$2" -a -n "$3" ] ; then 638 | JSON='{"type":"'${1}'","operationPayload":"{\"locale\":\"'${TTS_LOCALE}'\",\"musicProviderId\":\"'${2}'\",\"searchPhrase\":\"'${3}'\"}"}' 639 | else 640 | JSON='{"type":"'${SEQUENCECMD}'","operationPayload":"{\"locale\":\"'${TTS_LOCALE}'\",\"musicProviderId\":\"'${PROVIDERID}'\",\"searchPhrase\":\"'${SEARCHPHRASE}'\"}"}' 641 | fi 642 | 643 | ${CURL} ${OPTS} -s -b ${COOKIE} -A "${BROWSER}" -H "DNT: 1" -H "Connection: keep-alive" -L\ 644 | -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ 645 | -H "csrf: $(awk "\$0 ~/.${AMAZON}.*csrf[ \\s\\t]+/ {print \$7}" ${COOKIE})" -X POST -d "${JSON}" \ 646 | "https://${ALEXA}/api/behaviors/operation/validate" | ${JQ} -r '.operationPayload.sanitizedSearchPhrase' 647 | } 648 | 649 | # 650 | # build node_to_execute string 651 | # ARG1 - SEQUENCECMD 652 | # ARG2 - SEQUENCEVAL 653 | # 654 | node() 655 | { 656 | if [ -n "$1" -a -n "$2" ] ; then 657 | echo '{\"@type\":\"com.amazon.alexa.behaviors.model.OpaquePayloadOperationNode\",\"type\":\"'${1}'\",\"operationPayload\":{\"deviceType\":\"'${DEVICETYPE}'\",\"deviceSerialNumber\":\"'${DEVICESERIALNUMBER}'\",\"customerId\":\"'${MEDIAOWNERCUSTOMERID}'\",\"locale\":\"'${TTS_LOCALE}'\"'${2}'}}' 658 | else 659 | echo '{\"@type\":\"com.amazon.alexa.behaviors.model.OpaquePayloadOperationNode\",\"type\":\"'${SEQUENCECMD}'\",\"operationPayload\":{\"deviceType\":\"'${DEVICETYPE}'\",\"deviceSerialNumber\":\"'${DEVICESERIALNUMBER}'\",\"customerId\":\"'${MEDIAOWNERCUSTOMERID}'\",\"locale\":\"'${TTS_LOCALE}'\"'${SEQUENCEVAL}'}}' 660 | fi 661 | } 662 | 663 | # 664 | # create comma separated string 665 | # 666 | add_node() 667 | { 668 | if [ -n "$1" ] ; then 669 | if [ -n "$2" ] ; then 670 | echo ${1}','${2} 671 | else 672 | echo ${1} 673 | fi 674 | fi 675 | } 676 | 677 | # 678 | # execute command 679 | # 680 | run_cmd() 681 | { 682 | if [ -n "${SEQUENCECMD}" ] ; then 683 | if [ "${SEQUENCECMD}" = 'automation' ] ; then 684 | 685 | ${CURL} ${OPTS} -s -b ${COOKIE} -A "${BROWSER}" -H "DNT: 1" -H "Connection: keep-alive" -L\ 686 | -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ 687 | -H "csrf: $(awk "\$0 ~/.${AMAZON}.*csrf[ \\s\\t]+/ {print \$7}" ${COOKIE})" -X GET \ 688 | "https://${ALEXA}/api/behaviors/v2/automations?limit=200" > "${TMP}/.alexa.automation" 689 | 690 | AUTOMATION=$(${JQ} --arg utterance "${UTTERANCE}" -r '.[] | select( .triggers[].payload.utterance == $utterance) | .automationId' "${TMP}/.alexa.automation") 691 | if [ -z "${AUTOMATION}" ] ; then 692 | AUTOMATION=$(${JQ} --arg utterance "${UTTERANCE}" -r '.[] | select( .name == $utterance) | .automationId' "${TMP}/.alexa.automation") 693 | if [ -z "${AUTOMATION}" ] ; then 694 | echo "ERROR: no such utterance '${UTTERANCE}' in Alexa routines" 695 | rm -f "${TMP}/.alexa.automation" 696 | exit 1 697 | fi 698 | fi 699 | 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") 700 | rm -f "${TMP}/.alexa.automation" 701 | 702 | ALEXACMD='{"behaviorId":"'${AUTOMATION}'","sequenceJson":"'${SEQUENCE}'","status":"ENABLED"}' 703 | else 704 | VOLUMEPRENODESTOEXECUTE='' 705 | VOLUMEPOSTNODESTOEXECUTE='' 706 | NODESTOEXECUTE='' 707 | 708 | # sanitize search phrase 709 | if [ -n "${SEARCHPHRASE}" -a -n "${PROVIDERID}" ] ; then 710 | SEQUENCEVAL=${SEQUENCEVAL}'\"searchPhrase\":\"'${SEARCHPHRASE}'\",\"sanitizedSearchPhrase\":\"'$(sanitize_search)'\"' 711 | fi 712 | 713 | # iterate over member devices if target is multiroom 714 | # !!! this is no true multi-room - it just tries to play on every member device in parallel !!! 715 | if [ "${DEVICEFAMILY}" = "WHA" ] ; then 716 | MEMBERDEVICESERIALS=$(grep "${DEVICE}" ${DEVLIST}_wha.txt | cut -d'=' -f 2) 717 | for DEVICESERIALNUMBER in $MEMBERDEVICESERIALS ; do 718 | DEVICETYPE=$(grep "${DEVICESERIALNUMBER}" ${DEVLIST}.txt | cut -d'=' -f 2) 719 | NODESTOEXECUTE=$(add_node "$(node)" "${NODESTOEXECUTE}") 720 | 721 | # if SequenceCommand is "Alexa.DeviceControls.Volume" we have to adjust the local volume cache 722 | if [ "$SEQUENCECMD" = "Alexa.DeviceControls.Volume" ] ; then 723 | VOL=${SEQUENCEVAL%\\\"} 724 | VOL=${VOL##*\\\"} 725 | if [ $VOL -gt 0 ] ; then 726 | echo $VOL false > "${TMP}/.alexa.volume.${DEVICESERIALNUMBER}" 727 | else 728 | echo 0 true > "${TMP}/.alexa.volume.${DEVICESERIALNUMBER}" 729 | fi 730 | # add volume setting per device - the WHA volume is unrelyable 731 | # don't set volume if Alexa.Music.PlaySearchPhrase is used 732 | elif [ \( $SPEAKVOL -gt 0 -o -n "${DEVICEVOLSPEAK}" \) -a "${SEQUENCECMD}" != "Alexa.Music.PlaySearchPhrase" ] ; then 733 | DEVICE=$(grep "${DEVICESERIALNUMBER}" ${DEVLIST}.txt | cut -d'=' -f 1) 734 | get_volumes 735 | VOLUMEPRENODESTOEXECUTE=$(add_node $(node Alexa.DeviceControls.Volume ',\"value\":\"'${SVOL}'\"') ${VOLUMEPRENODESTOEXECUTE}) 736 | VOLUMEPOSTNODESTOEXECUTE=$(add_node $(node Alexa.DeviceControls.Volume ',\"value\":\"'${VOL}'\"') ${VOLUMEPOSTNODESTOEXECUTE}) 737 | fi 738 | done 739 | 740 | if [ -z "${NODESTOEXECUTE}" ] ; then 741 | echo "No clusterMembers found for command: ${COMMAND} on dev:${DEVICE} type:${DEVICETYPE} serial:${DEVICESERIALNUMBER} family:${DEVICEFAMILY}" 742 | return 743 | fi 744 | else 745 | NODESTOEXECUTE=$(add_node "$(node)" "${NODESTOEXECUTE}") 746 | 747 | if [ "$SEQUENCECMD" = "Alexa.DeviceControls.Volume" ] ; then 748 | VOL=${SEQUENCEVAL%\\\"} 749 | VOL=${VOL##*\\\"} 750 | if [ $VOL -gt 0 ] ; then 751 | echo $VOL false > "${TMP}/.alexa.volume.${DEVICESERIALNUMBER}" 752 | else 753 | echo 0 true > "${TMP}/.alexa.volume.${DEVICESERIALNUMBER}" 754 | fi 755 | # don't set volume if Alexa.Music.PlaySearchPhrase is used 756 | elif [ \( $SPEAKVOL -gt 0 -o -n "${DEVICEVOLSPEAK}" \) -a "${SEQUENCECMD}" != "Alexa.Music.PlaySearchPhrase" ] ; then 757 | get_volumes 758 | VOLUMEPRENODESTOEXECUTE=$(add_node $(node Alexa.DeviceControls.Volume ',\"value\":\"'${SVOL}'\"') ${VOLUMEPRENODESTOEXECUTE}) 759 | VOLUMEPOSTNODESTOEXECUTE=$(add_node $(node Alexa.DeviceControls.Volume ',\"value\":\"'${VOL}'\"') ${VOLUMEPOSTNODESTOEXECUTE}) 760 | fi 761 | fi 762 | 763 | if [ -n "${VOLUMEPRENODESTOEXECUTE}" -a -n "${VOLUMEPOSTNODESTOEXECUTE}" ] ; then 764 | # execute serially "set_speak_volume" => "sequence_command" => "set_normal_volume" 765 | # (each subtask is executed in parallel) 766 | 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"}' 767 | else 768 | # execute in parallel "sequence_command" 769 | ALEXACMD='{"behaviorId":"PREVIEW","sequenceJson":"{\"@type\":\"com.amazon.alexa.behaviors.model.Sequence\",\"startNode\":{\"@type\":\"com.amazon.alexa.behaviors.model.ParallelNode\",\"nodesToExecute\":['${NODESTOEXECUTE}']}}","status":"ENABLED"}' 770 | fi 771 | fi 772 | 773 | # Due to some weird shell-escape-behavior the command has to be written to a file before POSTing it 774 | echo $ALEXACMD > "${TMP}/.alexa.cmd" 775 | 776 | ${CURL} ${OPTS} -s -b ${COOKIE} -A "${BROWSER}" -H "DNT: 1" -H "Connection: keep-alive" -L\ 777 | -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ 778 | -H "csrf: $(awk "\$0 ~/.${AMAZON}.*csrf[ \\s\\t]+/ {print \$7}" ${COOKIE})" -X POST -d @"${TMP}/.alexa.cmd" \ 779 | "https://${ALEXA}/api/behaviors/preview" 780 | 781 | rm -f "${TMP}/.alexa.cmd" 782 | else 783 | ${CURL} ${OPTS} -s -b ${COOKIE} -A "${BROWSER}" -H "DNT: 1" -H "Connection: keep-alive" -L\ 784 | -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ 785 | -H "csrf: $(awk "\$0 ~/.${AMAZON}.*csrf[ \\s\\t]+/ {print \$7}" ${COOKIE})" -X POST -d ${COMMAND}\ 786 | "https://${ALEXA}/api/np/command?deviceSerialNumber=${DEVICESERIALNUMBER}&deviceType=${DEVICETYPE}" 787 | fi 788 | } 789 | 790 | # 791 | # play TuneIn radio station 792 | # 793 | play_radio() 794 | { 795 | JSON='{"contentToken":"music:'$(echo '["music/tuneIn/stationId","'${STATIONID}'"]|{"previousPageId":"TuneIn_SEARCH"}'| base64 -w 0| base64 -w 0 )'"}' 796 | 797 | ${CURL} ${OPTS} -s -b ${COOKIE} -A "${BROWSER}" -H "DNT: 1" -H "Connection: keep-alive" -L\ 798 | -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ 799 | -H "csrf: $(awk "\$0 ~/.${AMAZON}.*csrf[ \\s\\t]+/ {print \$7}" ${COOKIE})" -X PUT -d "${JSON}" \ 800 | "https://${ALEXA}/api/entertainment/v1/player/queue?deviceSerialNumber=${DEVICESERIALNUMBER}&deviceType=${DEVICETYPE}" 801 | } 802 | 803 | # 804 | # play library track 805 | # 806 | play_song() 807 | { 808 | if [ -z "${ALBUM}" ] ; then 809 | JSON="{\"trackId\":\"${SONG}\",\"playQueuePrime\":true}" 810 | else 811 | JSON="{\"albumArtistName\":\"${ARTIST}\",\"albumName\":\"${ALBUM}\"}" 812 | fi 813 | 814 | ${CURL} ${OPTS} -s -b ${COOKIE} -A "${BROWSER}" -H "DNT: 1" -H "Connection: keep-alive" -L\ 815 | -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ 816 | -H "csrf: $(awk "\$0 ~/.${AMAZON}.*csrf[ \\s\\t]+/ {print \$7}" ${COOKIE})" -X POST -d "${JSON}"\ 817 | "https://${ALEXA}/api/cloudplayer/queue-and-play?deviceSerialNumber=${DEVICESERIALNUMBER}&deviceType=${DEVICETYPE}&mediaOwnerCustomerId=${MEDIAOWNERCUSTOMERID}&shuffle=false" 818 | } 819 | 820 | # 821 | # play library playlist 822 | # 823 | play_playlist() 824 | { 825 | ${CURL} ${OPTS} -s -b ${COOKIE} -A "${BROWSER}" -H "DNT: 1" -H "Connection: keep-alive" -L\ 826 | -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ 827 | -H "csrf: $(awk "\$0 ~/.${AMAZON}.*csrf[ \\s\\t]+/ {print \$7}" ${COOKIE})" -X POST -d "{\"playlistId\":\"${PLIST}\",\"playQueuePrime\":true}"\ 828 | "https://${ALEXA}/api/cloudplayer/queue-and-play?deviceSerialNumber=${DEVICESERIALNUMBER}&deviceType=${DEVICETYPE}&mediaOwnerCustomerId=${MEDIAOWNERCUSTOMERID}&shuffle=false" 829 | } 830 | 831 | # 832 | # play PRIME playlist 833 | # 834 | play_prime_playlist() 835 | { 836 | ${CURL} ${OPTS} -s -b ${COOKIE} -A "${BROWSER}" -H "DNT: 1" -H "Connection: keep-alive" -L\ 837 | -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ 838 | -H "csrf: $(awk "\$0 ~/.${AMAZON}.*csrf[ \\s\\t]+/ {print \$7}" ${COOKIE})" -X POST -d "{\"asin\":\"${ASIN}\"}"\ 839 | "https://${ALEXA}/api/prime/prime-playlist-queue-and-play?deviceSerialNumber=${DEVICESERIALNUMBER}&deviceType=${DEVICETYPE}&mediaOwnerCustomerId=${MEDIAOWNERCUSTOMERID}" 840 | } 841 | 842 | # 843 | # play PRIME station 844 | # 845 | play_prime_station() 846 | { 847 | ${CURL} ${OPTS} -s -b ${COOKIE} -A "${BROWSER}" -H "DNT: 1" -H "Connection: keep-alive" -L\ 848 | -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ 849 | -H "csrf: $(awk "\$0 ~/.${AMAZON}.*csrf[ \\s\\t]+/ {print \$7}" ${COOKIE})" -X POST -d "{\"seed\":\"{\\\"type\\\":\\\"KEY\\\",\\\"seedId\\\":\\\"${SEEDID}\\\"}\",\"stationName\":\"none\",\"seedType\":\"KEY\"}"\ 850 | "https://${ALEXA}/api/gotham/queue-and-play?deviceSerialNumber=${DEVICESERIALNUMBER}&deviceType=${DEVICETYPE}&mediaOwnerCustomerId=${MEDIAOWNERCUSTOMERID}" 851 | } 852 | 853 | # 854 | # play PRIME historical queue 855 | # 856 | play_prime_hist_queue() 857 | { 858 | ${CURL} ${OPTS} -s -b ${COOKIE} -A "${BROWSER}" -H "DNT: 1" -H "Connection: keep-alive" -L\ 859 | -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ 860 | -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\"}"\ 861 | "https://${ALEXA}/api/media/play-historical-queue" 862 | } 863 | 864 | # 865 | # show library tracks 866 | # 867 | show_library() 868 | { 869 | OFFSET=""; 870 | SIZE=50; 871 | TOTAL=0; 872 | FILE=${TMP}/.alexa.${TYPE}.list 873 | 874 | if [ ! -f ${FILE} ] ; then 875 | echo -n '{"playlist":{"entryList":[' > ${FILE} 876 | 877 | while [ 50 -le ${SIZE} ] ; do 878 | 879 | ${CURL} ${OPTS} -s -b ${COOKIE} -A "${BROWSER}" -H "DNT: 1" -H "Connection: keep-alive" -L\ 880 | -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ 881 | -H "csrf: $(awk "\$0 ~/.${AMAZON}.*csrf[ \\s\\t]+/ {print \$7}" ${COOKIE})" -X GET \ 882 | "https://${ALEXA}/api/cloudplayer/playlists/${TYPE}-V0-OBJECTID?deviceSerialNumber=${DEVICESERIALNUMBER}&deviceType=${DEVICETYPE}&size=${SIZE}&offset=${OFFSET}&mediaOwnerCustomerId=${MEDIAOWNERCUSTOMERID}" > ${FILE}.tmp 883 | 884 | OFFSET=$(${JQ} -r '.nextResultsToken' ${FILE}.tmp) 885 | SIZE=$(${JQ} -r '.playlist | .trackCount' ${FILE}.tmp) 886 | ${JQ} -r -c '.playlist | .entryList' ${FILE}.tmp >> ${FILE} 887 | echo "," >> ${FILE} 888 | TOTAL=$((TOTAL+SIZE)) 889 | done 890 | echo "[]],\"trackCount\":\"${TOTAL}\"}}" >> ${FILE} 891 | rm -f ${FILE}.tmp 892 | fi 893 | ${JQ} -r '.playlist.trackCount' ${FILE} 894 | ${JQ} '.playlist.entryList[] | .[]' ${FILE} 895 | } 896 | 897 | # 898 | # show Prime stations and playlists 899 | # 900 | show_prime() 901 | { 902 | FILE=${TMP}/.alexa.${PRIME}.list 903 | 904 | if [ ! -f ${FILE} ] ; then 905 | ${CURL} ${OPTS} -s -b ${COOKIE} -A "${BROWSER}" -H "DNT: 1" -H "Connection: keep-alive" -L\ 906 | -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ 907 | -H "csrf: $(awk "\$0 ~/.${AMAZON}.*csrf[ \\s\\t]+/ {print \$7}" ${COOKIE})" -X GET \ 908 | "https://${ALEXA}/api/prime/{$PRIME}?deviceSerialNumber=${DEVICESERIALNUMBER}&deviceType=${DEVICETYPE}&mediaOwnerCustomerId=${MEDIAOWNERCUSTOMERID}" > ${FILE} 909 | 910 | if [ "$PRIME" = "prime-playlist-browse-nodes" ] ; then 911 | for I in $(${JQ} -r '.primePlaylistBrowseNodeList[].subNodes[].nodeId' ${FILE} 2>/dev/null) ; do 912 | ${CURL} ${OPTS} -s -b ${COOKIE} -A "${BROWSER}" -H "DNT: 1" -H "Connection: keep-alive" -L\ 913 | -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ 914 | -H "csrf: $(awk "\$0 ~/.${AMAZON}.*csrf[ \\s\\t]+/ {print \$7}" ${COOKIE})" -X GET \ 915 | "https://${ALEXA}/api/prime/prime-playlists-by-browse-node?browseNodeId=${I}&deviceSerialNumber=${DEVICESERIALNUMBER}&deviceType=${DEVICETYPE}&mediaOwnerCustomerId=${MEDIAOWNERCUSTOMERID}" >> ${FILE} 916 | done 917 | fi 918 | fi 919 | ${JQ} '.' ${FILE} 920 | } 921 | 922 | # 923 | # current queue 924 | # 925 | show_queue() 926 | { 927 | PARENT="" 928 | PARENTID=$(${JQ} --arg device "${DEVICE}" -r '.devices[] | select(.accountName == $device) | .parentClusters[0]' ${DEVLIST}.json) 929 | if [ "$PARENTID" != "null" ] ; then 930 | PARENTDEVICE=$(${JQ} --arg serial ${PARENTID} -r '.devices[] | select(.serialNumber == $serial) | .deviceType' ${DEVLIST}.json) 931 | PARENT="&lemurId=${PARENTID}&lemurDeviceType=${PARENTDEVICE}" 932 | fi 933 | 934 | ${CURL} ${OPTS} -s -b ${COOKIE} -A "${BROWSER}" -H "DNT: 1" -H "Connection: keep-alive" -L\ 935 | -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ 936 | -H "csrf: $(awk "\$0 ~/.${AMAZON}.*csrf[ \\s\\t]+/ {print \$7}" ${COOKIE})" -X GET \ 937 | "https://${ALEXA}/api/np/player?deviceSerialNumber=${DEVICESERIALNUMBER}&deviceType=${DEVICETYPE}${PARENT}" | ${JQ} '.' 938 | 939 | ${CURL} ${OPTS} -s -b ${COOKIE} -A "${BROWSER}" -H "DNT: 1" -H "Connection: keep-alive" -L\ 940 | -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ 941 | -H "csrf: $(awk "\$0 ~/.${AMAZON}.*csrf[ \\s\\t]+/ {print \$7}" ${COOKIE})" -X GET \ 942 | "https://${ALEXA}/api/media/state?deviceSerialNumber=${DEVICESERIALNUMBER}&deviceType=${DEVICETYPE}" | ${JQ} '.' 943 | 944 | ${CURL} ${OPTS} -s -b ${COOKIE} -A "${BROWSER}" -H "DNT: 1" -H "Connection: keep-alive" -L\ 945 | -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ 946 | -H "csrf: $(awk "\$0 ~/.${AMAZON}.*csrf[ \\s\\t]+/ {print \$7}" ${COOKIE})" -X GET \ 947 | "https://${ALEXA}/api/np/queue?deviceSerialNumber=${DEVICESERIALNUMBER}&deviceType=${DEVICETYPE}" | ${JQ} '.' 948 | } 949 | 950 | get_music_channels() 951 | { 952 | ${CURL} ${OPTS} -s -b ${COOKIE} -A "${BROWSER}" -H "DNT: 1" -H "Connection: keep-alive" -L\ 953 | -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ 954 | -H "csrf: $(awk "\$0 ~/.${AMAZON}.*csrf[ \\s\\t]+/ {print \$7}" ${COOKIE})" -X GET \ 955 | "https://${ALEXA}/api/behaviors/entities?skillId=amzn1.ask.1p.music" | ${JQ} -r '.[] | select( .supportedProperties[] == "Alexa.Music.PlaySearchPhrase" ) | "\(.id) - \(.displayName) \(.description)"' 956 | } 957 | 958 | # 959 | # device specific SPEAKVOL/NORMALVOL (sets SVOL/VOL) 960 | # 961 | get_volumes() 962 | { 963 | VOL="" 964 | SVOL="" 965 | 966 | # Not using arrays here in order to be compatible with non-Bash 967 | # Get the list position of the current device type 968 | IDX=0 969 | for D in $DEVICEVOLNAME ; do 970 | if [ "${D}" = "${DEVICE}" ] ; then 971 | break; 972 | fi 973 | IDX=$((IDX+1)) 974 | done 975 | 976 | # get the speak volume at that position 977 | C=0 978 | for D in $DEVICEVOLSPEAK ; do 979 | if [ $C -eq $IDX ] ; then 980 | if [ -n "${D}" ] ; then SVOL=$D ; fi 981 | break 982 | fi 983 | C=$((C+1)) 984 | done 985 | if [ -z "${SVOL}" ] ; then 986 | SVOL=$SPEAKVOL 987 | fi 988 | 989 | # try to retrieve the "currently playing" volume 990 | VOLMAXAGE=1 991 | VOL=$(get_volume) 992 | 993 | if [ -z "${VOL}" ] ; then 994 | # get the normal volume of the current device type 995 | C=0 996 | for D in $DEVICEVOLNORMAL; do 997 | if [ $C -eq $IDX ] ; then 998 | VOL=$D 999 | break 1000 | fi 1001 | C=$((C+1)) 1002 | done 1003 | # if the volume is still undefined, use $NORMALVOL 1004 | if [ -z "${VOL}" ] ; then 1005 | VOL=$NORMALVOL 1006 | fi 1007 | fi 1008 | 1009 | } 1010 | 1011 | # 1012 | # current volume level 1013 | # 1014 | get_volume() 1015 | { 1016 | VOLFILE=$(find "${TMP}/.alexa.volume.${DEVICESERIALNUMBER}" -mmin -${VOLMAXAGE} 2>/dev/null) 1017 | if [ -z "${VOLFILE}" ] ; then 1018 | VOL=$(${CURL} ${OPTS} -s -b ${COOKIE} -A "${BROWSER}" -H "DNT: 1" -H "Connection: keep-alive" -L\ 1019 | -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ 1020 | -H "csrf: $(awk "\$0 ~/.${AMAZON}.*csrf[ \\s\\t]+/ {print \$7}" ${COOKIE})" -X GET \ 1021 | "https://${ALEXA}/api/devices/deviceType/dsn/audio/v1/allDeviceVolumes" | ${JQ} -r --arg device "${DEVICESERIALNUMBER}" '.volumes[] | "\(.dsn) \(.speakerVolume) \(.speakerMuted)"') 1022 | 1023 | if [ -n "${VOL}" ] ; then 1024 | # write volume and mute state to file 1025 | OIFS=$IFS 1026 | IFS=' 1027 | ' 1028 | set -o noglob 1029 | for LINE in $VOL ; do 1030 | SERIAL=$(echo "${LINE}" | cut -d' ' -f1) 1031 | VOLUME=$(echo "${LINE}" | cut -d' ' -f2) 1032 | MUTED=$(echo "${LINE}" | cut -d' ' -f3) 1033 | echo "${VOLUME} ${MUTED}" > "${TMP}/.alexa.volume.${SERIAL}" 1034 | done 1035 | IFS=$OIFS 1036 | cut -d' ' -f1 "${TMP}/.alexa.volume.${DEVICESERIALNUMBER}" 1037 | fi 1038 | else 1039 | cut -d' ' -f1 "${TMP}/.alexa.volume.${DEVICESERIALNUMBER}" 1040 | fi 1041 | } 1042 | 1043 | # 1044 | # show notifications and alarms 1045 | # 1046 | show_notifications() 1047 | { 1048 | echo "/api/notifications" 1049 | ${CURL} ${OPTS} -s -b ${COOKIE} -A "${BROWSER}" -H "DNT: 1" -H "Connection: keep-alive" -L\ 1050 | -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ 1051 | -H "csrf: $(awk "\$0 ~/.${AMAZON}.*csrf[ \\s\\t]+/ {print \$7}" ${COOKIE})" -X GET \ 1052 | "https://${ALEXA}/api/notifications?deviceSerialNumber=${DEVICESERIALNUMBER}&deviceType=${DEVICETYPE}" 1053 | echo 1054 | } 1055 | 1056 | # 1057 | # deletes a multiroom device 1058 | # 1059 | delete_multiroom() 1060 | { 1061 | ${CURL} ${OPTS} -s -b ${COOKIE} -A "${BROWSER}" -H "DNT: 1" -H "Connection: keep-alive" -L\ 1062 | -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ 1063 | -H "csrf: $(awk "\$0 ~/.${AMAZON}.*csrf[ \\s\\t]+/ {print \$7}" ${COOKIE})" -X DELETE \ 1064 | "https://${ALEXA}/api/lemur/tail/${DEVICESERIALNUMBER}" 1065 | } 1066 | 1067 | # 1068 | # creates a multiroom device 1069 | # 1070 | create_multiroom() 1071 | { 1072 | JSON="{\"id\":null,\"name\":\"${LEMUR}\",\"members\":[" 1073 | for DEVICE in $CHILD ; do 1074 | set_var 1075 | JSON="${JSON}{\"dsn\":\"${DEVICESERIALNUMBER}\",\"deviceType\":\"${DEVICETYPE}\"}," 1076 | done 1077 | JSON="${JSON%,}]}" 1078 | 1079 | ${CURL} ${OPTS} -s -b ${COOKIE} -A "${BROWSER}" -H "DNT: 1" -H "Connection: keep-alive" -L\ 1080 | -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ 1081 | -H "csrf: $(awk "\$0 ~/.${AMAZON}.*csrf[ \\s\\t]+/ {print \$7}" ${COOKIE})" -X POST -d "${JSON}" \ 1082 | "https://${ALEXA}/api/lemur/tail" 1083 | } 1084 | 1085 | # 1086 | # list bluetooth devices 1087 | # 1088 | list_bluetooth() 1089 | { 1090 | ${CURL} ${OPTS} -s -b ${COOKIE} -A "${BROWSER}" -H "DNT: 1" -H "Connection: keep-alive" -L\ 1091 | -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ 1092 | -H "csrf: $(awk "\$0 ~/.${AMAZON}.*csrf[ \\s\\t]+/ {print \$7}" ${COOKIE})" -X GET \ 1093 | "https://${ALEXA}/api/bluetooth?cached=false" | ${JQ} --arg serial "${DEVICESERIALNUMBER}" -r '.bluetoothStates[] | select(.deviceSerialNumber == $serial) | "\(.pairedDeviceList[]?.address) \(.pairedDeviceList[]?.friendlyName)"' 1094 | } 1095 | 1096 | # 1097 | # connect bluetooth device 1098 | # 1099 | connect_bluetooth() 1100 | { 1101 | ${CURL} ${OPTS} -s -b ${COOKIE} -A "${BROWSER}" -H "DNT: 1" -H "Connection: keep-alive" -L\ 1102 | -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ 1103 | -H "csrf: $(awk "\$0 ~/.${AMAZON}.*csrf[ \\s\\t]+/ {print \$7}" ${COOKIE})" -X POST -d "{\"bluetoothDeviceAddress\":\"${BLUETOOTH}\"}"\ 1104 | "https://${ALEXA}/api/bluetooth/pair-sink/${DEVICETYPE}/${DEVICESERIALNUMBER}" 1105 | } 1106 | 1107 | # 1108 | # disconnect bluetooth device 1109 | # 1110 | disconnect_bluetooth() 1111 | { 1112 | ${CURL} ${OPTS} -s -b ${COOKIE} -A "${BROWSER}" -H "DNT: 1" -H "Connection: keep-alive" -L\ 1113 | -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ 1114 | -H "csrf: $(awk "\$0 ~/.${AMAZON}.*csrf[ \\s\\t]+/ {print \$7}" ${COOKIE})" -X POST \ 1115 | "https://${ALEXA}/api/bluetooth/disconnect-sink/${DEVICETYPE}/${DEVICESERIALNUMBER}" 1116 | } 1117 | 1118 | # 1119 | # get activity CSRF token 1120 | # 1121 | get_activity_csrf() 1122 | { 1123 | ${CURL} ${OPTS} -s -b ${COOKIE} -A "${BROWSER}" -H "DNT: 1" -H "Connection: keep-alive" -L\ 1124 | -H "Content-Type: application/json; charset=UTF-8" \ 1125 | -H "csrf: $(awk "\$0 ~/.${AMAZON}.*csrf[ \\s\\t]+/ {print \$7}" ${COOKIE})" -X GET\ 1126 | "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 1127 | } 1128 | 1129 | # 1130 | # get customer history records 1131 | # 1132 | get_history() 1133 | { 1134 | if ! [ -f ${TMP}/.alexa.activity.csrf ] ; then 1135 | get_activity_csrf 1136 | fi 1137 | 1138 | RES=$(${CURL} ${OPTS} -s -b ${COOKIE} -A "${BROWSER}" -H "DNT: 1" -H "Connection: keep-alive" -L -w "%{http_code}" \ 1139 | -H "Content-Type: application/json; charset=UTF-8" -H "anti-csrftoken-a2z: $(cat ${TMP}/.alexa.activity.csrf)" \ 1140 | -H "csrf: $(awk "\$0 ~/.${AMAZON}.*csrf[ \\s\\t]+/ {print \$7}" ${COOKIE})" -X POST -d '{"previousRequestToken": null}'\ 1141 | "https://www.${AMAZON}/alexa-privacy/apd/rvh/customer-history-records-v2/?startTime=0&endTime=2147483647000&pageType=VOICE_HISTORY" -o ${TMP}/.alexa.activity.json) 1142 | 1143 | # try again in case CSRF timed out 1144 | if [ $RES -ne 200 ] ; then 1145 | if [ -z "${try}" ] ; then 1146 | try=1 1147 | rm -f ${TMP}/.alexa.activity.csrf 1148 | get_history 1149 | else 1150 | echo "ERROR: unable to retrieve customer history records" 1151 | exit 1 1152 | fi 1153 | fi 1154 | } 1155 | 1156 | # 1157 | # device that sent the last command 1158 | # 1159 | last_alexa() 1160 | { 1161 | get_history 1162 | ${JQ} -r '.customerHistoryRecords | sort_by(.timestamp) | reverse | .[0] | .recordKey' ${TMP}/.alexa.activity.json | cut -d'#' -f4 | xargs -i grep -m 1 {} ${DEVLIST}.txt 1163 | } 1164 | # 1165 | # last command or last command of a specific device 1166 | # 1167 | last_command() 1168 | { 1169 | get_history 1170 | 1171 | if [ -z "$DEVICE" ] ; then 1172 | ${JQ} -r --arg device "$DEVICE" '.customerHistoryRecords | sort_by(.timestamp) | reverse | .[0] | .voiceHistoryRecordItems | map({key: .recordItemType, value: .transcriptText})' ${TMP}/.alexa.activity.json 1173 | else 1174 | ${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 1175 | fi 1176 | } 1177 | 1178 | # 1179 | # logout 1180 | # 1181 | log_off() 1182 | { 1183 | ${CURL} ${OPTS} -s -c ${COOKIE} -b ${COOKIE} -A "${BROWSER}" -H "DNT: 1" -H "Connection: keep-alive" -L\ 1184 | https://${ALEXA}/logout > /dev/null 1185 | 1186 | rm -f ${DEVLIST}.json 1187 | rm -f ${DEVLIST}.txt 1188 | rm -f ${DEVLIST}_wha.txt 1189 | rm -f ${COOKIE} 1190 | rm -f ${TMP}/.alexa.*.list 1191 | rm -f ${TMP}/.alexa.volume.* 1192 | } 1193 | 1194 | 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 1195 | echo "only logout option present, logging off ..." 1196 | log_off 1197 | exit 0 1198 | fi 1199 | 1200 | if [ ! -f ${COOKIE} ] ; then 1201 | echo "cookie does not exist. logging in ..." 1202 | log_in 1203 | fi 1204 | 1205 | check_status 1206 | if [ $? -eq 0 ] ; then 1207 | echo "cookie expired, logging in again ..." 1208 | log_in 1209 | check_status 1210 | if [ $? -eq 0 ] ; then 1211 | echo "log in failed, aborting" 1212 | exit 1 1213 | fi 1214 | fi 1215 | 1216 | if [ ! -f ${DEVLIST}.json -o ! -f ${DEVLIST}.txt ] ; then 1217 | echo "device list does not exist. downloading ..." 1218 | get_devlist 1219 | if [ ! -f ${DEVLIST}.json ] ; then 1220 | echo "failed to download device list, aborting" 1221 | exit 1 1222 | fi 1223 | fi 1224 | 1225 | if [ -n "$LOGIN" ] ; then 1226 | echo "logged in" 1227 | exit 0 1228 | fi 1229 | 1230 | if [ -n "$CHANNEL" ] ; then 1231 | get_music_channels 1232 | exit 0 1233 | fi 1234 | 1235 | if [ -n "$COMMAND" -o -n "$QUEUE" -o -n "$NOTIFICATIONS" -o -n "$GETVOL" ] ; then 1236 | if [ "${DEVICE}" = "ALL" ] ; then 1237 | 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 1238 | set_var 1239 | if [ -n "$COMMAND" ] ; then 1240 | echo "sending cmd:${COMMAND} to dev:${DEVICE} type:${DEVICETYPE} serial:${DEVICESERIALNUMBER} customerid:${MEDIAOWNERCUSTOMERID}" 1241 | run_cmd 1242 | # in order to prevent a "Rate exceeded" we need to delay the command 1243 | sleep 1 1244 | echo 1245 | elif [ -n "$GETVOL" ] ; then 1246 | echo "get volume for dev:${DEVICE} type:${DEVICETYPE} serial:${DEVICESERIALNUMBER}" 1247 | get_volume 1248 | elif [ -n "$NOTIFICATIONS" ] ; then 1249 | echo "notifications info for dev:${DEVICE} type:${DEVICETYPE} serial:${DEVICESERIALNUMBER}" 1250 | show_notifications 1251 | else 1252 | echo "queue info for dev:${DEVICE} type:${DEVICETYPE} serial:${DEVICESERIALNUMBER}" 1253 | show_queue 1254 | echo 1255 | fi 1256 | done 1257 | else 1258 | set_var 1259 | if [ -n "$COMMAND" ] ; then 1260 | echo "sending cmd:${COMMAND} to dev:${DEVICE} type:${DEVICETYPE} serial:${DEVICESERIALNUMBER} customerid:${MEDIAOWNERCUSTOMERID}" 1261 | run_cmd 1262 | echo 1263 | elif [ -n "$GETVOL" ] ; then 1264 | echo "get volume for dev:${DEVICE} type:${DEVICETYPE} serial:${DEVICESERIALNUMBER}" 1265 | get_volume 1266 | elif [ -n "$NOTIFICATIONS" ] ; then 1267 | echo "notifications info for dev:${DEVICE} type:${DEVICETYPE} serial:${DEVICESERIALNUMBER}" 1268 | show_notifications 1269 | else 1270 | echo "queue info for dev:${DEVICE} type:${DEVICETYPE} serial:${DEVICESERIALNUMBER}" 1271 | show_queue 1272 | echo 1273 | fi 1274 | fi 1275 | elif [ -n "$LEMUR" ] ; then 1276 | DEVICESERIALNUMBER=$(${JQ} --arg device "${LEMUR}" -r '.devices[] | select(.accountName == $device and .deviceFamily == "WHA") | .serialNumber' ${DEVLIST}.json) 1277 | if [ -n "$DEVICESERIALNUMBER" ] ; then 1278 | delete_multiroom 1279 | else 1280 | if [ -z "$CHILD" ] ; then 1281 | echo "ERROR: ${LEMUR} is no multiroom device. Cannot delete ${LEMUR}". 1282 | exit 1 1283 | fi 1284 | fi 1285 | if [ -z "$CHILD" ] ; then 1286 | echo "Deleted multi room dev:${LEMUR} serial:${DEVICESERIALNUMBER}" 1287 | else 1288 | echo "Creating multi room dev:${LEMUR} member_dev(s):${CHILD}" 1289 | create_multiroom 1290 | echo 1291 | fi 1292 | rm -f ${DEVLIST}.json 1293 | rm -f ${DEVLIST}.txt 1294 | rm -f ${DEVLIST}_wha.txt 1295 | get_devlist 1296 | elif [ -n "$BLUETOOTH" ] ; then 1297 | if [ "$BLUETOOTH" = "list" -o "$BLUETOOTH" = "List" -o "$BLUETOOTH" = "LIST" ] ; then 1298 | if [ "${DEVICE}" = "ALL" ] ; then 1299 | 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 1300 | set_var 1301 | echo "bluetooth devices for dev:${DEVICE} type:${DEVICETYPE} serial:${DEVICESERIALNUMBER}:" 1302 | list_bluetooth 1303 | echo 1304 | done 1305 | else 1306 | set_var 1307 | echo "bluetooth devices for dev:${DEVICE} type:${DEVICETYPE} serial:${DEVICESERIALNUMBER}:" 1308 | list_bluetooth 1309 | echo 1310 | fi 1311 | elif [ "$BLUETOOTH" = "null" ] ; then 1312 | set_var 1313 | echo "disconnecting dev:${DEVICE} type:${DEVICETYPE} serial:${DEVICESERIALNUMBER} from bluetooth" 1314 | disconnect_bluetooth 1315 | echo 1316 | else 1317 | set_var 1318 | echo "connecting dev:${DEVICE} type:${DEVICETYPE} serial:${DEVICESERIALNUMBER} to bluetooth device:${BLUETOOTH}" 1319 | connect_bluetooth 1320 | echo 1321 | fi 1322 | elif [ -n "$STATIONID" ] ; then 1323 | set_var 1324 | echo "playing stationID:${STATIONID} on dev:${DEVICE} type:${DEVICETYPE} serial:${DEVICESERIALNUMBER} mediaownerid:${MEDIAOWNERCUSTOMERID}" 1325 | play_radio 1326 | elif [ -n "$SONG" ] ; then 1327 | set_var 1328 | echo "playing library track:${SONG} on dev:${DEVICE} type:${DEVICETYPE} serial:${DEVICESERIALNUMBER} mediaownerid:${MEDIAOWNERCUSTOMERID}" 1329 | play_song 1330 | elif [ -n "$PLIST" ] ; then 1331 | set_var 1332 | echo "playing library playlist:${PLIST} on dev:${DEVICE} type:${DEVICETYPE} serial:${DEVICESERIALNUMBER} mediaownerid:${MEDIAOWNERCUSTOMERID}" 1333 | play_playlist 1334 | elif [ -n "$LIST" ] ; then 1335 | echo "the following devices exist in your account:" 1336 | list_devices 1337 | elif [ -n "$TYPE" ] ; then 1338 | set_var 1339 | echo -n "the following songs exist in your ${TYPE} library: " 1340 | show_library 1341 | elif [ -n "$PRIME" ] ; then 1342 | set_var 1343 | echo "the following songs exist in your PRIME ${PRIME}:" 1344 | show_prime 1345 | elif [ -n "$ASIN" ] ; then 1346 | set_var 1347 | echo "playing PRIME playlist ${ASIN}" 1348 | play_prime_playlist 1349 | elif [ -n "$SEEDID" ] ; then 1350 | set_var 1351 | echo "playing PRIME station ${SEEDID}" 1352 | play_prime_station 1353 | elif [ -n "$HIST" ] ; then 1354 | set_var 1355 | echo "playing PRIME historical queue ${HIST}" 1356 | play_prime_hist_queue 1357 | elif [ -n "$LASTALEXA" ] ; then 1358 | last_alexa 1359 | elif [ -n "$LASTCOMMAND" ] ; then 1360 | last_command 1361 | else 1362 | echo "no alexa command received" 1363 | fi 1364 | 1365 | if [ -n "$LOGOFF" ] ; then 1366 | echo "logout option present, logging off ..." 1367 | log_off 1368 | fi 1369 | --------------------------------------------------------------------------------