├── .gitignore ├── LICENSE ├── README.md ├── package-lock.json ├── package.json └── src ├── .DS_Store ├── api └── PlayerServices.js ├── index.js ├── integration └── player │ ├── Mixer.js │ ├── Player.js │ ├── Playlist.js │ └── SongPlayed.js └── slim-server-wrapper └── SlimHelper.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /src/api/token.js 3 | /package-lock.json -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rest-api-squeezebox 2 | 3 | ## Abstract 4 | This REST API, which runs on nodejs, is a translation of the slimserver / logitech squeezebox server Web RPC API. 5 | Why I do that ? Just beacause it'll be simpler to make mobile app, or actually for me it's to have nicer http requests in IFTTT that I use with my google home mini. I use it to say "Musique dans la salle" or "Chanson suivante" or "Volume à 75 dans la salle" and it's fun ;-) 6 | 7 | ## Notice 8 | To install, just run "npm install". 9 | To launch the API, run "npm start". 10 | 11 | But, it is very possible you have to change the "script" in your case. 12 | 13 | In fact, notice in package.json the start script is "node src/index.js 192.168.1.12:2311 2312". Usualy, the squeezebox server is 9000, not 2311, and your squeezebox sever is certainly not on 192.168.1.12. 14 | Personnaly, I don't use the default port (9000) for my server, just for security reason. 15 | 16 | 2312 in the command line means that the API will be accessible in the 2312 port. Of course, you can change it for 8080. 17 | 18 | Notice too a file (src/api/token.js) is not shared on github. You have to create this file which must contain the variable "token". It is used like a password. You will add it in every http request with the query param "token" (like GET /players?token=toto). So the file token.js is like : 19 | ``` 20 | exports.token = "what-a-token"; 21 | ``` 22 | It's not a good solution for security. It is just for waiting for a better solution. 23 | 24 | ## Endpoint that you can use 25 | 26 | ### GET /players to know your players on your multi-room logitech squeezebox system 27 | Get players informations. The array returned looks like : 28 | ``` 29 | [ 30 | { 31 | "name": "Salle chacha", 32 | "uuid": "********************************", 33 | "id": "**:**:**:**:**:**", 34 | "ip": "192.168.*.*:*****", 35 | "model": "Squeezebox Touch", 36 | "firmware_version": "7.8.0-r16754", 37 | "signal_strength": 88, 38 | "mixer": { 39 | "volume": "42", 40 | "bass": "50", 41 | "treble": "50", 42 | "power": "on" 43 | }, 44 | "play_state": "pause", 45 | "song_currently_played": { 46 | "index_in_playlist" : 3, 47 | "seconds_played": 183.890504037857, 48 | "duration": "258.466", 49 | "artist": "The Smashing Pumpkins", 50 | "album": "Mellon Collie and the Infinite Sadness (2012 - Remaster)", 51 | "title": "Bullet With Butterfly Wings", 52 | "is_remote": true, 53 | "path": "spotify://track:4qMzPtAZe0C9KWpWIzvZAP" 54 | } 55 | }, 56 | { 57 | "name": "Musique salle de bain", 58 | "uuid": "********************************", 59 | "id": "**:**:**:**:**:**", 60 | "ip": "192.168.*.*:*****", 61 | "model": "Squeezebox Radio", 62 | "firmware_version": "7.7.3-r16676", 63 | "signal_strength": 88, 64 | "mixer": { 65 | "volume": "42", 66 | "bass": "50", 67 | "treble": "50", 68 | "power": "on" 69 | }, 70 | "play_state": "pause", 71 | "song_currently_played": { 72 | "index_in_playlist" : 3, 73 | "seconds_played": 183.890504037857, 74 | "duration": "258.466", 75 | "artist": "The Smashing Pumpkins", 76 | "album": "Mellon Collie and the Infinite Sadness (2012 - Remaster)", 77 | "title": "Bullet With Butterfly Wings", 78 | "is_remote": true, 79 | "path": "spotify://track:4qMzPtAZe0C9KWpWIzvZAP" 80 | } 81 | } 82 | ] 83 | ``` 84 | 85 | ### GET /players/{uuid} just to have the information to display 86 | Get informations for one player. The object returned looks like : 87 | ``` 88 | { 89 | "name": "Musique salle de bain", 90 | "uuid": "********************************", 91 | "id": "**:**:**:**:**:**", 92 | "ip": "192.168.*.*:*****", 93 | "model": "Squeezebox Radio", 94 | "firmware_version": "7.7.3-r16676", 95 | "signal_strength": 88, 96 | "mixer": { 97 | "volume": "42", 98 | "bass": "50", 99 | "treble": "50", 100 | "power": "on" 101 | }, 102 | "play_state": "pause", 103 | "song_currently_played": { 104 | "index_in_playlist" : 3, 105 | "seconds_played": 183.890504037857, 106 | "duration": "258.466", 107 | "artist": "The Smashing Pumpkins", 108 | "album": "Mellon Collie and the Infinite Sadness (2012 - Remaster)", 109 | "title": "Bullet With Butterfly Wings", 110 | "is_remote": true, 111 | "path": "spotify://track:4qMzPtAZe0C9KWpWIzvZAP" 112 | } 113 | } 114 | ``` 115 | 116 | ### PATCH /players/{uuid} to play or stop your music, or to change the song to play 117 | Actually, you just can change the value of play_state and the index_in_playlist of the song_currently_played object. So the body of the request could look like : 118 | ``` 119 | { 120 | "play_state": "play", // can be play, pause or stop 121 | "song_currently_played" : { 122 | "index_in_playlist" : 4 123 | } 124 | } 125 | ``` 126 | If you change the value of play_state of you player, it will play or stop the music on your player. 127 | Notice that it's possible to change the song played for the next in playlist, if you send "+1" for song_currently_played.index_in_playlist. 128 | 129 | ### PATCH /players/{uuid}/mixer to turn off or on a player, or just to change the volume 130 | PATCH /players/{uuid}/mixer is to patch the mixer :-). So you can use it to turn on or off your player, or change volume, bass and treble. For example, if you want turn off your player, you can send : 131 | ``` 132 | { 133 | "power": "off" 134 | } 135 | ``` 136 | Or, if you want to change the volume : 137 | ``` 138 | { 139 | "volume": "15" 140 | } 141 | ``` 142 | Refer to the GET /players/{uuid} to see what is the mixer object. 143 | 144 | 145 | ### PATCH /players/{uuid}/playlist to change the playlist to play on the player 146 | You can change the playlist to play in different ways. 147 | 1. With the path of a playlist. 148 | 2. With the name of an artist or the title of an album, which will be used for a search on your slimserver. 149 | 150 | For the first way, the request object just contains one attibute : "path", for example : 151 | ``` 152 | { 153 | "path" : "spotify://user:legrosmanu:playlist:40xqGToCIq7PLn4CdZSNAO" 154 | } 155 | ``` 156 | For the second way, the request object contains "album_title" or "artist_name" attributes, for example : 157 | ``` 158 | { 159 | "album_title" : "black album" 160 | } 161 | or 162 | { 163 | "artist_name" : "metallica" 164 | } 165 | ``` 166 | 167 | ### DELETE /player/{uuid}/playlist to remove all the tracks on the playlist 168 | Just clear all the tracks with the DELETE verb. -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rest-api-squeezebox", 3 | "version": "1.1.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "accepts": { 8 | "version": "1.3.5", 9 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz", 10 | "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=", 11 | "requires": { 12 | "mime-types": "~2.1.18", 13 | "negotiator": "0.6.1" 14 | } 15 | }, 16 | "ajv": { 17 | "version": "5.5.2", 18 | "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", 19 | "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", 20 | "requires": { 21 | "co": "^4.6.0", 22 | "fast-deep-equal": "^1.0.0", 23 | "fast-json-stable-stringify": "^2.0.0", 24 | "json-schema-traverse": "^0.3.0" 25 | } 26 | }, 27 | "array-flatten": { 28 | "version": "1.1.1", 29 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", 30 | "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" 31 | }, 32 | "asn1": { 33 | "version": "0.2.3", 34 | "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", 35 | "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=" 36 | }, 37 | "assert-plus": { 38 | "version": "1.0.0", 39 | "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", 40 | "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" 41 | }, 42 | "asynckit": { 43 | "version": "0.4.0", 44 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", 45 | "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" 46 | }, 47 | "aws-sign2": { 48 | "version": "0.7.0", 49 | "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", 50 | "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" 51 | }, 52 | "aws4": { 53 | "version": "1.7.0", 54 | "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.7.0.tgz", 55 | "integrity": "sha512-32NDda82rhwD9/JBCCkB+MRYDp0oSvlo2IL6rQWA10PQi7tDUM3eqMSltXmY+Oyl/7N3P3qNtAlv7X0d9bI28w==" 56 | }, 57 | "bcrypt-pbkdf": { 58 | "version": "1.0.2", 59 | "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", 60 | "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", 61 | "optional": true, 62 | "requires": { 63 | "tweetnacl": "^0.14.3" 64 | } 65 | }, 66 | "body-parser": { 67 | "version": "1.18.2", 68 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.2.tgz", 69 | "integrity": "sha1-h2eKGdhLR9hZuDGZvVm84iKxBFQ=", 70 | "requires": { 71 | "bytes": "3.0.0", 72 | "content-type": "~1.0.4", 73 | "debug": "2.6.9", 74 | "depd": "~1.1.1", 75 | "http-errors": "~1.6.2", 76 | "iconv-lite": "0.4.19", 77 | "on-finished": "~2.3.0", 78 | "qs": "6.5.1", 79 | "raw-body": "2.3.2", 80 | "type-is": "~1.6.15" 81 | } 82 | }, 83 | "bytes": { 84 | "version": "3.0.0", 85 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", 86 | "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" 87 | }, 88 | "caseless": { 89 | "version": "0.12.0", 90 | "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", 91 | "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" 92 | }, 93 | "co": { 94 | "version": "4.6.0", 95 | "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", 96 | "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" 97 | }, 98 | "combined-stream": { 99 | "version": "1.0.6", 100 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", 101 | "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", 102 | "requires": { 103 | "delayed-stream": "~1.0.0" 104 | } 105 | }, 106 | "content-disposition": { 107 | "version": "0.5.2", 108 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", 109 | "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=" 110 | }, 111 | "content-type": { 112 | "version": "1.0.4", 113 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", 114 | "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" 115 | }, 116 | "cookie": { 117 | "version": "0.3.1", 118 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", 119 | "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" 120 | }, 121 | "cookie-signature": { 122 | "version": "1.0.6", 123 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", 124 | "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" 125 | }, 126 | "core-util-is": { 127 | "version": "1.0.2", 128 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", 129 | "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" 130 | }, 131 | "dashdash": { 132 | "version": "1.14.1", 133 | "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", 134 | "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", 135 | "requires": { 136 | "assert-plus": "^1.0.0" 137 | } 138 | }, 139 | "debug": { 140 | "version": "2.6.9", 141 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 142 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 143 | "requires": { 144 | "ms": "2.0.0" 145 | } 146 | }, 147 | "delayed-stream": { 148 | "version": "1.0.0", 149 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", 150 | "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" 151 | }, 152 | "depd": { 153 | "version": "1.1.2", 154 | "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", 155 | "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" 156 | }, 157 | "destroy": { 158 | "version": "1.0.4", 159 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", 160 | "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" 161 | }, 162 | "ecc-jsbn": { 163 | "version": "0.1.1", 164 | "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", 165 | "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", 166 | "optional": true, 167 | "requires": { 168 | "jsbn": "~0.1.0" 169 | } 170 | }, 171 | "ee-first": { 172 | "version": "1.1.1", 173 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 174 | "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" 175 | }, 176 | "encodeurl": { 177 | "version": "1.0.2", 178 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", 179 | "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" 180 | }, 181 | "escape-html": { 182 | "version": "1.0.3", 183 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 184 | "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" 185 | }, 186 | "etag": { 187 | "version": "1.8.1", 188 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", 189 | "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" 190 | }, 191 | "express": { 192 | "version": "4.16.3", 193 | "resolved": "https://registry.npmjs.org/express/-/express-4.16.3.tgz", 194 | "integrity": "sha1-avilAjUNsyRuzEvs9rWjTSL37VM=", 195 | "requires": { 196 | "accepts": "~1.3.5", 197 | "array-flatten": "1.1.1", 198 | "body-parser": "1.18.2", 199 | "content-disposition": "0.5.2", 200 | "content-type": "~1.0.4", 201 | "cookie": "0.3.1", 202 | "cookie-signature": "1.0.6", 203 | "debug": "2.6.9", 204 | "depd": "~1.1.2", 205 | "encodeurl": "~1.0.2", 206 | "escape-html": "~1.0.3", 207 | "etag": "~1.8.1", 208 | "finalhandler": "1.1.1", 209 | "fresh": "0.5.2", 210 | "merge-descriptors": "1.0.1", 211 | "methods": "~1.1.2", 212 | "on-finished": "~2.3.0", 213 | "parseurl": "~1.3.2", 214 | "path-to-regexp": "0.1.7", 215 | "proxy-addr": "~2.0.3", 216 | "qs": "6.5.1", 217 | "range-parser": "~1.2.0", 218 | "safe-buffer": "5.1.1", 219 | "send": "0.16.2", 220 | "serve-static": "1.13.2", 221 | "setprototypeof": "1.1.0", 222 | "statuses": "~1.4.0", 223 | "type-is": "~1.6.16", 224 | "utils-merge": "1.0.1", 225 | "vary": "~1.1.2" 226 | } 227 | }, 228 | "extend": { 229 | "version": "3.0.2", 230 | "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", 231 | "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" 232 | }, 233 | "extsprintf": { 234 | "version": "1.3.0", 235 | "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", 236 | "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" 237 | }, 238 | "fast-deep-equal": { 239 | "version": "1.1.0", 240 | "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", 241 | "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=" 242 | }, 243 | "fast-json-stable-stringify": { 244 | "version": "2.0.0", 245 | "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", 246 | "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" 247 | }, 248 | "finalhandler": { 249 | "version": "1.1.1", 250 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz", 251 | "integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==", 252 | "requires": { 253 | "debug": "2.6.9", 254 | "encodeurl": "~1.0.2", 255 | "escape-html": "~1.0.3", 256 | "on-finished": "~2.3.0", 257 | "parseurl": "~1.3.2", 258 | "statuses": "~1.4.0", 259 | "unpipe": "~1.0.0" 260 | } 261 | }, 262 | "forever-agent": { 263 | "version": "0.6.1", 264 | "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", 265 | "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" 266 | }, 267 | "form-data": { 268 | "version": "2.3.2", 269 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.2.tgz", 270 | "integrity": "sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=", 271 | "requires": { 272 | "asynckit": "^0.4.0", 273 | "combined-stream": "1.0.6", 274 | "mime-types": "^2.1.12" 275 | } 276 | }, 277 | "forwarded": { 278 | "version": "0.1.2", 279 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", 280 | "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" 281 | }, 282 | "fresh": { 283 | "version": "0.5.2", 284 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", 285 | "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" 286 | }, 287 | "getpass": { 288 | "version": "0.1.7", 289 | "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", 290 | "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", 291 | "requires": { 292 | "assert-plus": "^1.0.0" 293 | } 294 | }, 295 | "har-schema": { 296 | "version": "2.0.0", 297 | "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", 298 | "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" 299 | }, 300 | "har-validator": { 301 | "version": "5.0.3", 302 | "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz", 303 | "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=", 304 | "requires": { 305 | "ajv": "^5.1.0", 306 | "har-schema": "^2.0.0" 307 | } 308 | }, 309 | "http-errors": { 310 | "version": "1.6.3", 311 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", 312 | "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", 313 | "requires": { 314 | "depd": "~1.1.2", 315 | "inherits": "2.0.3", 316 | "setprototypeof": "1.1.0", 317 | "statuses": ">= 1.4.0 < 2" 318 | } 319 | }, 320 | "http-signature": { 321 | "version": "1.2.0", 322 | "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", 323 | "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", 324 | "requires": { 325 | "assert-plus": "^1.0.0", 326 | "jsprim": "^1.2.2", 327 | "sshpk": "^1.7.0" 328 | } 329 | }, 330 | "iconv-lite": { 331 | "version": "0.4.19", 332 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", 333 | "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==" 334 | }, 335 | "inherits": { 336 | "version": "2.0.3", 337 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 338 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" 339 | }, 340 | "ipaddr.js": { 341 | "version": "1.8.0", 342 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.8.0.tgz", 343 | "integrity": "sha1-6qM9bd16zo9/b+DJygRA5wZzix4=" 344 | }, 345 | "is-typedarray": { 346 | "version": "1.0.0", 347 | "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", 348 | "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" 349 | }, 350 | "isstream": { 351 | "version": "0.1.2", 352 | "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", 353 | "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" 354 | }, 355 | "jsbn": { 356 | "version": "0.1.1", 357 | "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", 358 | "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", 359 | "optional": true 360 | }, 361 | "json-schema": { 362 | "version": "0.2.3", 363 | "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", 364 | "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" 365 | }, 366 | "json-schema-traverse": { 367 | "version": "0.3.1", 368 | "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", 369 | "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=" 370 | }, 371 | "json-stringify-safe": { 372 | "version": "5.0.1", 373 | "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", 374 | "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" 375 | }, 376 | "jsprim": { 377 | "version": "1.4.1", 378 | "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", 379 | "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", 380 | "requires": { 381 | "assert-plus": "1.0.0", 382 | "extsprintf": "1.3.0", 383 | "json-schema": "0.2.3", 384 | "verror": "1.10.0" 385 | } 386 | }, 387 | "media-typer": { 388 | "version": "0.3.0", 389 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", 390 | "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" 391 | }, 392 | "merge-descriptors": { 393 | "version": "1.0.1", 394 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", 395 | "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" 396 | }, 397 | "methods": { 398 | "version": "1.1.2", 399 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", 400 | "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" 401 | }, 402 | "mime": { 403 | "version": "1.4.1", 404 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", 405 | "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==" 406 | }, 407 | "mime-db": { 408 | "version": "1.35.0", 409 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.35.0.tgz", 410 | "integrity": "sha512-JWT/IcCTsB0Io3AhWUMjRqucrHSPsSf2xKLaRldJVULioggvkJvggZ3VXNNSRkCddE6D+BUI4HEIZIA2OjwIvg==" 411 | }, 412 | "mime-types": { 413 | "version": "2.1.19", 414 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.19.tgz", 415 | "integrity": "sha512-P1tKYHVSZ6uFo26mtnve4HQFE3koh1UWVkp8YUC+ESBHe945xWSoXuHHiGarDqcEZ+whpCDnlNw5LON0kLo+sw==", 416 | "requires": { 417 | "mime-db": "~1.35.0" 418 | } 419 | }, 420 | "ms": { 421 | "version": "2.0.0", 422 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 423 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 424 | }, 425 | "negotiator": { 426 | "version": "0.6.1", 427 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", 428 | "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" 429 | }, 430 | "oauth-sign": { 431 | "version": "0.8.2", 432 | "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", 433 | "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=" 434 | }, 435 | "on-finished": { 436 | "version": "2.3.0", 437 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", 438 | "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", 439 | "requires": { 440 | "ee-first": "1.1.1" 441 | } 442 | }, 443 | "parseurl": { 444 | "version": "1.3.2", 445 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", 446 | "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=" 447 | }, 448 | "path-to-regexp": { 449 | "version": "0.1.7", 450 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", 451 | "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" 452 | }, 453 | "performance-now": { 454 | "version": "2.1.0", 455 | "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", 456 | "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" 457 | }, 458 | "proxy-addr": { 459 | "version": "2.0.4", 460 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.4.tgz", 461 | "integrity": "sha512-5erio2h9jp5CHGwcybmxmVqHmnCBZeewlfJ0pex+UW7Qny7OOZXTtH56TGNyBizkgiOwhJtMKrVzDTeKcySZwA==", 462 | "requires": { 463 | "forwarded": "~0.1.2", 464 | "ipaddr.js": "1.8.0" 465 | } 466 | }, 467 | "punycode": { 468 | "version": "1.4.1", 469 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", 470 | "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" 471 | }, 472 | "qs": { 473 | "version": "6.5.1", 474 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", 475 | "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==" 476 | }, 477 | "range-parser": { 478 | "version": "1.2.0", 479 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", 480 | "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=" 481 | }, 482 | "raw-body": { 483 | "version": "2.3.2", 484 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.2.tgz", 485 | "integrity": "sha1-vNYMd9Prk83gBQKVw/N5OJvIj4k=", 486 | "requires": { 487 | "bytes": "3.0.0", 488 | "http-errors": "1.6.2", 489 | "iconv-lite": "0.4.19", 490 | "unpipe": "1.0.0" 491 | }, 492 | "dependencies": { 493 | "depd": { 494 | "version": "1.1.1", 495 | "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz", 496 | "integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k=" 497 | }, 498 | "http-errors": { 499 | "version": "1.6.2", 500 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz", 501 | "integrity": "sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=", 502 | "requires": { 503 | "depd": "1.1.1", 504 | "inherits": "2.0.3", 505 | "setprototypeof": "1.0.3", 506 | "statuses": ">= 1.3.1 < 2" 507 | } 508 | }, 509 | "setprototypeof": { 510 | "version": "1.0.3", 511 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz", 512 | "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=" 513 | } 514 | } 515 | }, 516 | "request": { 517 | "version": "2.87.0", 518 | "resolved": "https://registry.npmjs.org/request/-/request-2.87.0.tgz", 519 | "integrity": "sha512-fcogkm7Az5bsS6Sl0sibkbhcKsnyon/jV1kF3ajGmF0c8HrttdKTPRT9hieOaQHA5HEq6r8OyWOo/o781C1tNw==", 520 | "requires": { 521 | "aws-sign2": "~0.7.0", 522 | "aws4": "^1.6.0", 523 | "caseless": "~0.12.0", 524 | "combined-stream": "~1.0.5", 525 | "extend": "~3.0.1", 526 | "forever-agent": "~0.6.1", 527 | "form-data": "~2.3.1", 528 | "har-validator": "~5.0.3", 529 | "http-signature": "~1.2.0", 530 | "is-typedarray": "~1.0.0", 531 | "isstream": "~0.1.2", 532 | "json-stringify-safe": "~5.0.1", 533 | "mime-types": "~2.1.17", 534 | "oauth-sign": "~0.8.2", 535 | "performance-now": "^2.1.0", 536 | "qs": "~6.5.1", 537 | "safe-buffer": "^5.1.1", 538 | "tough-cookie": "~2.3.3", 539 | "tunnel-agent": "^0.6.0", 540 | "uuid": "^3.1.0" 541 | } 542 | }, 543 | "safe-buffer": { 544 | "version": "5.1.1", 545 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", 546 | "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" 547 | }, 548 | "safer-buffer": { 549 | "version": "2.1.2", 550 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 551 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 552 | }, 553 | "send": { 554 | "version": "0.16.2", 555 | "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz", 556 | "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==", 557 | "requires": { 558 | "debug": "2.6.9", 559 | "depd": "~1.1.2", 560 | "destroy": "~1.0.4", 561 | "encodeurl": "~1.0.2", 562 | "escape-html": "~1.0.3", 563 | "etag": "~1.8.1", 564 | "fresh": "0.5.2", 565 | "http-errors": "~1.6.2", 566 | "mime": "1.4.1", 567 | "ms": "2.0.0", 568 | "on-finished": "~2.3.0", 569 | "range-parser": "~1.2.0", 570 | "statuses": "~1.4.0" 571 | } 572 | }, 573 | "serve-static": { 574 | "version": "1.13.2", 575 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz", 576 | "integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==", 577 | "requires": { 578 | "encodeurl": "~1.0.2", 579 | "escape-html": "~1.0.3", 580 | "parseurl": "~1.3.2", 581 | "send": "0.16.2" 582 | } 583 | }, 584 | "setprototypeof": { 585 | "version": "1.1.0", 586 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", 587 | "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" 588 | }, 589 | "sshpk": { 590 | "version": "1.14.2", 591 | "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.2.tgz", 592 | "integrity": "sha1-xvxhZIo9nE52T9P8306hBeSSupg=", 593 | "requires": { 594 | "asn1": "~0.2.3", 595 | "assert-plus": "^1.0.0", 596 | "bcrypt-pbkdf": "^1.0.0", 597 | "dashdash": "^1.12.0", 598 | "ecc-jsbn": "~0.1.1", 599 | "getpass": "^0.1.1", 600 | "jsbn": "~0.1.0", 601 | "safer-buffer": "^2.0.2", 602 | "tweetnacl": "~0.14.0" 603 | } 604 | }, 605 | "statuses": { 606 | "version": "1.4.0", 607 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", 608 | "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==" 609 | }, 610 | "tough-cookie": { 611 | "version": "2.3.4", 612 | "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz", 613 | "integrity": "sha512-TZ6TTfI5NtZnuyy/Kecv+CnoROnyXn2DN97LontgQpCwsX2XyLYCC0ENhYkehSOwAp8rTQKc/NUIF7BkQ5rKLA==", 614 | "requires": { 615 | "punycode": "^1.4.1" 616 | } 617 | }, 618 | "tunnel-agent": { 619 | "version": "0.6.0", 620 | "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", 621 | "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", 622 | "requires": { 623 | "safe-buffer": "^5.0.1" 624 | } 625 | }, 626 | "tweetnacl": { 627 | "version": "0.14.5", 628 | "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", 629 | "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", 630 | "optional": true 631 | }, 632 | "type-is": { 633 | "version": "1.6.16", 634 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz", 635 | "integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==", 636 | "requires": { 637 | "media-typer": "0.3.0", 638 | "mime-types": "~2.1.18" 639 | } 640 | }, 641 | "unpipe": { 642 | "version": "1.0.0", 643 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 644 | "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" 645 | }, 646 | "utils-merge": { 647 | "version": "1.0.1", 648 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", 649 | "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" 650 | }, 651 | "uuid": { 652 | "version": "3.3.2", 653 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", 654 | "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" 655 | }, 656 | "vary": { 657 | "version": "1.1.2", 658 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 659 | "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" 660 | }, 661 | "verror": { 662 | "version": "1.10.0", 663 | "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", 664 | "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", 665 | "requires": { 666 | "assert-plus": "^1.0.0", 667 | "core-util-is": "1.0.2", 668 | "extsprintf": "^1.2.0" 669 | } 670 | } 671 | } 672 | } 673 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rest-api-squeezebox", 3 | "version": "1.1.0", 4 | "description": "This REST API is a translation of the slimserver / squeezebox server JSON RPC API.", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/legrosmanu/rest-api-squeezebox" 8 | }, 9 | "scripts": { 10 | "start": "node src/index.js 192.168.1.11:2311 2312" 11 | }, 12 | "author": "Emmanuel LEGROS", 13 | "license": "Apache-2.0", 14 | "dependencies": { 15 | "express": "^4.16.3", 16 | "request": "^2.87.0" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/legrosmanu/rest-api-squeezebox/5885192a174dd8392e9a687deff1ee387e6e6059/src/.DS_Store -------------------------------------------------------------------------------- /src/api/PlayerServices.js: -------------------------------------------------------------------------------- 1 | var Security = require('./token'); 2 | let SlimHelper = require('../slim-server-wrapper/SlimHelper'); 3 | var Player = require('../integration/player/Player'); 4 | 5 | exports.setEndPoints = function (app) { 6 | 7 | // We have to check how much we have players to get all the players, 8 | // then get the players. 9 | app.get('/players', requireAuthentication, async (req, res) => { 10 | try { 11 | const resultNbPlayers = await SlimHelper.sendRequest(['-', ['player', 'count', '?']]); 12 | const resultPlayers = await SlimHelper.sendRequest(['-', ['players', '0', resultNbPlayers._count]]); 13 | let players = resultPlayers.players_loop.map(playerFromSlim => new Player(playerFromSlim.uuid)); 14 | const initPlayerPromises = players.map(player => player.init()); 15 | await Promise.all(initPlayerPromises); 16 | let playersToSend = []; 17 | for (let player of players) { 18 | playersToSend.push(player.toAPI()); 19 | } 20 | res.send(playersToSend); 21 | } catch (error) { 22 | let errorToSend = errorManager(error, 'GET', '/players'); 23 | res.status(errorToSend.codeHTTP).send(errorToSend); 24 | } 25 | }); 26 | 27 | app.get('/players/:uuid', requireAuthentication, async (req, res) => { 28 | try { 29 | let player = new Player(req.params.uuid); 30 | await player.init(); 31 | res.send(player.toAPI()); 32 | } catch (error) { 33 | let errorToSend = errorManager(error, 'GET', '/players/:uuid'); 34 | res.status(errorToSend.codeHTTP).send(errorToSend); 35 | } 36 | }); 37 | 38 | // According to the body, we had on an array the promises to execute, and then execute them. 39 | app.patch('/players/:uuid', requireAuthentication, async (req, res) => { 40 | try { 41 | let player = new Player(req.params.uuid); 42 | await player.init(); 43 | let changesToDo = []; 44 | if (req.body.play_state !== undefined) changesToDo.push(player.setPlayState(req.body.play_state)); 45 | if (req.body.song_currently_played !== undefined) { 46 | var songPlayed = player.getSongPlayed(); 47 | if (req.body.song_currently_played.index_in_playlist == '+1') { 48 | changesToDo.push(songPlayed.nextTrack()); 49 | } else if (req.body.song_currently_played.index_in_playlist == '-1') { 50 | changesToDo.push(songPlayed.previousTrack()); 51 | } else { 52 | changesToDo.push(player.songPlayed.setIndexSongPlayedOnPlaylist(req.body.song_currently_played.index_in_playlist)); 53 | } 54 | } 55 | await Promise.all(changesToDo); 56 | res.sendStatus(204); 57 | } catch (error) { 58 | let errorToSend = errorManager(error, 'PATCH', '/players/:uuid'); 59 | res.status(errorToSend.codeHTTP).send(errorToSend); 60 | } 61 | }); 62 | 63 | // To change the playlist to play on the player, this endpoint wait the path of the new playlist 64 | app.patch('/players/:uuid/playlist', requireAuthentication, async (req, res) => { 65 | try { 66 | let player = new Player(req.params.uuid); 67 | await player.init(); 68 | if (req.body.path !== undefined) { 69 | await player.getPlaylist().changePath(req.body.path); 70 | } else if (req.body.album_title !== undefined) { 71 | await player.getPlaylist().searchAlbum(req.body.album_title); 72 | } else if (req.body.artist_name !== undefined) { 73 | await player.getPlaylist().searchArtist(req.body.artist_name); 74 | } else { 75 | let error = { 76 | codeHTTP: 400, 77 | message: "The object is not well formed." 78 | }; 79 | throw error; 80 | } 81 | res.sendStatus(204); 82 | } catch (error) { 83 | let errorToSend = errorManager(error, 'PATCH', '/players/:uuid/playlist'); 84 | res.status(errorToSend.codeHTTP).send(errorToSend); 85 | } 86 | }); 87 | 88 | app.delete('/players/:uuid/playlist', requireAuthentication, async (req, res) => { 89 | try { 90 | let player = new Player(req.params.uuid); 91 | await player.init(); 92 | await player.getPlaylist().clear(); 93 | res.sendStatus(204); 94 | } catch (error) { 95 | let errorToSend = errorManager(error, 'DELETE', '/players/:uuid/playlist'); 96 | res.status(errorToSend.codeHTTP).send(errorToSend); 97 | } 98 | }); 99 | 100 | // According to the body, we had on an array the promises to execute, and then execute them. 101 | app.patch('/players/:uuid/mixer', requireAuthentication, async (req, res) => { 102 | try { 103 | let player = new Player(req.params.uuid); 104 | await player.init(); 105 | let mixer = await player.getMixer(); 106 | let changesToDo = []; 107 | if (req.body.power !== undefined) changesToDo.push(mixer.setPower(req.body.power)); 108 | if (req.body.volume !== undefined) changesToDo.push(mixer.setVolume(req.body.volume)); 109 | if (req.body.bass !== undefined) changesToDo.push(mixer.setBass(req.body.bass)); 110 | if (req.body.treble !== undefined) changesToDo.push(mixer.setTreble(req.body.treble)); 111 | await Promise.all(changesToDo); 112 | res.sendStatus(204); 113 | } catch (error) { 114 | let errorToSend = errorManager(error, 'PATCH', '/players/:uuid/mixer'); 115 | res.status(errorToSend.codeHTTP).send(errorToSend); 116 | } 117 | }); 118 | 119 | }; 120 | 121 | var errorManager = (error, HTTPMethod, URI) => { 122 | let errorToSend; 123 | if (error && error.codeHTTP !== undefined) { 124 | errorToSend = error; 125 | } else { 126 | errorToSend = { 127 | codeHTTP: 500, 128 | message: 'Ooooppppssss. There is a problem with the ' + HTTPMethod + ' on ' + URI 129 | }; 130 | } 131 | console.log(errorToSend.message); 132 | console.log(error); 133 | return errorToSend; 134 | }; 135 | 136 | // Simple security - just check a token. 137 | var requireAuthentication = (req, res, next) => { 138 | if (req.query.token == Security.token) { 139 | next(); 140 | } else { 141 | res.sendStatus(403); 142 | } 143 | }; -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var http = require('http'); 3 | var bodyParser = require('body-parser'); 4 | 5 | var PlayerAPI = require('./api/PlayerServices'); 6 | 7 | var app = express(); 8 | 9 | app.use(bodyParser.json()); // for parsing application/json 10 | 11 | if (process.argv.length < 4) { 12 | console.log("ERROR : Check the parameters. You have to use 'node {squeezebox_server_url} {port_for_your_api}'"); 13 | return; 14 | } 15 | 16 | let SlimHelper = require('./slim-server-wrapper/SlimHelper'); 17 | SlimHelper.setUrl(process.argv[2]); 18 | 19 | PlayerAPI.setEndPoints(app); 20 | 21 | var port = process.env.PORT || process.argv[3]; 22 | http.createServer(app).listen(port); -------------------------------------------------------------------------------- /src/integration/player/Mixer.js: -------------------------------------------------------------------------------- 1 | let SlimHelper = require('../../slim-server-wrapper/SlimHelper'); 2 | 3 | module.exports = class Mixer { 4 | 5 | constructor(player) { 6 | this.player = player; 7 | } 8 | 9 | async init() { 10 | const data = await Promise.all([ 11 | SlimHelper.sendRequest([this.player.id, ['power', '?']]), 12 | SlimHelper.sendRequest([this.player.id, ['mixer', 'volume', '?']]), 13 | SlimHelper.sendRequest([this.player.id, ['mixer', 'bass', '?']]), 14 | SlimHelper.sendRequest([this.player.id, ['mixer', 'treble', '?']]) 15 | ]); 16 | this.power = (data[0]._power) ? "on" : "off"; 17 | this.volume = data[1]._volume; 18 | this.bass = data[2]._bass; 19 | this.treble = data[3]._treble; 20 | } 21 | 22 | async setPower(newPower) { 23 | this.power = (newPower === "on") ? '1' : '0'; 24 | await SlimHelper.sendRequest([this.player.id, ['power', this.power]]); 25 | } 26 | 27 | async setVolume(newVolume) { 28 | this.volume = newVolume; 29 | await SlimHelper.sendRequest([this.player.id, ['mixer', 'volume', this.volume]]); 30 | } 31 | 32 | async setBass(newBass) { 33 | this.bass = newBass; 34 | await SlimHelper.sendRequest([this.player.id, ['mixer', 'bass', this.bass]]); 35 | } 36 | 37 | async setTreble(newTreble) { 38 | this.treble = newTreble; 39 | await SlimHelper.sendRequest([this.player.id, ['mixer', 'treble', this.treble]]); 40 | } 41 | 42 | toAPI() { 43 | return { 44 | power: this.power, 45 | volume: this.volume, 46 | bass: this.bass, 47 | treble: this.treble 48 | }; 49 | } 50 | 51 | } -------------------------------------------------------------------------------- /src/integration/player/Player.js: -------------------------------------------------------------------------------- 1 | let SlimHelper = require('../../slim-server-wrapper/SlimHelper'); 2 | let Mixer = require('./Mixer'); 3 | let SongPlayed = require('./SongPlayed'); 4 | let Playlist = require('./Playlist'); 5 | 6 | module.exports = class Player { 7 | 8 | constructor(uuid) { 9 | this.uuid = uuid; 10 | } 11 | 12 | // Call slim server to get the data for this player. 13 | // uuid is not an id for the rpc api to get the data. So, we have to check how much we have players to get all the players, 14 | // then find the player with this uuid. 15 | // After that, we can get the data. 16 | async init() { 17 | 18 | 19 | const resultNbPlayers = await SlimHelper.sendRequest(['-', ['player', 'count', '?']]); 20 | 21 | const resultPlayers = await SlimHelper.sendRequest(['-', ['players', '0', resultNbPlayers._count]]); 22 | 23 | let player = null; 24 | for (let it = 0; it < resultPlayers.players_loop.length && player === null; it++) { 25 | if (resultPlayers.players_loop[it].uuid === this.uuid) { 26 | player = resultPlayers.players_loop[it]; 27 | } 28 | } 29 | 30 | if (player === null) { 31 | let error = { 32 | codeHTTP: 404, 33 | message: "player not found on Player.init() " + this.uuid 34 | }; 35 | throw error; 36 | } else { 37 | this.name = player.name; 38 | this.id = player.playerid; 39 | this.ip = player.ip; 40 | this.model = player.modelname; 41 | this.firmwareVersion = player.firmware; 42 | 43 | this.playlist = new Playlist(this); 44 | 45 | this.mixer = new Mixer(this); 46 | this.songPlayed = new SongPlayed(this); 47 | const data = await Promise.all([ 48 | SlimHelper.sendRequest([this.id, ['signalstrength', '?']]), 49 | SlimHelper.sendRequest([this.id, ['mode', '?']]), 50 | this.mixer.init(), 51 | this.songPlayed.init() 52 | ]); 53 | this.signalStrength = data[0]._signalstrength; 54 | this.playState = data[1]._mode; 55 | } 56 | 57 | } 58 | 59 | 60 | getMixer() { 61 | return this.mixer; 62 | } 63 | 64 | getSongPlayed() { 65 | return this.songPlayed; 66 | } 67 | 68 | getPlaylist() { 69 | return this.playlist; 70 | } 71 | 72 | // newState can be on or off 73 | async setPower(newState) { 74 | if (newState === "on" || newState === "off") { 75 | await this.mixer.setPower(newState); 76 | } else { 77 | let error = { 78 | codeHTTP: 400, 79 | message: "the power for the player " + this.uuid + " has to be on or off" 80 | }; 81 | throw error; 82 | } 83 | } 84 | 85 | // newState can be play, stop or pause 86 | async setPlayState(newState) { 87 | if (newState === "play" || newState === "stop" || newState === "pause") { 88 | this.playState = newState; 89 | await SlimHelper.sendRequest([this.id, ['mode', this.playState]]); 90 | } else { 91 | let error = { 92 | codeHTTP: 400, 93 | message: "the play state for the player " + this.uuid + " has to be play, stop or pause" 94 | }; 95 | throw error; 96 | } 97 | } 98 | 99 | toAPI() { 100 | return { 101 | uuid: this.uuid, 102 | name: this.name, 103 | id: this.id, 104 | ip: this.ip, 105 | model: this.model, 106 | firmware_version: this.firmwareVersion, 107 | signal_strength: this.signalStrength, 108 | play_state: this.playState, 109 | mixer: this.mixer.toAPI(), 110 | song_currently_played: this.songPlayed.toAPI() 111 | }; 112 | } 113 | 114 | } -------------------------------------------------------------------------------- /src/integration/player/Playlist.js: -------------------------------------------------------------------------------- 1 | let SlimHelper = require('../../slim-server-wrapper/SlimHelper'); 2 | 3 | module.exports = class Playlist { 4 | 5 | constructor(player) { 6 | this.player = player; 7 | } 8 | 9 | async changePath(newPath) { 10 | if (newPath !== undefined) { 11 | await SlimHelper.sendRequest([this.player.id, ['playlist', 'play', newPath]]); 12 | } else { 13 | let error = { 14 | codeHTTP: 400, 15 | message: "To change the playlist, the object on the request must to have a path attribute." 16 | }; 17 | throw error; 18 | } 19 | } 20 | 21 | async searchAlbum(albumTitle) { 22 | if (albumTitle !== undefined) { 23 | await SlimHelper.sendRequest([this.player.id, ['playlist', 'loadtracks', "album.titlesearch=" + albumTitle]]); 24 | } else { 25 | let error = { 26 | codeHTTP: 400, 27 | message: "To load an album, the object on the request must to have a album_title attribute." 28 | }; 29 | throw error; 30 | } 31 | } 32 | 33 | async searchArtist(artistName) { 34 | if (artistName !== undefined) { 35 | await SlimHelper.sendRequest([this.player.id, ['playlist', 'loadtracks', "contributor.namesearch=" + artistName]]); 36 | } else { 37 | let error = { 38 | codeHTTP: 400, 39 | message: "To load the tracks of an artist, the object on the request must to have a artist_name attribute." 40 | }; 41 | throw error; 42 | } 43 | } 44 | 45 | async clear() { 46 | await SlimHelper.sendRequest([this.player.id, ['playlist', 'clear']]); 47 | } 48 | 49 | } -------------------------------------------------------------------------------- /src/integration/player/SongPlayed.js: -------------------------------------------------------------------------------- 1 | let SlimHelper = require('../../slim-server-wrapper/SlimHelper'); 2 | 3 | module.exports = class SongPlayed { 4 | 5 | constructor(player) { 6 | this.player = player; 7 | } 8 | 9 | async init() { 10 | const data = await Promise.all([ 11 | SlimHelper.sendRequest([this.player.id, ['playlist', 'index', '?']]), 12 | SlimHelper.sendRequest([this.player.id, ['time', '?']]), 13 | SlimHelper.sendRequest([this.player.id, ['duration', '?']]), 14 | SlimHelper.sendRequest([this.player.id, ['artist', '?']]), 15 | SlimHelper.sendRequest([this.player.id, ['album', '?']]), 16 | SlimHelper.sendRequest([this.player.id, ['title', '?']]), 17 | SlimHelper.sendRequest([this.player.id, ['remote', '?']]), 18 | SlimHelper.sendRequest([this.player.id, ['path', '?']]) 19 | ]); 20 | this.indexInPlaylist = data[0]._index; 21 | this.secondsPlayed = data[1]._time; 22 | this.duration = data[2]._duration; 23 | this.artist = data[3]._artist; 24 | this.album = data[4]._album; 25 | this.title = data[5]._title; 26 | this.isRemote = (data[6]._remote) ? true : false; 27 | this.path = data[7]._path; 28 | } 29 | 30 | async setIndexSongPlayedOnPlaylist(newIndex) { 31 | await SlimHelper.sendRequest([this.player.id, ['playlist', 'index', newIndex]]); 32 | } 33 | 34 | 35 | async nextTrack() { 36 | await SlimHelper.sendRequest([this.player.id, ['playlist', 'index', '+1']]); 37 | } 38 | 39 | async previousTrack() { 40 | if (this.indexInPlaylist > 0) { 41 | await SlimHelper.sendRequest([this.player.id, ['playlist', 'index', '-1']]); 42 | } else { 43 | await this.setIndexSongPlayedOnPlaylist(0); 44 | } 45 | } 46 | 47 | toAPI() { 48 | return { 49 | index_in_playlist: this.indexInPlaylist, 50 | seconds_played: this.secondsPlayed, 51 | duration: this.duration, 52 | artist: this.artist, 53 | album: this.album, 54 | title: this.title, 55 | is_remote: this.isRemote, 56 | path: this.path 57 | }; 58 | } 59 | 60 | } -------------------------------------------------------------------------------- /src/slim-server-wrapper/SlimHelper.js: -------------------------------------------------------------------------------- 1 | let request = require('request'); 2 | 3 | let slimServerURL = null; 4 | 5 | module.exports = class SlimHelper { 6 | 7 | static sendRequest(requestParams) { 8 | return new Promise((resolve, reject) => { 9 | request({ 10 | url: slimServerURL, 11 | method: "POST", 12 | json: true, 13 | body: { 14 | id: 1, 15 | method: 'slim.request', 16 | params: requestParams 17 | } 18 | }, (error, response, body) => { 19 | if (error) { 20 | console.log("Error SlimHelper.sendRequest : " + error); 21 | reject(error); 22 | } else { 23 | resolve(body.result); 24 | } 25 | }); 26 | }); 27 | } 28 | 29 | static setUrl(url) { 30 | slimServerURL = 'http://' + url + '/jsonrpc.js'; 31 | } 32 | 33 | } --------------------------------------------------------------------------------