diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 00000000..5aab52d6 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,11 @@ +{ + "extends": "airbnb-base", + "env": { + "node": true, + "mocha": true + }, + "rules": { + "comma-dangle": 0, + "strict": 0 + } +} diff --git a/.gitignore b/.gitignore index 03515cba..be7ee133 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,12 @@ obj/ *.crt *.pfx *.key + +# VS +.vscode +.vscode/launch.json + +# Ignore Mac DS_Store files +.DS_Store +yarn.lock +npm-debug.log diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md new file mode 100644 index 00000000..562fb4f4 --- /dev/null +++ b/ISSUE_TEMPLATE.md @@ -0,0 +1,11 @@ +## Before you submit an issue + +Search among the current open _and_ closed issues for an answer to your question before submitting an issue! + +If you are looking for assistance with installing or just general questions, reach out on the gitter chat (https://gitter.im/node-sonos-http-api/Lobby) instead of filing an issue. It's easier for me if issues are primarily fokused on bugs and feature requests, and hopefully other people can assist you in the chat. + +If your question is Docker related, please file the issue at the https://github.com/chrisns/docker-node-sonos-http-api and also, make sure you have tested that image before asking a question. This is the only image that has any correlation to this project, and is guaranteed to be up to date. + +If your question has anything to do with Amazon Echo, it is probably better suited at https://github.com/rgraciano/echo-sonos. If you have questions about the requests sent to this library, figure out the exact request that is sent from the Alexa Skill (through the logs) and include that in the issue. + +Thank you! diff --git a/README.md b/README.md index 5fe6168a..30cb8063 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,13 @@ -[![PayPal donate button](https://img.shields.io/badge/paypal-donate-yellow.svg)](https://www.paypal.me/jishi "Donate once-off to this project using Paypal") +[![PayPal donate button](https://img.shields.io/badge/paypal-donate-yellow.svg)](https://www.paypal.me/jishi "Donate once-off to this project using Paypal") [![Join the chat at gitter](https://img.shields.io/gitter/room/badges/shields.svg)](https://gitter.im/node-sonos-http-api/Lobby "Need assistance? Join the chat at Gitter.im") -Feel free to use it as you please. Consider donating if you want to support further development. +⚠WARNING!⚠ + +The Sonos S2 update, released June 2020, still works with this API. However, it might break in the future if and when Sonos decide to drop UPnP as the control protocol. + + +Feel free to use it as you please. Consider donating if you want to support further development. Reach out on the gitter chat if you have issues getting it to run, instead of creating new issues, thank you! + +If you are also looking for cloud control (ifttt, public webhooks etc), see the [bronos-client](http://www.bronos.net) project! That pi image also contains an installation of this http-api. SONOS HTTP API ============== @@ -72,6 +79,7 @@ The actions supported as of today: * groupVolume (parameter is absolute or relative volume. Prefix +/- indicates relative volume) * mute / unmute * groupMute / groupUnmute +* togglemute (toggles mute state) * trackseek (parameter is queue index) * timeseek (parameter is in seconds, 60 for 1:00, 120 for 2:00 etc) * next @@ -81,19 +89,27 @@ The actions supported as of today: * favorites (with optional "detailed" parameter) * playlist * lockvolumes / unlockvolumes (experimental, will enforce the volume that was selected when locking!) -* repeat (on/off) -* shuffle (on/off) -* crossfade (on/off) +* repeat (on(=all)/one/off(=none)/toggle) +* shuffle (on/off/toggle) +* crossfade (on/off/toggle) * pauseall (with optional timeout in minutes) * resumeall (will resume the ones that was pause on the pauseall call. Useful for doorbell, phone calls, etc. Optional timeout) * say * sayall +* saypreset * queue * clearqueue * sleep (values in seconds) * linein (only analog linein, not PLAYBAR yet) * clip (announce custom mp3 clip) * clipall +* clippreset +* join / leave (Grouping actions) +* sub (on/off/gain/crossover/polarity) See SUB section for more info +* nightmode (on/off/toggle, PLAYBAR only) +* speechenhancement (on/off/toggle, PLAYBAR only) +* bass/treble (use -10 through to 10 as the value. 0 is neutral) + State ----- @@ -128,28 +144,37 @@ Example of a state json: "shuffle":true, "repeat":false, "crossfade":false - } + }, + "equalizer": { + "bass": 0, + "treble": 0, + "loudness": true + } } Queue ----- Obtain the current queue list from a specified player. The request will accept: - - No parameters + - limit (optional) + - offset (optional, requires limit) + - detailed flag (optional, include uri in response) - `http://localhost:5005/living room/queue` + http://localhost:5005/living room/queue + http://localhost:5005/living room/queue/10 (only return top 10) + http://localhost:5005/living room/queue/10/10 (return result 11-20) + http://localhost:5005/living room/queue/detailed + http://localhost:5005/living room/queue/10/detailed Example queue response: ``` [ { - "uri": "x-sonos-spotify:spotify%3atrack%3a0AvV49z4EPz5ocYN7eKGAK?sid=9&flags=8224&sn=3", "albumArtURI": "/getaa?s=1&u=x-sonos-spotify%3aspotify%253atrack%253a0AvV49z4EPz5ocYN7eKGAK%3fsid%3d9%26flags%3d8224%26sn%3d3", "title": "No Diggity", "artist": "Blackstreet", "album": "Another Level" }, { - "uri": "x-sonos-spotify:spotify%3atrack%3a5OQGeJ1ceykovrykZsGhqL?sid=9&flags=8224&sn=3", "albumArtURI": "/getaa?s=1&u=x-sonos-spotify%3aspotify%253atrack%253a5OQGeJ1ceykovrykZsGhqL%3fsid%3d9%26flags%3d8224%26sn%3d3", "title": "Breathless", "artist": "The Corrs", @@ -245,7 +270,7 @@ In the example, there is one preset called `all`, which you can apply by invokin presets folder -------------- -You can create a preset files in the presets folder with pre made presets, called presets.json. It will be loaded upon start, any changes made to files in this folder (addition, removal, modification) will trigger a reload of your presets. +You can create a preset files in the presets folder with pre made presets. It will be loaded upon start, any changes made to files in this folder (addition, removal, modification) will trigger a reload of your presets. The name of the file (xxxxxx.json) will become the name of the preset. It will be parsed as JSON5, to be more forgiving of typos. See http://json5.org/ for more info. Example content: @@ -273,6 +298,8 @@ Example content: "volume": 15 } ], + "trackNo": 3, + "elapsedTime": 42, "playMode": { "shuffle": true, "repeat": "all", @@ -288,22 +315,29 @@ There is an example.json bundled with this repo. The name of the file will becom settings.json ------------- -If you want to change default settings, you can create a settings.json file and put in the root folder. +If you want to change default settings, you can create a settings.json file and put in the root folder. This will be parsed as JSON5, to be more forgiving. See http://json5.org/ for more info. Available options are: * port: change the listening port +* ip: change the listening IP * https: use https which requires a key and certificate or pfx file * auth: require basic auth credentials which requires a username and password * announceVolume: the percentual volume use when invoking say/sayall without any volume parameter * presetDir: absolute path to look for presets (folder must exist!) +* household: when theres multiple sonos accounts on one network (example: Sonos_ab7d67898dcc5a6d, find it in [Your sonos IP]:1400/status/zp). Note that the value after the '.' should not be removed. See more info here: https://github.com/jishi/node-sonos-http-api/issues/783 Example: ```json { "voicerss": "Your api key for TTS with voicerss", + "microsoft": { + "key": "Your api for Bing speech API", + "name": "ZiraRUS" + }, "port": 5005, + "ip": "0.0.0.0", "securePort": 5006, "https": { "key": "/path/to/key.pem", @@ -322,14 +356,22 @@ Example: "username": "your-pandora-account-email-address", "password": "your-pandora-password" }, - "library": { - "randomQueueLimit": 50 - } + "spotify": { + "clientId": "your-spotify-application-clientId", + "clientSecret": "your-spotify-application-clientSecret" + }, + "library": { + "randomQueueLimit": 50 + } } ``` Override as it suits you. +Note for Spotify users! +----------------------- + +To use Spotify, go to https://developer.spotify.com/my-applications/#!/applications/create and create a Spotify application to get your client keys. You can name it Sonos or anything else and you don't have to change any values. Use the Client ID and the Client Secret values in the settings.json file as indicated above. Favorites @@ -354,7 +396,20 @@ and it will replace the queue with the playlist and starts playing. Say (TTS support) ----------------- -Experimental support for TTS. This REQUIRES a registered API key from voiceRSS! See http://www.voicerss.org/ for info. +Experimental support for TTS. Today the following providers are available: + +* voicerss +* Microsoft Cognitive Services (Bing Text to Speech API) +* AWS Polly +* Google (default) +* macOS say command +* Elevenlabs + +It will use the one you configure in settings.json. If you define settings for multiple TTS services, it will not be guaranteed which one it will choose! + +#### VoiceRSS + +This REQUIRES a registered API key from voiceRSS! See http://www.voicerss.org/ for info. You need to add this to a file called settings.json (create if it doesn't exist), like this: @@ -414,6 +469,302 @@ The supported language codes are: | es-es | Spanish (Spain) | | sv-se | Swedish (Sweden) | +#### Microsoft +This one also requires a registered api key. You can sign up for free here: https://www.microsoft.com/cognitive-services/en-US/subscriptions?mode=NewTrials and select "Bing Speech - Preview". + +The following configuration is available (the entered values except key are default, and may be omitted): + +```json + { + "microsoft": { + "key": "Your api for Bing speech API", + "name": "ZiraRUS" + } + } +``` + +You change language by specifying a voice name correlating to the desired language. +Name should be specified according to this list: https://www.microsoft.com/cognitive-services/en-us/speech-api/documentation/API-Reference-REST/BingVoiceOutput#SupLocales +where name is the right most part of the voice font name (without optional Apollo suffix). Example: + +`Microsoft Server Speech Text to Speech Voice (ar-EG, Hoda)` name should be specified as `Hoda` + +`Microsoft Server Speech Text to Speech Voice (de-DE, Stefan, Apollo)` name should be specified as `Stefan` + +`Microsoft Server Speech Text to Speech Voice (en-US, BenjaminRUS)` name should be specified as `BenjaminRUS` + +Action is: + + /[Room name]/say/[phrase][/[name]][/[announce volume]] + /sayall/[phrase][/[name]][/[announce volume]] + +Example: + + /Office/say/Hello, dinner is ready + /Office/say/Hello, dinner is ready/BenjaminRUS + /Office/say/Guten morgen/Stefan + /sayall/Hello, dinner is ready + /Office/say/Hello, dinner is ready/90 + /Office/say/Guten morgen/Stefan/90 + +Supported voices are: + + Hoda, Naayf, Ivan, HerenaRUS, Jakub, Vit, HelleRUS, Michael, Karsten, Hedda, Stefan, Catherine, Linda, Susan, George, Ravi, ZiraRUS, BenjaminRUS, Laura, Pablo, Raul, Caroline, Julie, Paul, Cosimo, Ayumi, Ichiro, Daniel, Irina, Pavel, HuihuiRUS, Yaoyao, Kangkang, Tracy, Danny, Yating, Zhiwei + +See https://www.microsoft.com/cognitive-services/en-us/speech-api/documentation/API-Reference-REST/BingVoiceOutput#SupLocales to identify +which language and gender it maps against. If your desired voice is not in the list of supported one, raise an issue about adding it or send me a PR. + +#### AWS Polly + +Requires AWS access tokens, which you generate for your user. Since this uses the AWS SDK, it will look for settings in either Environment variables, the ~/.aws/credentials or ~/.aws/config. + +You can also specify it for this application only, using: +```json + { + "aws": { + "credentials": { + "region": "eu-west-1", + "accessKeyId": "Your access key id", + "secretAccessKey": "Your secret" + }, + "name": "Joanna" + } + } +``` + +To select the neural engine, append `Neural` to the name, e.g. `DanielNeural`. + +Choose the region where you registered your account, or the one closest to you. + +If you have your credentials elsewhere and want to stick with the default voice, you still need to make sure that the aws config option is set to trigger AWS TTS: + +```json + { + "aws": {} + } +``` + +Action is: + + /[Room name]/say/[phrase][/[name]][/[announce volume]] + /sayall/[phrase][/[name]][/[announce volume]] + +Example: + + /Office/say/Hello, dinner is ready + /Office/say/Hello, dinner is ready/Nicole + /Office/say/Hej, maten är klar/Astrid + /sayall/Hello, dinner is ready + /Office/say/Hello, dinner is ready/90 + /Office/say/Hej, maten är klar/Astrid/90 + +This is the current list of voice names and their corresponding language and accent (as of Dec 2016). +To get a current list of voices, you would need to use the AWS CLI and invoke the describe-voices command. + +| Language | Code | Gender | Name | +| --------- | ---- | ------ | ---- | +| Australian English | en-AU | Female | Nicole | +| Australian English | en-AU | Male | Russell | +| Brazilian Portuguese | pt-BR | Female | Vitoria | +| Brazilian Portuguese | pt-BR | Male | Ricardo | +| British English | en-GB | Male | Brian | +| British English | en-GB | Female | Emma | +| British English | en-GB | Female | Amy | +| Canadian French | fr-CA | Female | Chantal | +| Castilian Spanish | es-ES | Female | Conchita | +| Castilian Spanish | es-ES | Male | Enrique | +| Danish | da-DK | Female | Naja | +| Danish | da-DK | Male | Mads | +| Dutch | nl-NL | Male | Ruben | +| Dutch | nl-NL | Female | Lotte | +| French | fr-FR | Male | Mathieu | +| French | fr-FR | Female | Celine | +| German | de-DE | Female | Marlene | +| German | de-DE | Male | Hans | +| Icelandic | is-IS | Male | Karl | +| Icelandic | is-IS | Female | Dora | +| Indian English | en-IN | Female | Raveena | +| Italian | it-IT | Female | Carla | +| Italian | it-IT | Male | Giorgio | +| Japanese | ja-JP | Female | Mizuki | +| Norwegian | nb-NO | Female | Liv | +| Polish | pl-PL | Female | Maja | +| Polish | pl-PL | Male | Jacek | +| Polish | pl-PL | Male | Jan | +| Polish | pl-PL | Female | Ewa | +| Portuguese | pt-PT | Female | Ines | +| Portuguese | pt-PT | Male | Cristiano | +| Romanian | ro-RO | Female | Carmen | +| Russian | ru-RU | Female | Tatyana | +| Russian | ru-RU | Male | Maxim | +| Swedish | sv-SE | Female | Astrid | +| Turkish | tr-TR | Female | Filiz | +| US English | en-US | Male | Justin | +| US English | en-US | Female | Joanna | +| US English | en-US | Male | Joey | +| US English | en-US | Female | Ivy | +| US English | en-US | Female | Salli | +| US English | en-US | Female | Kendra | +| US English | en-US | Female | Kimberly | +| US Spanish | es-US | Female | Penelope | +| US Spanish | es-US | Male | Miguel | +| Welsh | cy-GB | Female | Gwyneth | +| Welsh English | en-GB-WLS | Male | Geraint | + +#### Elevenlabs + +Elevenlabs is a TTS service enabling generatiung TTS audio files using AI generated voices. + +Requires API Key and optionally default voiceId. + +Since Elevenlabs AI models are multilingual by default, there is no need (nor place) for `language` parameter in +Elevenlabs API. Because of this, `language` parameter in URL is used to inject custom `voiceId` on per-request basis. You will +need to either configure default voiceId in `settings.json` or provide `voiceId` with every HTTP request. + +##### Config + +Minimal: +```json + { + "elevenlabs": { + "auth": { + "apiKey": "" + } + } + } +``` + +Full: +```json + { + "elevenlabs": { + "auth": { + "apiKey": "" + }, + "config": { + "voiceId": "", + "stability": 0.5, + "similarityBoost": 0.5, + "speakerBoost": true, + "style": 1, + "modelId": "eleven_multilingual_v2" + } + } + } +``` + +#### Google (default if no other has been configured) + +Does not require any API keys. Please note that Google has been known in the past to change the requirements for its Text-to-Speech API, and this may stop working in the future. There is also limiations to how many requests one is allowed to do in a specific time period. + +The following language codes are supported + +| Language code | Language | +| ------------- | -------- | +| af | Afrikaans | +| sq | Albanian | +| ar | Arabic | +| hy | Armenian | +| bn | Bengali | +| ca | Catalan | +| zh | Chinese | +| zh-cn | Chinese (Mandarin/China) | +| zh-tw | Chinese (Mandarin/Taiwan) | +| zh-yue | Chinese (Cantonese) | +| hr | Croatian | +| cs | Czech | +| da | Danish | +| nl | Dutch | +| en | English | +| en-au | English (Australia) | +| en-gb | English (Great Britain) | +| en-us | English (United States) | +| eo | Esperanto | +| fi | Finnish | +| fr | Franch | +| de | German | +| el | Greek | +| hi | Hindi | +| hu | Hungarian | +| is | Icelandic | +| id | Indonesian | +| it | Italian | +| ja | Japanese | +| ko | Korean | +| la | Latin | +| lv | Latvian | +| mk | Macedonian | +| no | Norwegian | +| pl | Polish | +| pt | Portuguese | +| pt-br | Portuguese (Brazil) | +| ro | Romanian | +| ru | Russian | +| sr | Serbian | +| sk | Slovak | +| es | Spanish | +| es-es | Spanish (Spain) | +| es-us | Spanish (United States) | +| sw | Swahili | +| sv | Swedish | +| ta | Tamil | +| th | Thai | +| tr | Turkish | +| vi | Vietnamese | +| cy | Welsh | + +Action is: + + /[Room name]/say/[phrase][/[language_code]][/[announce volume]] + /sayall/[phrase][/[language_code]][/[announce volume]] + +#### macOS say command +On macOS the "say" command can be used for text to speech. If your installation runs on macOS you can activate the system TTS by giving an empty configuration: + +```json +{ + "macSay": {} +} +``` + +Or you can provide a default voice and a speech rate: + +```json +{ + "macSay": { + "voice" : "Alex", + "rate": 90 + } +} +``` + +Action is: + + /[Room name]/say/[phrase][/[voice]][/[announce volume]] + /sayall/[phrase][/[voice]][/[announce volume]] + +Example: + + /Office/say/Hello, dinner is ready + /Office/say/Hello, dinner is ready/Agnes + /Office/say/Guten morgen/Anna + /sayall/Hello, dinner is ready + /Office/say/Hello, dinner is ready/90 + /Office/say/Guten morgen/Anna/90 + +Supported voices are: + +Alex, Alice, Alva, Amelie, Anna, Carmit, Damayanti, Daniel, Diego, Ellen, Fiona, Fred, Ioana, Joana, Jorge, Juan, Kanya, Karen, Kyoko, Laura, Lekha, Luca, Luciana, Maged, Mariska, Mei-Jia, Melina, Milena, Moira, Monica, Nora, Paulina, Samantha, Sara, Satu, Sin-ji, Tessa, Thomas, Ting-Ting, Veena, Victoria, Xander, Yelda, Yuna, Yuri, Zosia, Zuzana + +A list of available voices can be printed by this command: +``` + say -v '?' +``` + +See also https://gist.github.com/mculp/4b95752e25c456d425c6 and https://stackoverflow.com/questions/1489800/getting-list-of-mac-text-to-speech-voices-programmatically + +To download more voices go to: System Preferences -> Accessibility -> Speech -> System Voice + Line-in ------- @@ -423,7 +774,7 @@ Optional parameter is line-in from another player. Examples: `/Office/linein` Selects line-in on zone Office belongs to, with source Office. -`Office/linein/TV Room` +`/Office/linein/TV%20Room` Selects line-in for zone Office belongs to, with source TV Room. If you want to to isolate a player and then select line-in, use the `/Office/leave` first. @@ -438,17 +789,51 @@ Like "Say" but instead of a phrase, reference a custom track from the `static/cl Examples: - clipall/sample_clip.mp3 - clipall/sample_clip.mp3/80 + /clipall/sample_clip.mp3 + /clipall/sample_clip.mp3/80 /Office/clip/sample_clip.mp3 /Office/clip/sample_clip.mp3/30 *Pro-tip: announce your arrival with an epic theme song!* -Spotify and Apple Music (Experimental) +Grouping +-------- + +You have basic grouping capabilities. `join` will join the selected player to the specified group (specify group by addressing any of the players in that group): + +`/Kitchen/join/Office` +This will join the Kitchen player to the group that Office currently belong to. + +`/Kitchen/leave` +Kitchen will leave any group it was part of and become a standalone player. + +You don't have to ungroup a player in order to join it to another group, just join it to the new group and it will jump accordingly. + +SUB +--- + +SUB actions include the following: +`/TV%20Room/sub/off` +Turn off sub + +`/TV%20Room/sub/on` +Turn on sub + +`/TV%20Room/sub/gain/3` +Adjust gain, -15 to 15. You can make bigger adjustments, but I'm limiting it for now because it might damage the SUB. + +`/TV%20Room/sub/crossover/90` +adjust crossover frequency in hz. Official values are 50 through 110 in increments of 10. Use other values at your own risk! My SUB gave a loud bang and shut down when setting this to high, and I thought I broke it. However, a restart woke it up again. + +`/TV%20Room/sub/polarity/1` +Switch "placement adjustment" or more commonly known as phase. 0 = 0°, 1 = 180° + +Spotify, Apple Music and Amazon Music (Experimental) ---------------------- -Allows you to perform your own external searches for Apple Music or Spotify songs or albums and play a specified song or track ID. The Music Search funtionality outlined further below performs a search of its own and plays the specified music. +Allows you to perform your own external searches for Spotify, Apple Music or Amazon Music songs or albums and play a specified song or track ID. The Music Search funtionality outlined further below performs a search of its own and plays the specified music. + +Ensure you have added and registered the respective service with your Sonos account, before trying to control your speakers with node-sonos-http-api. Instructions on how to do this can be found here: https://support.sonos.com/s/article/2757?language=en_US The following endpoints are available: @@ -461,13 +846,147 @@ The following endpoints are available: # Apple Music /RoomName/applemusic/{now,next,queue}/song:{songID} /RoomName/applemusic/{now,next,queue}/album:{albumID} +/RoomName/applemusic/{now,next,queue}/playlist:{playlistID} + +# Amazon Music +/RoomName/amazonmusic/{now,next,queue}/song:{songID} +/RoomName/amazonmusic/{now,next,queue}/album:{albumID} ``` -You can find Apple Music song and album IDs via the [iTunes Search +**Spotify** + +You can find the **Spotify** track and album IDs as the last part of the URL. + +How to find the URL? +- Web player: the address bar URL for albums and playlist; select _Copy Song Link_ from the dot menu. +- Desktop client: via _Share > Copy {Album,Playlist,Song} Link_ +- Mobile client: via _Share > Copy Link_ + +For Spotify playlists, you'll need this format: `spotify:user:spotify:playlist:{playlistid}`. Even if it's a public playlist, you always need to prefix with `spotify:user:`. An example of a great playlist: `/kitchen/spotify/now/spotify:user:spotify:playlist:32O0SSXDNWDrMievPkV0Im`. + +To get more technical, you actually use the Spotify URI (not URL) for the endpoint, like so: `{room}/spotify/{now,next,queue}/{spotifyuri}`. + +It only handles a single **spotify** account currently. It will probably use the first account added on your system. + +**Apple Music** + +You can find **Apple Music** song and album IDs via the [iTunes Search API](https://affiliate.itunes.apple.com/resources/documentation/itunes-store-web-service-search-api/). -It only handles a single spotify account currently. It will probably use the first account added on your system. +You can also use iTunes to figure out song, album, and playlist IDs. Right click on a song, album, or playlist and select "Share" -> "Copy Link". You can do this when you searched within Apple Music or from your media library as long as the song is available in Apple Music. + +Have a look at the link you just copied. + +*If you shared the link to a song:* +The format is: https://itunes.apple.com/{countryCode}/album/{songName}/{albumID}?i={songID} +> eg: https://itunes.apple.com/de/album/blood-of-my-enemies/355363490?i=355364259 + +*If you shared the link to an album:* +The format is: https://itunes.apple.com/{countryCode}/album/{albumName}/{albumID} +> eg: https://itunes.apple.com/de/album/f-g-restless/355363490 + +*If you shared the link to a playlist:* +The format is: https://itunes.apple.com/{countryCode}/playlist/{playlistName}/{playlistID} +> eg: https://music.apple.com/gb/playlist/lofi-girls-favorites/pl.ed52c9eeaa0740079c21fa8e455b225e + + +**Amazon Music** + +To find **Amazon Music** song and album IDs you can use the Amazon Music App, search for a song or an album and share a link. + +Look at the link you just shared. This works with Amazon Music Prime as well as with Amazon Music Prime which is included in your Amazon Prime membership. + +*If you shared the link to a song:* +The format is: https://music.amazon.de/albums/{albumID}?trackAsin={songID}&ref=dm_sh_d74d-4daa-dmcp-63cb-e8747&musicTerritory=DE&marketplaceId=A1PA6795UKMFR9 +> eg: https://music.amazon.de/albums/B0727SH7LW?trackAsin=B071918VCR&ref=dm_sh_d74d-4daa-dmcp-63cb-e8747&musicTerritory=DE&marketplaceId=A1PA6795UKMFR9 + +*If you shared the link to an album:* +The format is: https://music.amazon.de/albums/{albumID}?ref=dm_sh_97aa-255b-dmcp-c6ba-4ff00&musicTerritory=DE&marketplaceId=A1PA6795UKMFR9 +> eg: https://music.amazon.de/albums/B0727SH7LW?ref=dm_sh_97aa-255b-dmcp-c6ba-4ff00&musicTerritory=DE&marketplaceId=A1PA6795UKMFR9 + +BBC Sounds (as of 2022 only available in the UK) +---------------------- +Ensure you have added and registered the BBC Sounds service with your Sonos account, before trying to control your speakers with node-sonos-http-api. Instructions on how to do this can be found here: https://www.bbc.co.uk/sounds/help/questions/listening-on-a-smart-speaker/sonos or here: https://support.sonos.com/s/article/2757?language=en_US + +You can specify a BBC station and the station will be played or set depending on the command used. + +To play immediately: +``` +/RoomName/bbcsounds/play/{stream code} +``` +To set the station without playing: +``` +/RoomName/bbcsounds/set/{stream code} +``` +Refer to the table below for available codes for BBC Radio Stations + +| BBC Radio Station Name | Stream Code | +|----------------------------------|----------------------------------| +| BBC Radio 1 | bbc_radio_one | +| BBC 1Xtra | bbc_1xtra | +| BBC 1Dance | bbc_radio_one_dance | +| BBC 1Relax | bbc_radio_one_relax | +| BBC Radio 2 | bbc_radio_two | +| BBC Radio 3 | bbc_radio_three | +| BBC Radio 4 FM | bbc_radio_fourfm | +| BBC Radio 4 LW | bbc_radio_fourlw | +| BBC Radio 4 Extra | bbc_radio_four_extra | +| BBC Radio 5 Live | bbc_radio_five_live | +| BBC Radio 5 Live Sports Extra | bbc_five_live_sports_extra | +| BBC Radio 6 Music | bbc_6music | +| BBC Asian Network | bbc_asian_network | +| BBC Radio Berkshire | bbc_radio_berkshire | +| BBC Radio Bristol | bbc_radio_bristol | +| BBC Radio Cambridge | bbc_radio_cambridge | +| BBC Radio Cornwall | bbc_radio_cornwall | +| BBC Radio Cumbria | bbc_radio_cumbria | +| BBC Radio Cymru | bbc_radio_cymru | +| BBC Radio Cymru 2 | bbc_radio_cymru_2 | +| BBC Radio CWR | bbc_radio_coventry_warwickshire | +| BBC Radio Derby | bbc_radio_derby | +| BBC Radio Devon | bbc_radio_devon | +| BBC Radio Essex | bbc_radio_essex | +| BBC Radio Foyle | bbc_radio_foyle | +| BBC Radio Gloucestershire | bbc_radio_gloucestershire | +| BBC Radio Guernsey | bbc_radio_guernsey | +| BBC Radio Hereford Worcester | bbc_radio_hereford_worcester | +| BBC Radio Humberside | bbc_radio_humberside | +| BBC Radio Jersey | bbc_radio_jersey | +| BBC Radio Kent | bbc_radio_kent | +| BBC Radio Lancashire | bbc_radio_lancashire | +| BBC Radio Leeds | bbc_radio_leeds | +| BBC Radio Leicester | bbc_radio_leicester | +| BBC Radio Lincolnshire | bbc_radio_lincolnshire | +| BBC Radio London | bbc_london | +| BBC Radio Manchester | bbc_radio_manchester | +| BBC Radio Merseyside | bbc_radio_merseyside | +| BBC Radio nan Gaidheal | bbc_radio_nan_gaidheal | +| BBC Radio Newcastle | bbc_radio_newcastle | +| BBC Radio Norfolk | bc_radio_norfolk | +| BBC Radio Northampton | bbc_radio_northampton | +| BBC Radio Nottingham | bbc_radio_nottingham | +| BBC Radio Oxford | bbc_radio_oxford | +| BBC Radio Scotland FM | bbc_radio_scotland_fm | +| BBC Radio Scotland Extra | bbc_radio_scotland_mw | +| BBC Radio Sheffield | bbc_radio_sheffield | +| BBC Radio Shropshire | bbc_radio_shropshire | +| BBC Radio Solent | bbc_radio_solent | +| BBC Radio Somerset | bbc_radio_somerset_sound | +| BBC Radio Stoke | bbc_radio_stoke | +| BBC Radio Suffolk | bbc_radio_suffolk | +| BBC Radio Surrey | bbc_radio_surrey | +| BBC Radio Sussex | bbc_radio_sussex | +| BBC Radio Tees | bbc_tees | +| BBC Radio Three Counties Radio | bbc_three_counties_radio | +| BBC Radio Ulster | bbc_radio_ulster | +| BBC Radio Wales | bbc_radio_wales_fm | +| BBC Radio Wales Extra | bbc_radio_wales_am | +| BBC Radio Wiltshire | bbc_radio_wiltshire | +| BBC Radio WM | bbc_wm | +| BBC Radio York | bbc_radio_york | +| BBC World_Service | bbc_world_service | +| Cbeebies Radio | cbeebies_radio | SiriusXM ---------------------- @@ -480,14 +999,14 @@ You can specify a SiriusXM channel number or station name and the station will b Pandora ---------------------- -Perform a search for one of your Pandora stations and begin playing. Give the currently playing song a thumbs up or thumbs down. Requires a valid Pandora account and credentials. +Perform a search for one of your Pandora stations and begin playing. Give the currently playing song a thumbs up or thumbs down. Requires a valid Pandora account and credentials. The following endpoints are available: ``` /RoomName/pandora/play/{station name} Plays the closest match to the specified station name in your list of Pandora stations /RoomName/pandora/thumbsup Gives the current playing Pandora song a thumbs up -/RoomName/pandora/thumbsdown Gives the current playing Pandora song a thumbs down +/RoomName/pandora/thumbsdown Gives the current playing Pandora song a thumbs down ``` Your Pandora credentials need to be added to the settings.json file @@ -498,11 +1017,32 @@ Your Pandora credentials need to be added to the settings.json file "password": "your-pandora-password" } ``` - + + +Tunein +---------------------- +Given a station id this will play or set the streaming broadcast via the tunein service. You can find tunein station ids via services like [radiotime](http://opml.radiotime.com/) + +The following endpoint is available: + +``` +/RoomName/tunein/play/{station id} +Will set and start playing given Station id + +/RoomName/tunein/set/{station id} +Will set without start playing given Station id +``` + +For example to play Radio 6 Music - [tunein.com/radio/s44491](https://tunein.com/radio/s44491) + +``` +/RoomName/tunein/play/44491 +note the droping of the 's' in 's44491' +``` Music Search and Play ---------------------- -Perform a search for a song, artist, album or station and begin playing. Supports Apple Music, Spotify, Deezer, Deezer Elite, and your local Music Library. +Perform a search for a song, artist, album or station and begin playing. Supports Apple Music, Spotify, Deezer, Deezer Elite, and your local Music Library. The following endpoint is available: @@ -511,13 +1051,13 @@ The following endpoint is available: Service options: apple, spotify, deezer, elite, library -Type options for apple, spotify, deezer, and elite: album, song, station -Station plays a Pandora like artist radio station for a specified artist name. +Type options for apple, spotify, deezer, and elite: album, song, station, playlist +Station plays a Pandora like artist radio station for a specified artist name. Apple Music also supports song titles and artist name + song title. -Type options for library: album, song, load -Load performs an initial load or relaod of the local Sonos music library. -The music library will also get loaded the first time that the library service is +Type options for library: album, song, load +Load performs an initial load or relaod of the local Sonos music library. +The music library will also get loaded the first time that the library service is used if the load command has not been issued before. Search terms for song for all services: artist name, song title, artist name + song title @@ -528,8 +1068,8 @@ Search terms for station for spotify and deezer: artist name Search terms for station for library: not supported Specifying just an artist name will load the queue with up to 50 of the artist's most popular songs -Specifying a song title or artist + song title will insert the closest match to the song into -the queue and start playing it. More than 50 tracks can be loaded from the local library by using +Specifying a song title or artist + song title will insert the closest match to the song into +the queue and start playing it. More than 50 tracks can be loaded from the local library by using library.randomQueueLimit in the settings.json file to set the maximum to a higher value. Examples: @@ -541,6 +1081,9 @@ Examples: /Playroom/musicsearch/library/album/red+hot+chili+peppers+the+getaway /Kitchen/musicsearch/spotify/album/dark+necessities +/Kitchen/musicsearch/spotify/playlist/morning+acoustic +/Kitchen/musicsearch/spotify/playlist/dinner+with+friends + /Den/musicsearch/spotify/station/red+hot+chili+peppers /Kitchen/musicsearch/apple/station/dark+necessities (Only Apple Music supports song titles) /Playroom/musicsearch/apple/station/red+hot+chili+peppers+dark+necessities (Only Apple Music supports song titles) @@ -609,3 +1152,93 @@ or "data" property will be equal to the same data as you would get from /RoomName/state or /zones. There is an example endpoint in the root if this project called test_endpoint.js which you may fire up to get an understanding of what is posted, just invoke it with "node test_endpoint.js" in a terminal, and then start the http-api in another terminal. +Server Sent Events +----- + +As an alternative to the web hook you can also call the `/events` endpoint to receive every state change and topology change as [Server Sent Event](https://html.spec.whatwg.org/multipage/server-sent-events.html#server-sent-events). +Compared to the web hook there is no configuration required on the server, and you can listen for events from multiple clients. + +Because it is a long-polling connection, you must take care of errors in your client code and re-connect if necessary. + +The server sends events formatted as single-line JSON in the format of Server Sent Events: every event starts with the string `data: `, followed by the single-line JSON formatted event, and is terminated by two new line characters. + +There are [several client libraries available](https://en.wikipedia.org/wiki/Server-sent_events#Libraries) to listen for Server Sent Events. +Using `curl` yields the following output for some volume changes: + +```shell +host:~ user$ curl localhost:5005/events +data: {"type":"volume-change","data":{"uuid":"RINCON_E2832F58D9074C45B","previousVolume":13,"newVolume":19,"roomName":"Office"}} + +data: {"type":"volume-change","data":{"uuid":"RINCON_E2832F58D9074C45B","previousVolume":19,"newVolume":25,"roomName":"Office"}} + +data: {"type":"volume-change","data":{"uuid":"RINCON_E2832F58D9074C45B","previousVolume":25,"newVolume":24,"roomName":"Office"}} + +data: {"type":"volume-change","data":{"uuid":"RINCON_E2832F58D9074C45B","previousVolume":23,"newVolume":23,"roomName":"Office"}} + +``` + +DOCKER +----- + +Docker usage is maintained by [Chris Nesbitt-Smith](https://github.com/chrisns) at [chrisns/docker-node-sonos-http-api](https://github.com/chrisns/docker-node-sonos-http-api) + +## FIREWALL + +If you are running this in an environment where you manually have to unblock traffic to and from the machine, the following traffic needs to be allowed: + +### Incoming +``` +TCP, port 3500 (Sonos events) +UDP, port 1905 (Sonos initial discovery) +TCP, port 5005 (if using the default api port) +TCP, port 5006 (if using https support, optional) +``` +### Outgoing +``` +TCP, port 1400 (Sonos control commands) +UDP, port 1900 (Sonos initial discovery) +TCP, whatever port used for webhooks (optional) +TCP, port 80/443 (for looking up hig res cover arts on various music services) +``` + +The UDP traffic is a mixture of multicast (outgoing), broadcast (outgoing) and unicast (incoming). The multicast address is 239.255.255.250, the broadcast is 255.255.255.255 and the unicast is from the Sonos players. + +If port 3500 is occupied while trying to bind it, it will try using 3501, 3502, 3503 etc. You would need to adjust your firewall rules accordingly, if running multiple instances of this software, or any other software utilizing these ports. + +### Projects built with this API + +**Alexa For Sonos (Alexa Skills)** + +Amazon Alexa voice layer on top of the amazing NodeJS component +https://github.com/hypermoose/AlexaForSonos + +**Echo Sonos (Alexa Skills)** + +Amazon Echo integration with Sonos +https://github.com/rgraciano/echo-sonos + +**JukeBot (Ruby)** + +A Slack bot that can control a Sonos instance. Custom spotify integration to find music. +https://github.com/estiens/jukebot + +**Sonos Controller (JS / Electron)** + +A Sonos controller, built with the Electron framework. +https://github.com/anton-christensen/sonos-controller + +**Sonos Cron (PHP)** + +Service for retrieving commands from an AWS SQS queue and passing them to an instance of the Sonos HTTP API +https://github.com/cjrpaterson/sonos-cron + +**Sonos Push Server (JS)** + +A Node server to receive notifications from node-sonos-http-api and push them via socket.io to the clients. +https://github.com/TimoKorinth/sonos-push-server + +**SonoBoss (Siri Shortcut)** + +A ChatGPT-assisted Siri Shortcut that acts as a virtual assistant to let you find music and control Sonos through voice and chat. +https://github.com/Barloew/SonoBoss + diff --git a/lib/actions/aldilifeMusic.js b/lib/actions/aldilifeMusic.js new file mode 100644 index 00000000..570a7285 --- /dev/null +++ b/lib/actions/aldilifeMusic.js @@ -0,0 +1,65 @@ +'use strict'; +function getMetadata(id, parentUri, type, title) { + return ` + "${title}"${type} + SA_RINCON55303_X_#Svc55303-0-Token`; +} + +function getUri(id, type) { + var uri = { + song: `x-sonos-http:ondemand_track%3a%3atra.${id}%7cv1%7cALBUM%7calb.mp4?sid=216&flags=8224&sn=13`, + album: `x-rincon-cpcontainer:100420ecexplore%3aalbum%3a%3aAlb.${id}` + }; + + return uri[type]; +} + +const CLASSES = { + song: 'object.item.audioItem.musicTrack', + album: 'object.container.album.musicAlbum' +}; + +const METADATA_URI_STARTERS = { + song: '10032020ondemand_track%3a%3atra.', + album: '100420ec' +}; + +const PARENTS = { + song: '100420ecexplore%3a', + album: '100420ecexplore%3aalbum%3a' +}; + +function aldilifeMusic(player, values) { + const action = values[0]; + const trackID = values[1].split(':')[1]; + const type = values[1].split(':')[0]; + var nextTrackNo = 0; + + const metadataID = METADATA_URI_STARTERS[type] + encodeURIComponent(trackID); + const metadata = getMetadata(metadataID, PARENTS[type], CLASSES[type], ''); + const uri = getUri(encodeURIComponent(trackID), type); + + if (action == 'queue') { + return player.coordinator.addURIToQueue(uri, metadata); + } else if (action == 'now') { + nextTrackNo = player.coordinator.state.trackNo + 1; + let promise = Promise.resolve(); + if (player.coordinator.avTransportUri.startsWith('x-rincon-queue') === false) { + promise = promise.then(() => player.coordinator.setAVTransport(`x-rincon-queue:${player.coordinator.uuid}#0`)); + } + + return promise.then(() => { + return player.coordinator.addURIToQueue(uri, metadata, true, nextTrackNo) + .then((addToQueueStatus) => player.coordinator.trackSeek(addToQueueStatus.firsttracknumberenqueued)) + .then(() => player.coordinator.play()); + }); + } else if (action == 'next') { + nextTrackNo = player.coordinator.state.trackNo + 1; + return player.coordinator.addURIToQueue(uri, metadata, true, nextTrackNo); + } +} + +module.exports = function (api) { + api.registerAction('aldilifemusic', aldilifeMusic); +}; diff --git a/lib/actions/amazonMusic.js b/lib/actions/amazonMusic.js new file mode 100644 index 00000000..35207364 --- /dev/null +++ b/lib/actions/amazonMusic.js @@ -0,0 +1,79 @@ +'use strict'; +function getMetadata(id, parentUri, type, title) { +return ` + + "${title}" + ${type} + SA_RINCON51463_X_#Svc51463-0-Token + +`; +} + +function getSongUri(id) { + return `x-sonosapi-hls-static:catalog%2ftracks%2f${id}%2f%3falbumAsin%3dB01JDKZWK0?sid=201&flags=0&sn=4`; +} + +function getAlbumUri(id) { + return `x-rincon-cpcontainer:1004206ccatalog%2falbums%2f${id}%2f%23album_desc?sid=201&flags=8300&sn=4`; +} + +const uriTemplates = { + song: getSongUri, + album: getAlbumUri +}; + +const CLASSES = { + song: 'object.container.album.musicAlbum.#AlbumView', + album: 'object.container.album.musicAlbum' +}; + +const METADATA_URI_STARTERS = { + song: '10030000catalog%2ftracks%2f', + album: '1004206ccatalog' +}; + +const METADATA_URI_ENDINGS = { + song: '%2f%3falbumAsin%3d', + album: '%2f%23album_desc' +}; + + +const PARENTS = { + song: '1004206ccatalog%2falbums%2f', + album: '10052064catalog%2fartists%2f' +}; + +function amazonMusic(player, values) { + const action = values[0]; + const track = values[1]; + const type = track.split(':')[0]; + const trackID = track.split(':')[1]; + + var nextTrackNo = 0; + + const metadataID = METADATA_URI_STARTERS[type] + encodeURIComponent(trackID) + METADATA_URI_ENDINGS[type]; + + const metadata = getMetadata(metadataID, PARENTS[type], CLASSES[type], ''); + const uri = uriTemplates[type](encodeURIComponent(trackID)); + + if (action == 'queue') { + return player.coordinator.addURIToQueue(uri, metadata); + } else if (action == 'now') { + nextTrackNo = player.coordinator.state.trackNo + 1; + let promise = Promise.resolve(); + if (player.coordinator.avTransportUri.startsWith('x-rincon-queue') === false) { + promise = promise.then(() => player.coordinator.setAVTransport(`x-rincon-queue:${player.coordinator.uuid}#0`)); + } + + return promise.then(() => player.coordinator.addURIToQueue(uri, metadata, true, nextTrackNo)) + .then(() => { if (nextTrackNo != 1) player.coordinator.nextTrack() }) + .then(() => player.coordinator.play()); + } else if (action == 'next') { + nextTrackNo = player.coordinator.state.trackNo + 1; + return player.coordinator.addURIToQueue(uri, metadata, true, nextTrackNo); + } +} + +module.exports = function (api) { + api.registerAction('amazonmusic', amazonMusic); +}; diff --git a/lib/actions/appleMusic.js b/lib/actions/appleMusic.js index 2842df77..015fac90 100644 --- a/lib/actions/appleMusic.js +++ b/lib/actions/appleMusic.js @@ -1,8 +1,9 @@ +'use strict'; function getMetadata(id, parentUri, type, title) { return ` - "${title}"object.item.audioItem.${type} + "${title}"${type} SA_RINCON52231_X_#Svc52231-0-Token`; } @@ -14,49 +15,63 @@ function getAlbumUri(id) { return `x-rincon-cpcontainer:0004206c${id}`; } +function getPlaylistUri(id) { + return `x-rincon-cpcontainer:1006206c${id}`; +} + const uriTemplates = { song: getSongUri, - album: getAlbumUri + album: getAlbumUri, + playlist: getPlaylistUri, }; const CLASSES = { - song: 'musicTrack', - album: 'musicAlbum' + song: 'object.item.audioItem.musicTrack', + album: 'object.item.audioItem.musicAlbum', + playlist: 'object.container.playlistContainer.#PlaylistView' }; const METADATA_URI_STARTERS = { song: '00032020', - album: '0004206c' + album: '0004206c', + playlist: '1006206c' }; const PARENTS = { song: '0004206calbum%3a', - album: '00020000album%3a' + album: '00020000album%3a', + playlist: '1006206cplaylist%3a' }; function appleMusic(player, values) { const action = values[0]; const trackID = values[1]; const type = trackID.split(':')[0]; - var nextTrackNo = 0; + let nextTrackNo = 0; const metadataID = METADATA_URI_STARTERS[type] + encodeURIComponent(trackID); const metadata = getMetadata(metadataID, PARENTS[type], CLASSES[type], ''); const uri = uriTemplates[type](encodeURIComponent(trackID)); - if (action == 'queue') { + if (action === 'queue') { return player.coordinator.addURIToQueue(uri, metadata); - } else if (action == 'now') { + } else if (action === 'now') { nextTrackNo = player.coordinator.state.trackNo + 1; - return player.coordinator.addURIToQueue(uri, metadata, true, nextTrackNo) - .then(() => player.coordinator.nextTrack()) + let promise = Promise.resolve(); + if (player.coordinator.avTransportUri.startsWith('x-rincon-queue') === false) { + promise = promise.then(() => player.coordinator.setAVTransport(`x-rincon-queue:${player.coordinator.uuid}#0`)); + } + return promise.then(() => player.coordinator.addURIToQueue(uri, metadata, true, nextTrackNo)) + .then(() => { if (nextTrackNo !== 1) player.coordinator.nextTrack(); }) .then(() => player.coordinator.play()); - } else if (action == 'next') { + } else if (action === 'next') { nextTrackNo = player.coordinator.state.trackNo + 1; return player.coordinator.addURIToQueue(uri, metadata, true, nextTrackNo); } + + return null; } -module.exports = function (api) { +module.exports = function appleMusicAction(api) { api.registerAction('applemusic', appleMusic); }; diff --git a/lib/actions/bbcSounds.js b/lib/actions/bbcSounds.js new file mode 100644 index 00000000..cb4d63c9 --- /dev/null +++ b/lib/actions/bbcSounds.js @@ -0,0 +1,38 @@ +'use strict'; + +function getMetadata(station) { + return ` + BBC Soundsobject.item.audioItem.audioBroadcast + SA_RINCON83207_`; +} + +function getUri(station) { + return `x-sonosapi-hls:stations%7eplayable%7e%7e${station}%7e%7eurn%3abbc%3aradio%3anetwork%3a${station}?sid=325&flags=288&sn=10`; +} + +/** + * @link https://gist.github.com/bpsib/67089b959e4fa898af69fea59ad74bc3 Stream names can be found here + */ +function bbcSounds(player, values) { + const action = values[0]; + const station = encodeURIComponent(values[1]); + + if (!station) { + return Promise.reject('Expected BBC Sounds station name.'); + } + + const metadata = getMetadata(station); + const uri = getUri(station); + + if (action === 'play') { + return player.coordinator.setAVTransport(uri, metadata).then(() => player.coordinator.play()); + } else if (action === 'set') { + return player.coordinator.setAVTransport(uri, metadata); + } + + return Promise.reject('BBC Sounds only handles the {play} & {set} actions.'); +} + +module.exports = function (api) { + api.registerAction('bbcsounds', bbcSounds); +} diff --git a/lib/actions/clip.js b/lib/actions/clip.js index a566b0a0..479d8285 100644 --- a/lib/actions/clip.js +++ b/lib/actions/clip.js @@ -1,13 +1,13 @@ 'use strict'; const path = require('path'); -const crypto = require('crypto'); -const fs = require('fs'); -const http = require('http'); +const fileDuration = require('../helpers/file-duration'); const settings = require('../../settings'); const singlePlayerAnnouncement = require('../helpers/single-player-announcement'); let port; +const LOCAL_PATH_LOCATION = path.join(settings.webroot, 'clips'); + const backupPresets = {}; function playClip(player, values) { @@ -19,10 +19,13 @@ function playClip(player, values) { announceVolume = values[1]; } - return singlePlayerAnnouncement(player, `http://${player.system.localEndpoint}:${port}/clips/${clipFileName}`, announceVolume); + return fileDuration(path.join(LOCAL_PATH_LOCATION, clipFileName)) + .then((duration) => { + return singlePlayerAnnouncement(player, `http://${player.system.localEndpoint}:${port}/clips/${clipFileName}`, announceVolume, duration); + }); } -module.exports = function (api) { +module.exports = function(api) { port = api.getPort(); api.registerAction('clip', playClip); } diff --git a/lib/actions/clipall.js b/lib/actions/clipall.js index 4395a019..fe969356 100644 --- a/lib/actions/clipall.js +++ b/lib/actions/clipall.js @@ -1,9 +1,13 @@ 'use strict'; +const path = require('path'); const settings = require('../../settings'); const allPlayerAnnouncement = require('../helpers/all-player-announcement'); +const fileDuration = require('../helpers/file-duration'); let port; +const LOCAL_PATH_LOCATION = path.join(settings.webroot, 'clips'); + function playClipOnAll(player, values) { const clipFileName = values[0]; let announceVolume = settings.announceVolume || 40; @@ -13,7 +17,10 @@ function playClipOnAll(player, values) { announceVolume = values[1]; } - return allPlayerAnnouncement(player.system, `http://${player.system.localEndpoint}:${port}/clips/${clipFileName}`, announceVolume); + return fileDuration(path.join(LOCAL_PATH_LOCATION, clipFileName)) + .then((duration) => { + return allPlayerAnnouncement(player.system, `http://${player.system.localEndpoint}:${port}/clips/${clipFileName}`, announceVolume, duration); + }); } module.exports = function (api) { diff --git a/lib/actions/clippreset.js b/lib/actions/clippreset.js new file mode 100644 index 00000000..dfa0e3f0 --- /dev/null +++ b/lib/actions/clippreset.js @@ -0,0 +1,30 @@ +'use strict'; +const path = require('path'); +const settings = require('../../settings'); +const presetAnnouncement = require('../helpers/preset-announcement'); +const fileDuration = require('../helpers/file-duration'); +const presets = require('../presets-loader'); + +let port; +const LOCAL_PATH_LOCATION = path.join(settings.webroot, 'clips'); + +function playClipOnPreset(player, values) { + const presetName = decodeURIComponent(values[0]); + const clipFileName = decodeURIComponent(values[1]); + + const preset = presets[presetName]; + + if (!preset) { + return Promise.reject(new Error(`No preset named ${presetName} could be found`)); + } + + return fileDuration(path.join(LOCAL_PATH_LOCATION, clipFileName)) + .then((duration) => { + return presetAnnouncement(player.system, `http://${player.system.localEndpoint}:${port}/clips/${clipFileName}`, preset, duration); + }); +} + +module.exports = function (api) { + port = api.getPort(); + api.registerAction('clippreset', playClipOnPreset); +} diff --git a/lib/actions/debug.js b/lib/actions/debug.js new file mode 100644 index 00000000..f60f3e06 --- /dev/null +++ b/lib/actions/debug.js @@ -0,0 +1,29 @@ +'use strict'; +const pkg = require('../../package.json'); + +function debug(player) { + const system = player.system; + const debugInfo = { + version: pkg.version, + system: { + localEndpoint: system.localEndpoint, + availableServices: system.availableServices, + }, + players: system.players.map(x => ({ + roomName: x.roomName, + uuid: x.uuid, + coordinator: x.coordinator.uuid, + avTransportUri: x.avTransportUri, + avTransportUriMetadata: x.avTransportUriMetadata, + enqueuedTransportUri: x.enqueuedTransportUri, + enqueuedTransportUriMetadata: x.enqueuedTransportUriMetadata, + baseUrl: x.baseUrl, + state: x._state + })) + }; + return Promise.resolve(debugInfo); +} + +module.exports = function (api) { + api.registerAction('debug', debug); +} diff --git a/lib/actions/equalizer.js b/lib/actions/equalizer.js new file mode 100644 index 00000000..66c50fd0 --- /dev/null +++ b/lib/actions/equalizer.js @@ -0,0 +1,34 @@ +'use strict'; + +function nightMode(player, values) { + let enable = values[0] === 'on'; + if(values[0] == "toggle") enable = !player.coordinator.state.equalizer.nightMode; + return player.nightMode(enable).then((response) => { + return { status: 'success', nightmode: enable }; + }); +} + +function speechEnhancement(player, values) { + let enable = values[0] === 'on'; + if(values[0] == "toggle") enable = !player.coordinator.state.equalizer.speechEnhancement; + return player.speechEnhancement(enable).then((response) => { + return { status: 'success', speechenhancement: enable }; + }); +} + +function bass(player, values) { + const level = parseInt(values[0]); + return player.setBass(level); +} + +function treble(player, values) { + const level = parseInt(values[0]); + return player.setTreble(level); +} + +module.exports = function (api) { + api.registerAction('nightmode', nightMode); + api.registerAction('speechenhancement', speechEnhancement); + api.registerAction('bass', bass); + api.registerAction('treble', treble); +} diff --git a/lib/actions/favorite.js b/lib/actions/favorite.js index 59bf326f..e16394c3 100644 --- a/lib/actions/favorite.js +++ b/lib/actions/favorite.js @@ -1,3 +1,4 @@ +'use strict'; function favorite(player, values) { return player.coordinator.replaceWithFavorite(decodeURIComponent(values[0])) .then(() => player.coordinator.play()); diff --git a/lib/actions/group.js b/lib/actions/group.js index cb67bf04..49fec91a 100644 --- a/lib/actions/group.js +++ b/lib/actions/group.js @@ -6,7 +6,7 @@ function addToGroup(player, values) { const joiningPlayer = player.system.getPlayer(joiningRoomName); if(!joiningPlayer) { logger.warn(`Room ${joiningRoomName} not found - can't group with ${player.roomName}`); - return; + return Promise.reject(new Error(`Room ${joiningRoomName} not found - can't group with ${player.roomName}`)); } return attachTo(joiningPlayer, player.coordinator); } @@ -16,21 +16,11 @@ function joinPlayer(player, values) { const receivingPlayer = player.system.getPlayer(receivingRoomName); if(!receivingPlayer) { logger.warn(`Room ${receivingRoomName} not found - can't make ${player.roomName} join it`); - return; + return Promise.reject(new Error(`Room ${receivingRoomName} not found - can't make ${player.roomName} join it`)); } return attachTo(player, receivingPlayer.coordinator); } -function removeFromGroup(player, values) { - const leavingRoomName = decodeURIComponent(values[0]); - var leavingPlayer = player.system.getPlayer(leavingRoomName); - if(!leavingPlayer) { - logger.warn(`Room ${leavingRoomName} not found - can't remove from group of ${player.roomName}`); - return; - } - return isolate(leavingPlayer); -} - function rinconUri(player) { return `x-rincon:${player.uuid}`; } @@ -48,6 +38,5 @@ module.exports = function (api) { api.registerAction('isolate', isolate); api.registerAction('ungroup', isolate); api.registerAction('leave', isolate); - api.registerAction('remove', removeFromGroup); api.registerAction('join', joinPlayer); } diff --git a/lib/actions/lockvolumes.js b/lib/actions/lockvolumes.js index 7245ab87..26315960 100644 --- a/lib/actions/lockvolumes.js +++ b/lib/actions/lockvolumes.js @@ -1,30 +1,32 @@ -var lockVolumes = {}; +'use strict'; +const logger = require('sonos-discovery/lib/helpers/logger'); +const lockVolumes = {}; function lockvolumes(player) { - console.log("locking volumes"); + logger.debug('locking volumes'); // Locate all volumes var system = player.system; - for (var i in system.players) { - var player = system.players[i]; - lockVolumes[i] = player.state.volume; - } + system.players.forEach((player) => { + lockVolumes[player.uuid] = player.state.volume; + }); + // prevent duplicates, will ignore if no event listener is here - system.removeListener("volume", restrictVolume); - system.on("volume", restrictVolume); + system.removeListener('volume-change', restrictVolume); + system.on('volume-change', restrictVolume); return Promise.resolve(); } function unlockvolumes(player) { - console.log("unlocking volumes"); + logger.debug('unlocking volumes'); var system = player.system; - system.removeListener("volume", restrictVolume); + system.removeListener('volume-change', restrictVolume); return Promise.resolve(); } function restrictVolume(info) { - console.log("should revert volume to", lockVolumes[info.uuid]); - var player = this.getPlayerByUUID(info.uuid); + logger.debug(`should revert volume to ${lockVolumes[info.uuid]}`); + const player = this.getPlayerByUUID(info.uuid); // Only do this if volume differs if (player.state.volume != lockVolumes[info.uuid]) return player.setVolume(lockVolumes[info.uuid]); diff --git a/lib/actions/musicSearch.js b/lib/actions/musicSearch.js index 3e8522dc..5625284c 100644 --- a/lib/actions/musicSearch.js +++ b/lib/actions/musicSearch.js @@ -1,3 +1,4 @@ +'use strict'; const request = require('request-promise'); const fs = require("fs"); const isRadioOrLineIn = require('../helpers/is-radio-or-line-in'); @@ -10,7 +11,7 @@ const libraryDef = require('../music_services/libraryDef'); const musicServices = ['apple','spotify','deezer','elite','library']; const serviceNames = {apple:'Apple Music',spotify:'Spotify',deezer:'Deezer',elite:'Deezer',library:'Library'}; -const musicTypes = ['album','song','station','load']; +const musicTypes = ['album','song','station','load','playlist']; var country = ''; var accountId = ''; @@ -61,11 +62,20 @@ function getAccountId(player, service) } } +function getRequestOptions(serviceDef, url) { + const headers = serviceDef.headers(); + return { + url: url, + json: true, + headers: headers, + } +}; function doSearch(service, type, term) { var serviceDef = getService(service); var url = serviceDef.search[type]; + var authenticate = serviceDef.authenticate; term = decodeURIComponent(term); @@ -113,14 +123,14 @@ function doSearch(service, type, term) .then((res) => { country = res.country; url += serviceDef.country + country; - return request({url: url, json: true}); + return authenticate().then(() => request(getRequestOptions(serviceDef, url))); }); } else { if (serviceDef.country != '') { url += serviceDef.country + country; } - return request({url: url, json: true}); + return authenticate().then(() => request(getRequestOptions(serviceDef, url))); } } @@ -133,7 +143,7 @@ Array.prototype.shuffle=function(){ return this; } -function loadTracks(service, type, tracksJson) +function loadTracks(player, service, type, tracksJson) { var tracks = getService(service).tracks(type, tracksJson); @@ -171,11 +181,13 @@ function loadTracks(service, type, tracksJson) } else { tracks.isArtist = (searchType == 2); } - if (tracks.isArtist) { - tracks.queueTracks.shuffle(); - } } - + + //To avoid playing the same song first in a list of artist tracks when shuffle is on + if (tracks.isArtist && player.coordinator.state.playMode.shuffle) { + tracks.queueTracks.shuffle(); + } + return tracks; } @@ -184,9 +196,8 @@ function musicSearch(player, values) { const service = values[0]; const type = values[1]; const term = values[2]; - const queueURI = 'x-rincon-queue:' + player.uuid + '#0'; - var serviceDef = null; - + const queueURI = 'x-rincon-queue:' + player.coordinator.uuid + '#0'; + if (musicServices.indexOf(service) == -1) { return Promise.reject('Invalid music service'); } @@ -201,11 +212,11 @@ function musicSearch(player, values) { return getAccountId(player, service) .then(() => { - serviceDef = getService(service); - serviceDef.service(player, accountId, accountSN, country); return doSearch(service, type, term); }) .then((resList) => { + const serviceDef = getService(service); + serviceDef.service(player, accountId, accountSN, country); if (serviceDef.empty(type, resList)) { return Promise.reject('No matches were found'); } else { @@ -217,30 +228,22 @@ function musicSearch(player, values) { return player.coordinator.setAVTransport(UaM.uri, UaM.metadata) .then(() => player.coordinator.play()); } else - if ((type == 'album') && (service != 'library')) { + if ((type == 'album' || type =='playlist') && (service != 'library')) { UaM = serviceDef.urimeta(type, resList); return player.coordinator.clearQueue() - .then(() => { - if (isRadioOrLineIn(player.coordinator.avTransportUri)) { - return player.coordinator.setAVTransport(queueURI, ''); - } - }) + .then(() => player.coordinator.setAVTransport(queueURI, '')) .then(() => player.coordinator.addURIToQueue(UaM.uri, UaM.metadata, true, 1)) .then(() => player.coordinator.play()); } else { // Play songs - var tracks = loadTracks(service, type, resList); + var tracks = loadTracks(player, service, type, resList); if (tracks.count == 0) { return Promise.reject('No matches were found'); } else { if (tracks.isArtist) { // Play numerous songs by the specified artist return player.coordinator.clearQueue() - .then(() => { - if (isRadioOrLineIn(player.coordinator.avTransportUri)) { - return player.coordinator.setAVTransport(queueURI, ''); - } - }) + .then(() => player.coordinator.setAVTransport(queueURI, '')) .then(() => player.coordinator.addURIToQueue(tracks.queueTracks[0].uri, tracks.queueTracks[0].metadata, true, 1)) .then(() => player.coordinator.play()) .then(() => { @@ -259,11 +262,7 @@ function musicSearch(player, values) { nextTrackNo = (empty) ? 1 : player.coordinator.state.trackNo + 1; }) .then(() => player.coordinator.addURIToQueue(tracks.queueTracks[0].uri, tracks.queueTracks[0].metadata, true, nextTrackNo)) - .then(() => { - if (isRadioOrLineIn(player.coordinator.state.currentTrack.uri)) { - return player.coordinator.setAVTransport(queueURI, ''); - } - }) + .then(() => player.coordinator.setAVTransport(queueURI, '')) .then(() => { if (!empty) { return player.coordinator.nextTrack(); diff --git a/lib/actions/mute.js b/lib/actions/mute.js index 4fdf2395..6ac2a76e 100644 --- a/lib/actions/mute.js +++ b/lib/actions/mute.js @@ -1,3 +1,4 @@ +'use strict'; function mute(player) { return player.mute(); } @@ -14,6 +15,17 @@ function groupUnmute(player) { return player.coordinator.unMuteGroup(); } +function toggleMute(player) { + let ret = { status: 'success', muted: true }; + + if(player.state.mute) { + ret.muted = false; + return player.unMute().then((response) => { return ret; }); + }; + + return player.mute().then((response) => { return ret; }); +} + module.exports = function (api) { api.registerAction('mute', mute); api.registerAction('unmute', unmute); @@ -21,4 +33,5 @@ module.exports = function (api) { api.registerAction('groupunmute', groupUnmute); api.registerAction('mutegroup', groupMute); api.registerAction('unmutegroup', groupUnmute); + api.registerAction('togglemute', toggleMute); } diff --git a/lib/actions/napster.js b/lib/actions/napster.js new file mode 100644 index 00000000..7540800b --- /dev/null +++ b/lib/actions/napster.js @@ -0,0 +1,65 @@ +'use strict'; +function getMetadata(id, parentUri, type, title) { + return ` + "${title}"${type} + SA_RINCON51975_X_#Svc51975-0-Token`; +} + +function getUri(id, type) { + var uri = { + song: `x-sonos-http:ondemand_track%3a%3atra.${id}%7cv1%7cALBUM%7calb.mp4?sid=203&flags=8224&sn=13`, + album: `x-rincon-cpcontainer:100420ecexplore%3aalbum%3a%3aAlb.${id}` + }; + + return uri[type]; +} + +const CLASSES = { + song: 'object.item.audioItem.musicTrack', + album: 'object.container.album.musicAlbum' +}; + +const METADATA_URI_STARTERS = { + song: '10032020ondemand_track%3a%3atra.', + album: '100420ec' +}; + +const PARENTS = { + song: '100420ecexplore%3a', + album: '100420ecexplore%3aalbum%3a' +}; + +function napster(player, values) { + const action = values[0]; + const trackID = values[1].split(':')[1]; + const type = values[1].split(':')[0]; + var nextTrackNo = 0; + + const metadataID = METADATA_URI_STARTERS[type] + encodeURIComponent(trackID); + const metadata = getMetadata(metadataID, PARENTS[type], CLASSES[type], ''); + const uri = getUri(encodeURIComponent(trackID), type); + + if (action == 'queue') { + return player.coordinator.addURIToQueue(uri, metadata); + } else if (action == 'now') { + nextTrackNo = player.coordinator.state.trackNo + 1; + let promise = Promise.resolve(); + if (player.coordinator.avTransportUri.startsWith('x-rincon-queue') === false) { + promise = promise.then(() => player.coordinator.setAVTransport(`x-rincon-queue:${player.coordinator.uuid}#0`)); + } + + return promise.then(() => { + return player.coordinator.addURIToQueue(uri, metadata, true, nextTrackNo) + .then((addToQueueStatus) => player.coordinator.trackSeek(addToQueueStatus.firsttracknumberenqueued)) + .then(() => player.coordinator.play()); + }); + } else if (action == 'next') { + nextTrackNo = player.coordinator.state.trackNo + 1; + return player.coordinator.addURIToQueue(uri, metadata, true, nextTrackNo); + } +} + +module.exports = function (api) { + api.registerAction('napster', napster); +}; diff --git a/lib/actions/nextprevious.js b/lib/actions/nextprevious.js index 6cab0283..236a4eb0 100644 --- a/lib/actions/nextprevious.js +++ b/lib/actions/nextprevious.js @@ -1,3 +1,4 @@ +'use strict'; function next(player) { return player.coordinator.nextTrack(); } diff --git a/lib/actions/pandora.js b/lib/actions/pandora.js index dc922742..faa1772e 100644 --- a/lib/actions/pandora.js +++ b/lib/actions/pandora.js @@ -1,24 +1,25 @@ 'use strict'; -const promise = require('request-promise'); -const Anesidora = require("anesidora"); +const url = require('url'); +const querystring = require('querystring'); +const Anesidora = require('anesidora'); const Fuse = require('fuse.js'); const settings = require('../../settings'); -function getPandoraMetadata(id, title, auth) { +function getPandoraMetadata(id, title, serviceType) { return ` - ${title}object.item.audioItem.audioBroadcast - SA_RINCON3_${auth}`; + ${title}object.item.audioItem.audioBroadcast.#station + SA_RINCON${serviceType}_X_#Svc${serviceType}-0-Token`; } function getPandoraUri(id, title, albumart) { - if (albumart == undefined) { - return `pndrradio:${id}?sn=2`; - } else { - return `pndrradio:${id}?sn=2,"title":"${title}","albumArtUri":"${albumart}"`; - } + return `x-sonosapi-radio:ST%3a${id}?sid=236&flags=8300&sn=1`; } +function parseQuerystring(uri) { + const parsedUri = url.parse(uri); + return querystring.parse(parsedUri.query); +} function pandora(player, values) { const cmd = values[0]; @@ -53,6 +54,8 @@ function pandora(player, values) { var uri = ''; var metadata = ''; + var sid = player.system.getServiceId('Pandora'); + return userLogin() .then(() => pandoraAPI("user.getStationList", {"includeStationArtUrl" : true})) .then((stationList) => { @@ -86,14 +89,14 @@ function pandora(player, values) { if (results.length > 0) { const station = results[0]; if (station.type == undefined) { - uri = getPandoraUri(station.stationId, station.stationName, station.artUrl); - metadata = getPandoraMetadata(station.stationId, station.stationName, settings.pandora.username); + uri = getPandoraUri(station.item.stationId, station.item.stationName, station.item.artUrl); + metadata = getPandoraMetadata(station.item.stationId, station.item.stationName, player.system.getServiceType('Pandora')); return Promise.resolve(); } else { - return pandoraAPI("station.createStation", {"musicToken":station.stationId, "musicType":station.type}) + return pandoraAPI("station.createStation", {"musicToken":station.item.stationId, "musicType":station.item.type}) .then((stationInfo) => { uri = getPandoraUri(stationInfo.stationId); - metadata = getPandoraMetadata(stationInfo.stationId, stationInfo.stationName, settings.pandora.username); + metadata = getPandoraMetadata(stationInfo.stationId, stationInfo.stationName, player.system.getServiceType('Pandora')); return Promise.resolve(); }); } @@ -112,11 +115,16 @@ function pandora(player, values) { if (cmd == 'play') { return playPandora(player, values[1]); } if ((cmd == 'thumbsup')||(cmd == 'thumbsdown')) { + + var sid = player.system.getServiceId('Pandora'); const uri = player.state.currentTrack.uri; + + const parameters = parseQuerystring(uri); - if (uri.startsWith('pndrradio-http')) { - const stationToken = uri.substring(uri.search('&x=') + 3); - const trackToken = uri.substring(uri.search('&m=') + 3,uri.search('&f=')); + if (uri.startsWith('x-sonosapi-radio') && parameters.sid == sid && player.state.currentTrack.trackUri) { + const trackUri = player.state.currentTrack.trackUri; + const trackToken = trackUri.substring(trackUri.search('x-sonos-http:') + 13, trackUri.search('%3a%3aST%3a')); + const stationToken = trackUri.substring(trackUri.search('%3a%3aST%3a') + 11, trackUri.search('%3a%3aRINCON')); const up = (cmd == 'thumbsup'); return userLogin() diff --git a/lib/actions/pauseall.js b/lib/actions/pauseall.js index 3c3c7796..9312162e 100644 --- a/lib/actions/pauseall.js +++ b/lib/actions/pauseall.js @@ -1,11 +1,13 @@ +'use strict'; +const logger = require('sonos-discovery/lib/helpers/logger'); var pausedPlayers = []; function pauseAll(player, values) { - console.log("pausing all players"); + logger.debug("pausing all players"); // save state for resume if (values[0] && values[0] > 0) { - console.log("in", values[0], "minutes"); + logger.debug("in", values[0], "minutes"); setTimeout(function () { doPauseAll(player.system); }, values[0] * 1000 * 60); @@ -16,10 +18,10 @@ function pauseAll(player, values) { } function resumeAll(player, values) { - console.log("resuming all players"); + logger.debug("resuming all players"); if (values[0] && values[0] > 0) { - console.log("in", values[0], "minutes"); + logger.debug("in", values[0], "minutes"); setTimeout(function () { doResumeAll(player.system); }, values[0] * 1000 * 60); @@ -33,11 +35,9 @@ function doPauseAll(system) { pausedPlayers = []; const promises = system.zones .filter(zone => { - console.log(zone.coordinator.state) return zone.coordinator.state.playbackState === 'PLAYING' }) .map(zone => { - console.log(zone.uuid) pausedPlayers.push(zone.uuid); const player = system.getPlayerByUUID(zone.uuid); return player.pause(); @@ -62,4 +62,4 @@ function doResumeAll(system) { module.exports = function (api) { api.registerAction('pauseall', pauseAll); api.registerAction('resumeall', resumeAll); -} \ No newline at end of file +} diff --git a/lib/actions/playlists.js b/lib/actions/playlists.js index b7b7e5b3..4e068d3a 100644 --- a/lib/actions/playlists.js +++ b/lib/actions/playlists.js @@ -1,3 +1,4 @@ +'use strict'; function playlists(player, values) { return player.system.getPlaylists() diff --git a/lib/actions/playmode.js b/lib/actions/playmode.js index 8dcd101f..7aa2f634 100644 --- a/lib/actions/playmode.js +++ b/lib/actions/playmode.js @@ -1,17 +1,42 @@ +'use strict'; function repeat(player, values) { - return player.coordinator.repeat(values[0] == "on" ? true : false); + let mode = values[0]; + + if (mode === "on") { + mode = "all"; + } else if (mode === "off") { + mode = "none"; + } else if (mode === "toggle") { + switch (player.coordinator.state.playMode.repeat) { + case 'all': mode = "one"; break; + case 'one': mode = "off"; break; + default: mode = "all"; + } + } + + return player.coordinator.repeat(mode).then((response) => { + return { status: 'success', repeat: mode }; + }); } function shuffle(player, values) { - return player.coordinator.shuffle(values[0] == "on" ? true : false); + let enable = values[0] === "on"; + if(values[0] == "toggle") enable = !player.coordinator.state.playMode.shuffle; + return player.coordinator.shuffle(enable).then((response) => { + return { status: 'success', shuffle: enable }; + }); } function crossfade(player, values) { - return player.coordinator.crossfade(values[0] == "on" ? true : false); + let enable = values[0] === "on"; + if(values[0] == "toggle") enable = !player.coordinator.state.playMode.crossfade; + return player.coordinator.crossfade(enable).then((response) => { + return { status: 'success', crossfade: enable }; + }); } module.exports = function (api) { api.registerAction('repeat', repeat); api.registerAction('shuffle', shuffle); api.registerAction('crossfade', crossfade); -} \ No newline at end of file +} diff --git a/lib/actions/playpause.js b/lib/actions/playpause.js index 5755ad07..655c0544 100644 --- a/lib/actions/playpause.js +++ b/lib/actions/playpause.js @@ -1,10 +1,13 @@ +'use strict'; function playpause(player) { - console.log(player.coordinator.state.playbackState) + let ret = { status: 'success', paused: false }; + if(player.coordinator.state.playbackState === 'PLAYING') { - return player.coordinator.pause(); + ret.paused = true; + return player.coordinator.pause().then((response) => { return ret; }); } - return player.coordinator.play(); + return player.coordinator.play().then((response) => { return ret; }); } function play(player) { diff --git a/lib/actions/preset.js b/lib/actions/preset.js index 22e158e6..93252212 100644 --- a/lib/actions/preset.js +++ b/lib/actions/preset.js @@ -1,12 +1,8 @@ 'use strict'; const fs = require('fs'); const util = require('util'); -const path = require('path'); const logger = require('sonos-discovery/lib/helpers/logger'); -const settings = require('../../settings'); -const presetsFilename = __dirname + '/../../presets.json'; -const presetsPath = settings.presetDir; -let presets = {}; +const presets = require('../presets-loader'); function presetsAction(player, values) { const value = decodeURIComponent(values[0]); @@ -25,76 +21,6 @@ function presetsAction(player, values) { } } -function readPresetsFromDir(presets, presetPath) { - let files; - try { - files = fs.readdirSync(presetPath); - } catch (e) { - logger.warn(`Could not find dir ${presetPath}, are you sure it exists?`); - logger.warn(e.message); - return; - } - - files.map((name) => { - let fullPath = path.join(presetPath, name); - return { - name, - fullPath, - stat: fs.statSync(fullPath) - }; - }).filter((file) => { - return !file.stat.isDirectory() && !file.name.startsWith('.') && file.name.endsWith('.json'); - }).forEach((file) => { - const presetName = file.name.replace(/\.json/i, ''); - try { - presets[presetName] = JSON.parse(fs.readFileSync(file.fullPath)); - } catch (err) { - logger.warn(`could not parse preset file ${file.name} ("${err.message}"), please validate it with a JSON parser.`); - } - }); - -} - -function readPresetsFromFile(presets, filename) { - try { - const presetStat = fs.statSync(filename); - if (!presetStat.isFile()) { - return; - } - - const filePresets = require(filename); - Object.keys(filePresets).forEach(presetName => { - presets[presetName] = filePresets[presetName]; - }); - - logger.warn('You are using a presets.json file! ' + - 'Consider migrating your presets into the presets/ ' + - 'folder instead, and enjoy auto-reloading of presets when you change them'); - } catch (err) { - logger.debug(`no presets.json file exists, skipping`); - } -} - -function initPresets() { - presets = {}; - readPresetsFromFile(presets, presetsFilename); - readPresetsFromDir(presets, presetsPath); - - logger.info('Presets loaded:', util.inspect(presets, { depth: null })); - -} - module.exports = function (api) { - initPresets(); - let watchTimeout; - try { - fs.watch(presetsPath, { persistent: false }, () => { - clearTimeout(watchTimeout); - watchTimeout = setTimeout(initPresets, 200); - }); - } catch (e) { - logger.warn(`Could not start watching dir ${presetsPath}, will not auto reload any presets. Make sure the dir exists`); - logger.warn(e.message); - } api.registerAction('preset', presetsAction); -} \ No newline at end of file +}; \ No newline at end of file diff --git a/lib/actions/queue.js b/lib/actions/queue.js index 3f48fad1..4f23e020 100644 --- a/lib/actions/queue.js +++ b/lib/actions/queue.js @@ -13,9 +13,19 @@ function simplify(items) { } function queue(player, values) { - const detailed = values[0] === 'detailed'; + const detailed = values[values.length - 1] === 'detailed'; + let limit; + let offset; - const promise = player.coordinator.getQueue(); + if (/\d+/.test(values[0])) { + limit = parseInt(values[0]); + } + + if (/\d+/.test(values[1])) { + offset = parseInt(values[1]); + } + + const promise = player.coordinator.getQueue(limit, offset); if (detailed) { return promise; diff --git a/lib/actions/reindex.js b/lib/actions/reindex.js index c5aaebdc..16437e48 100644 --- a/lib/actions/reindex.js +++ b/lib/actions/reindex.js @@ -1,3 +1,4 @@ +'use strict'; function reindex(player) { return player.system.refreshShareIndex(); } diff --git a/lib/actions/say.js b/lib/actions/say.js index d22eebb6..7af008a4 100644 --- a/lib/actions/say.js +++ b/lib/actions/say.js @@ -11,22 +11,30 @@ let port; let system; function say(player, values) { - const text = values[0]; + let text; + try { + text = decodeURIComponent(values[0]); + } catch (err) { + if (err instanceof URIError) { + err.message = `The encoded phrase ${values[0]} could not be URI decoded. Make sure your url encoded values (%xx) are within valid ranges. xx should be hexadecimal representations`; + } + return Promise.reject(err); + } let announceVolume; let language; if (/^\d+$/i.test(values[1])) { // first parameter is volume announceVolume = values[1]; - language = 'en-gb'; + // language = 'en-gb'; } else { - language = values[1] || 'en-gb'; + language = values[1]; announceVolume = values[2] || settings.announceVolume || 40; } return tryDownloadTTS(text, language) - .then((path) => { - return singlePlayerAnnouncement(player, `http://${system.localEndpoint}:${port}${path}`, announceVolume); + .then((result) => { + return singlePlayerAnnouncement(player, `http://${system.localEndpoint}:${port}${result.uri}`, announceVolume, result.duration); }); } diff --git a/lib/actions/sayall.js b/lib/actions/sayall.js index d2b9fea8..d9fbdc8e 100644 --- a/lib/actions/sayall.js +++ b/lib/actions/sayall.js @@ -7,23 +7,31 @@ let port; let system; function sayAll(player, values) { - const text = values[0]; + let text; + try { + text = decodeURIComponent(values[0]); + } catch (err) { + if (err instanceof URIError) { + err.message = `The encoded phrase ${values[0]} could not be URI decoded. Make sure your url encoded values (%xx) are within valid ranges. xx should be hexadecimal representations`; + } + return Promise.reject(err); + } let announceVolume; let language; if (/^\d+$/i.test(values[1])) { // first parameter is volume announceVolume = values[1]; - language = 'en-gb'; + // language = 'en-gb'; } else { - language = values[1] || 'en-gb'; + language = values[1]; announceVolume = values[2] || settings.announceVolume || 40; } return tryDownloadTTS(text, language) - .then(uri => { - return allPlayerAnnouncement(player.system, `http://${player.system.localEndpoint}:${port}${uri}`, announceVolume); + .then((result) => { + return allPlayerAnnouncement(player.system, `http://${player.system.localEndpoint}:${port}${result.uri}`, announceVolume, result.duration); }) } @@ -31,4 +39,4 @@ function sayAll(player, values) { module.exports = function (api) { port = api.getPort(); api.registerAction('sayall', sayAll); -}; \ No newline at end of file +}; diff --git a/lib/actions/saypreset.js b/lib/actions/saypreset.js new file mode 100644 index 00000000..cb408645 --- /dev/null +++ b/lib/actions/saypreset.js @@ -0,0 +1,40 @@ +'use strict'; +const tryDownloadTTS = require('../helpers/try-download-tts'); +const presetAnnouncement = require('../helpers/preset-announcement'); +const presets = require('../presets-loader'); + +let port; +let system; + +function sayPreset(player, values) { + let text; + const presetName = decodeURIComponent(values[0]); + + const preset = presets[presetName]; + + if (!preset) { + return Promise.reject(new Error(`No preset named ${presetName} could be found`)); + } + + try { + text = decodeURIComponent(values[1]); + } catch (err) { + if (err instanceof URIError) { + err.message = `The encoded phrase ${values[0]} could not be URI decoded. Make sure your url encoded values (%xx) are within valid ranges. xx should be hexadecimal representations`; + } + return Promise.reject(err); + } + + const language = values[2]; + + return tryDownloadTTS(text, language) + .then((result) => { + return presetAnnouncement(player.system, `http://${player.system.localEndpoint}:${port}${result.uri}`, preset, result.duration); + }) + +} + +module.exports = function (api) { + port = api.getPort(); + api.registerAction('saypreset', sayPreset); +}; diff --git a/lib/actions/seek.js b/lib/actions/seek.js index cce8e516..27e31288 100644 --- a/lib/actions/seek.js +++ b/lib/actions/seek.js @@ -1,3 +1,4 @@ +'use strict'; function timeSeek(player, values) { return player.coordinator.timeSeek(values[0]); } diff --git a/lib/actions/setavtransporturi.js b/lib/actions/setavtransporturi.js index 80275306..885b68dc 100644 --- a/lib/actions/setavtransporturi.js +++ b/lib/actions/setavtransporturi.js @@ -1,3 +1,4 @@ +'use strict'; function setAVTransportURI(player, values) { return player.setAVTransport(decodeURIComponent(values[0])); } diff --git a/lib/actions/siriusXM.js b/lib/actions/siriusXM.js index 8c77d23e..9e3dc168 100644 --- a/lib/actions/siriusXM.js +++ b/lib/actions/siriusXM.js @@ -3,13 +3,11 @@ const request = require('request-promise'); const Fuse = require('fuse.js'); const channels = require('../sirius-channels.json'); -var accountId = ''; - -function getSiriusXmMetadata(id, parent, title, auth) { +function getSiriusXmMetadata(id, parent, title) { return ` ${title}object.item.audioItem.audioBroadcast - SA_RINCON9479_${auth}`; + _`; } function getSiriusXmUri(id) { @@ -27,24 +25,6 @@ function adjustStation(name) { return name; } -function getAccountId(player) -{ - accountId = ''; - - return request({url: player.baseUrl + '/status/accounts',json: false}) - .then((res) => { - var actLoc = res.indexOf('Account Type="9479"'); - - if (actLoc != -1) { - var idLoc = res.indexOf('', actLoc)+4; - - accountId = res.substring(idLoc,res.indexOf('',idLoc)); - } - - return Promise.resolve(); - }); -} - function siriusXM(player, values) { var auth = ''; var results = []; @@ -86,24 +66,18 @@ function siriusXM(player, values) { return Promise.resolve("success"); } else { // Play the specified SiriusXM channel or station + var searchVal = values[0]; + var fuzzy = new Fuse(channels, { keys: ["channelNum", "title"] }); - return getAccountId(player) - .then(() => { - if (accountId != '') { - var searchVal = values[0]; - var fuzzy = new Fuse(channels, { keys: ["channelNum", "title"] }); + results = fuzzy.search(searchVal); + if (results.length > 0) { + const channel = results[0]; + const uri = getSiriusXmUri(channel.item.id); + const metadata = getSiriusXmMetadata(channel.item.id, channel.item.parentID, channel.item.fullTitle); - results = fuzzy.search(searchVal); - if (results.length > 0) { - const channel = results[0]; - const uri = getSiriusXmUri(channel.id); - const metadata = getSiriusXmMetadata(channel.id, channel.parentID, channel.fullTitle, accountId); - - return player.coordinator.setAVTransport(uri, metadata) - .then(() => player.coordinator.play()); - } - } - }); + return player.coordinator.setAVTransport(uri, metadata) + .then(() => player.coordinator.play()); + } } } diff --git a/lib/actions/spotify.js b/lib/actions/spotify.js index c57702d6..6b281489 100644 --- a/lib/actions/spotify.js +++ b/lib/actions/spotify.js @@ -28,9 +28,12 @@ function spotify(player, values) { return player.coordinator.addURIToQueue(uri, metadata); } else if (action == 'now') { var nextTrackNo = player.coordinator.state.trackNo + 1; - return player.coordinator.addURIToQueue(uri, metadata, true, nextTrackNo) - .then(() => player.coordinator.nextTrack()) - .then(() => player.coordinator.play()) + let promise = Promise.resolve(); + return promise.then(() => player.coordinator.setAVTransport(`x-rincon-queue:${player.coordinator.uuid}#0`)) + .then(() => player.coordinator.addURIToQueue(uri, metadata, true, nextTrackNo)) + .then((addToQueueStatus) => player.coordinator.trackSeek(addToQueueStatus.firsttracknumberenqueued)) + .then(() => player.coordinator.play()); + } else if (action == 'next') { var nextTrackNo = player.coordinator.state.trackNo + 1; return player.coordinator.addURIToQueue(uri, metadata, true, nextTrackNo); diff --git a/lib/actions/sub.js b/lib/actions/sub.js new file mode 100644 index 00000000..49ef927f --- /dev/null +++ b/lib/actions/sub.js @@ -0,0 +1,31 @@ +'use strict'; + +function sub(player, values) { + if (!player.hasSub) { + return Promise.reject(new Error('This zone doesn\'t have a SUB connected')); + } + + const action = values[0]; + const value = values[1]; + + switch (action) { + case 'on': + return player.subEnable(); + case 'off': + return player.subDisable(); + case 'gain': + return player.subGain(value); + case 'crossover': + return player.subCrossover(value); + case 'polarity': + return player.subPolarity(value); + } + + return Promise.resolve({ + message: 'Valid options are on, off, gain, crossover, polarity' + }); +} + +module.exports = function (api) { + api.registerAction('sub', sub); +} \ No newline at end of file diff --git a/lib/actions/tunein.js b/lib/actions/tunein.js new file mode 100644 index 00000000..add0fb57 --- /dev/null +++ b/lib/actions/tunein.js @@ -0,0 +1,35 @@ +'use strict'; + +function getTuneInMetadata(uri, serviceType) { + return ` + tuneinobject.item.audioItem.audioBroadcast + SA_RINCON${serviceType}_`; +} + +function tuneIn(player, values) { + const action = values[0]; + const tuneInUri = values[1]; + const encodedTuneInUri = encodeURIComponent(tuneInUri); + const sid = player.system.getServiceId('TuneIn'); + const metadata = getTuneInMetadata(encodedTuneInUri, player.system.getServiceType('TuneIn')); + const uri = `x-sonosapi-stream:s${encodedTuneInUri}?sid=${sid}&flags=8224&sn=0`; + + if (!tuneInUri) { + return Promise.reject('Expected TuneIn station id'); + } + + if (action == 'play') { + return player.coordinator.setAVTransport(uri, metadata) + .then(() => player.coordinator.play()); + } + if (action == 'set') { + return player.coordinator.setAVTransport(uri, metadata); + } + + return Promise.reject('TuneIn only handles the {play} & {set} action'); +} + +module.exports = function (api) { + api.registerAction('tunein', tuneIn); +} diff --git a/lib/actions/volume.js b/lib/actions/volume.js index 2c90e841..31fe8eb2 100644 --- a/lib/actions/volume.js +++ b/lib/actions/volume.js @@ -1,3 +1,4 @@ +'use strict'; function volume(player, values) { var volume = values[0]; return player.setVolume(volume); diff --git a/lib/helpers/all-player-announcement.js b/lib/helpers/all-player-announcement.js index 054ff2e7..a55a1ce2 100644 --- a/lib/helpers/all-player-announcement.js +++ b/lib/helpers/all-player-announcement.js @@ -1,14 +1,12 @@ 'use strict'; const logger = require('sonos-discovery/lib/helpers/logger'); const isRadioOrLineIn = require('../helpers/is-radio-or-line-in'); -let onOneBigGroup; -let globalListenerRegistered = false; function saveAll(system) { const backupPresets = system.zones.map((zone) => { - var coordinator = zone.coordinator; - var state = coordinator.state; - var preset = { + const coordinator = zone.coordinator; + const state = coordinator.state; + const preset = { players: [ { roomName: coordinator.roomName, volume: state.volume } ], @@ -34,20 +32,14 @@ function saveAll(system) { }); - return backupPresets; -} - -function topologyChanged() { - if (onOneBigGroup instanceof Function) { - onOneBigGroup(); - } + logger.trace('backup presets', backupPresets); + return backupPresets.sort((a,b) => { + return a.players.length < b.players.length; + }); } -function announceAll(system, uri, volume) { - if (!globalListenerRegistered) { - system.on('topology-change', topologyChanged); - globalListenerRegistered = true; - } +function announceAll(system, uri, volume, duration) { + let abortTimer; // Save all players var backupPresets = saveAll(system); @@ -80,58 +72,57 @@ function announceAll(system, uri, volume) { state: 'STOPPED' }; - let announceFinished; - let afterPlayingStateChange; - - const onTransportChange = (state) => { - logger.debug(coordinator.roomName, state.playbackState); - - if (state.playbackState === 'PLAYING') { - afterPlayingStateChange = announceFinished; - } - - if (state.playbackState !== "STOPPED") { - return; - } + const oneGroupPromise = new Promise((resolve) => { + const onTopologyChanged = (topology) => { + if (topology.length === 1) { + return resolve(); + } + // Not one group yet, continue listening + system.once('topology-change', onTopologyChanged); + }; - if (afterPlayingStateChange instanceof Function) { - logger.debug('announcement finished'); - afterPlayingStateChange(); - } - }; + system.once('topology-change', onTopologyChanged); + }); + const restoreTimeout = duration + 2000; return system.applyPreset(preset) .then(() => { if (system.zones.length === 1) return; - - return new Promise((resolve) => { - onOneBigGroup = resolve; - }) - }) - .then(() => { - return coordinator.play(); + return oneGroupPromise; }) .then(() => { - coordinator.on('transport-state', onTransportChange); + coordinator.play(); return new Promise((resolve) => { - announceFinished = resolve; + const transportChange = (state) => { + logger.debug(`Player changed to state ${state.playbackState}`); + if (state.playbackState === 'STOPPED') { + return resolve(); + } + + coordinator.once('transport-state', transportChange); + }; + setTimeout(() => { + coordinator.once('transport-state', transportChange); + }, duration / 2); + + logger.debug(`Setting restore timer for ${restoreTimeout} ms`); + abortTimer = setTimeout(resolve, restoreTimeout); }); }) .then(() => { - logger.debug('removing listener from', coordinator.roomName); - coordinator.removeListener('transport-state', onTransportChange); + clearTimeout(abortTimer); }) .then(() => { - logger.debug(backupPresets); return backupPresets.reduce((promise, preset) => { + logger.trace('Restoring preset', preset); return promise.then(() => system.applyPreset(preset)); }, Promise.resolve()); }) .catch((err) => { logger.error(err.stack); - coordinator.removeListener('transport-state', onTransportChange); + throw err; }); } -module.exports = announceAll; \ No newline at end of file +module.exports = announceAll; diff --git a/lib/helpers/file-duration.js b/lib/helpers/file-duration.js new file mode 100644 index 00000000..1055acbf --- /dev/null +++ b/lib/helpers/file-duration.js @@ -0,0 +1,10 @@ +const musicMeta = require('music-metadata'); + +function fileDuration(path) { + return musicMeta.parseFile(path, { duration: true }) + .then((info) => { + return Math.ceil(info.format.duration * 1000); + }) +} + +module.exports = fileDuration; diff --git a/lib/helpers/http-event-server.js b/lib/helpers/http-event-server.js new file mode 100644 index 00000000..56e5d176 --- /dev/null +++ b/lib/helpers/http-event-server.js @@ -0,0 +1,19 @@ +function HttpEventServer() { + let clients = []; + + const removeClient = client => clients = clients.filter(value => value !== client); + + this.addClient = res => clients.push(new HttpEventSource(res, removeClient)); + + this.sendEvent = event => clients.forEach(client => client.sendEvent(event)) +} + +function HttpEventSource(res, done) { + this.sendEvent = event => res.write('data: ' + event + '\n\n') + + res.on('close', () => done(this)) + + res.setHeader('Content-Type', 'text/event-stream'); +} + +module.exports = HttpEventServer; diff --git a/lib/helpers/preset-announcement.js b/lib/helpers/preset-announcement.js new file mode 100644 index 00000000..20fdc696 --- /dev/null +++ b/lib/helpers/preset-announcement.js @@ -0,0 +1,116 @@ +'use strict'; +const logger = require('sonos-discovery/lib/helpers/logger'); +const isRadioOrLineIn = require('../helpers/is-radio-or-line-in'); + +function saveAll(system) { + const backupPresets = system.zones.map((zone) => { + const coordinator = zone.coordinator; + const state = coordinator.state; + const preset = { + players: [ + { roomName: coordinator.roomName, volume: state.volume } + ], + state: state.playbackState, + uri: coordinator.avTransportUri, + metadata: coordinator.avTransportUriMetadata, + playMode: { + repeat: state.playMode.repeat + } + }; + + if (!isRadioOrLineIn(preset.uri)) { + preset.trackNo = state.trackNo; + preset.elapsedTime = state.elapsedTime; + } + + zone.members.forEach(function (player) { + if (coordinator.uuid != player.uuid) + preset.players.push({ roomName: player.roomName, volume: player.state.volume }); + }); + + return preset; + + }); + + logger.trace('backup presets', backupPresets); + return backupPresets.sort((a, b) => { + return a.players.length < b.players.length; + }); +} + +function announcePreset(system, uri, preset, duration) { + let abortTimer; + + // Save all players + var backupPresets = saveAll(system); + + const simplifiedPreset = { + uri, + players: preset.players, + playMode: preset.playMode, + pauseOthers: true, + state: 'STOPPED' + }; + + function hasReachedCorrectTopology(zones) { + return zones.some(group => + group.members.length === preset.players.length && + group.coordinator.roomName === preset.players[0].roomName); + } + + const oneGroupPromise = new Promise((resolve) => { + const onTopologyChanged = (topology) => { + if (hasReachedCorrectTopology(topology)) { + return resolve(); + } + // Not one group yet, continue listening + system.once('topology-change', onTopologyChanged); + }; + + system.once('topology-change', onTopologyChanged); + }); + + const restoreTimeout = duration + 2000; + const coordinator = system.getPlayer(preset.players[0].roomName); + return coordinator.pause() + .then(() => system.applyPreset(simplifiedPreset)) + .catch(() => system.applyPreset(simplifiedPreset)) + .then(() => { + if (hasReachedCorrectTopology(system.zones)) return; + return oneGroupPromise; + }) + .then(() => { + coordinator.play(); + return new Promise((resolve) => { + const transportChange = (state) => { + logger.debug(`Player changed to state ${state.playbackState}`); + if (state.playbackState === 'STOPPED') { + return resolve(); + } + + coordinator.once('transport-state', transportChange); + }; + setTimeout(() => { + coordinator.once('transport-state', transportChange); + }, duration / 2); + logger.debug(`Setting restore timer for ${restoreTimeout} ms`); + abortTimer = setTimeout(resolve, restoreTimeout); + }); + }) + .then(() => { + clearTimeout(abortTimer); + }) + .then(() => { + return backupPresets.reduce((promise, preset) => { + logger.trace('Restoring preset', preset); + return promise.then(() => system.applyPreset(preset)); + }, Promise.resolve()); + }) + .catch((err) => { + logger.error(err.stack); + throw err; + }); + +} + +module.exports = announcePreset; \ No newline at end of file diff --git a/lib/helpers/single-player-announcement.js b/lib/helpers/single-player-announcement.js index 262e058e..50117adf 100644 --- a/lib/helpers/single-player-announcement.js +++ b/lib/helpers/single-player-announcement.js @@ -3,7 +3,7 @@ const logger = require('sonos-discovery/lib/helpers/logger'); const isRadioOrLineIn = require('../helpers/is-radio-or-line-in'); const backupPresets = {}; -function singlePlayerAnnouncement(player, uri, volume) { +function singlePlayerAnnouncement(player, uri, volume, duration) { // Create backup preset to restore this player const state = player.state; const system = player.system; @@ -21,7 +21,7 @@ function singlePlayerAnnouncement(player, uri, volume) { // remember which group you were part of. const group = system.zones.find(zone => zone.coordinator.uuid === player.coordinator.uuid); if (group.members.length > 1) { - console.log('Think its coordinator, will find uri later'); + logger.debug('Think its coordinator, will find uri later'); groupToRejoin = group.id; backupPreset.group = group.id; } else { @@ -44,7 +44,7 @@ function singlePlayerAnnouncement(player, uri, volume) { backupPreset.uri = `x-rincon:${player.coordinator.uuid}`; } - logger.debug('backup preset was', backupPreset); + logger.debug('backup state was', backupPreset); // Use the preset action to play the tts file var ttsPreset = { @@ -57,26 +57,6 @@ function singlePlayerAnnouncement(player, uri, volume) { uri }; - let announceFinished; - let afterPlayingStateChange; - - const onTransportChange = (state) => { - logger.debug(`playback state switched to ${state.playbackState}`); - if (state.playbackState === 'PLAYING') { - logger.debug('I believe announcement starts here'); - afterPlayingStateChange = announceFinished; - } - - if (state.playbackState !== "STOPPED") { - return; - } - - if (afterPlayingStateChange instanceof Function) { - logger.debug('announcement finished because of STOPPED state identified'); - afterPlayingStateChange(); - } - }; - let abortTimer; if (!backupPresets[player.roomName]) { @@ -84,18 +64,23 @@ function singlePlayerAnnouncement(player, uri, volume) { } backupPresets[player.roomName].unshift(backupPreset); + logger.debug('backup presets array', backupPresets[player.roomName]); const prepareBackupPreset = () => { - const relevantBackupPreset = backupPresets[player.roomName].shift(); - - if (!relevantBackupPreset) { + if (backupPresets[player.roomName].length > 1) { + backupPresets[player.roomName].shift(); + logger.debug('more than 1 backup presets during prepare', backupPresets[player.roomName]); return Promise.resolve(); } - if (backupPresets[player.roomName].length > 0) { + if (backupPresets[player.roomName].length < 1) { return Promise.resolve(); } + const relevantBackupPreset = backupPresets[player.roomName][0]; + + logger.debug('exactly 1 preset left', relevantBackupPreset); + if (relevantBackupPreset.group) { const zone = system.zones.find(zone => zone.id === relevantBackupPreset.group); if (zone) { @@ -104,35 +89,48 @@ function singlePlayerAnnouncement(player, uri, volume) { } logger.debug('applying preset', relevantBackupPreset); - return system.applyPreset(relevantBackupPreset); + return system.applyPreset(relevantBackupPreset) + .then(() => { + backupPresets[player.roomName].shift(); + logger.debug('after backup preset applied', backupPresets[player.roomName]); + }); } + let timer; + const restoreTimeout = duration + 2000; return system.applyPreset(ttsPreset) .then(() => { - player.on('transport-state', onTransportChange); return new Promise((resolve) => { - announceFinished = resolve; - abortTimer = setTimeout(() => { - announceFinished = null; - logger.debug('Restoring backup preset because 30 seconds passed'); - resolve(); - }, 30000); + const transportChange = (state) => { + logger.debug(`Player changed to state ${state.playbackState}`); + if (state.playbackState === 'STOPPED') { + return resolve(); + } + + player.once('transport-state', transportChange); + }; + setTimeout(() => { + player.once('transport-state', transportChange); + }, duration / 2); + + logger.debug(`Setting restore timer for ${restoreTimeout} ms`); + timer = Date.now(); + abortTimer = setTimeout(resolve, restoreTimeout); }); }) .then(() => { + const elapsed = Date.now() - timer; + logger.debug(`${elapsed} elapsed with ${restoreTimeout - elapsed} to spare`); clearTimeout(abortTimer); - player.removeListener('transport-state', onTransportChange); }) .then(prepareBackupPreset) .catch((err) => { logger.error(err); - player.removeListener('transport-state', onTransportChange); return prepareBackupPreset() .then(() => { - // we still want to inform that stuff broke throw err; }); }); } -module.exports = singlePlayerAnnouncement; \ No newline at end of file +module.exports = singlePlayerAnnouncement; diff --git a/lib/helpers/try-download-tts.js b/lib/helpers/try-download-tts.js index 2f308d82..ec05034d 100644 --- a/lib/helpers/try-download-tts.js +++ b/lib/helpers/try-download-tts.js @@ -1,49 +1,26 @@ 'use strict'; -const crypto = require('crypto'); -const fs = require('fs'); -const http = require('http'); const path = require('path'); -const settings = require('../../settings'); +const requireDir = require('sonos-discovery/lib/helpers/require-dir'); +const providers = []; -function tryDownloadTTS(phrase, language) { - if (!settings.voicerss) { - console.error('You need to register an apikey at http://www.voicerss.org and add it to settings.json!'); - return Promise.resolve(`/missing_api_key.mp3`); - - } - // Use voicerss tts translation service to create a mp3 file - const ttsRequestUrl = `http://api.voicerss.org/?key=${settings.voicerss}&f=22khz_16bit_mono&hl=${language}&src=${phrase}`; - - // Construct a filesystem neutral filename - const filename = crypto.createHash('sha1').update(phrase).digest('hex') + '-' + language + '.mp3'; - const filepath = path.resolve(settings.webroot, 'tts', filename); +requireDir(path.join(__dirname, '../tts-providers'), (provider) => { + providers.push(provider); +}); - const expectedUri = `/tts/${filename}`; - try { - fs.accessSync(filepath, fs.R_OK); - return Promise.resolve(expectedUri); - } catch (err) { - console.log(`announce file for phrase "${phrase}" does not seem to exist, downloading`); - } +providers.push(require('../tts-providers/default/google')); - return new Promise((resolve, reject) => { - var file = fs.createWriteStream(filepath); - http.get(ttsRequestUrl, function (response) { - if (response.statusCode < 300 && response.statusCode >= 200) { - response.pipe(file); - file.on('finish', function () { - file.end(); - resolve(expectedUri); +function tryDownloadTTS(phrase, language) { + let result; + return providers.reduce((promise, provider) => { + return promise.then(() => { + if (result) return result; + return provider(phrase, language) + .then((_result) => { + result = _result; + return result; }); - } else { - reject(new Error(`Download failed with status ${response.statusCode}, ${response.message}`)); - - } - }).on('error', function (err) { - fs.unlink(dest); - reject(err); - }); - }); + }); + }, Promise.resolve()); } module.exports = tryDownloadTTS; \ No newline at end of file diff --git a/lib/helpers/try-load-json.js b/lib/helpers/try-load-json.js new file mode 100644 index 00000000..ff90afbf --- /dev/null +++ b/lib/helpers/try-load-json.js @@ -0,0 +1,20 @@ +const fs = require('fs'); +const JSON5 = require('json5'); +const logger = require('sonos-discovery/lib/helpers/logger'); + +function tryLoadJson(path) { + try { + const fileContent = fs.readFileSync(path); + const parsedContent = JSON5.parse(fileContent); + return parsedContent; + } catch (e) { + if (e.code === 'ENOENT') { + logger.info(`Could not find file ${path}`); + } else { + logger.warn(`Could not read file ${path}, ignoring.`, e); + } + } + return {}; +} + +module.exports = tryLoadJson; \ No newline at end of file diff --git a/lib/music_services/appleDef.js b/lib/music_services/appleDef.js index 247a605f..52df8891 100644 --- a/lib/music_services/appleDef.js +++ b/lib/music_services/appleDef.js @@ -1,14 +1,15 @@ +'use strict'; const appleDef = { country: '&country=', search: { album: 'https://itunes.apple.com/search?media=music&limit=1&entity=album&attribute=albumTerm&term=', song: 'https://itunes.apple.com/search?media=music&limit=50&entity=song&term=', - station: 'https://sticky-summer-lb.inkstone-clients.net/api/v1/searchMusic?limit=1&media=appleMusic&entity=station&term=' + station: 'https://itunes.apple.com/search?media=music&limit=50&entity=musicArtist&term=' }, metastart: { album: '0004206calbum%3a', song: '00032020song%3a', - station: '000c206cradio%3a' + station: '000c206cradio%3ara.' }, parent: { album: '00020000album:', @@ -26,9 +27,19 @@ const appleDef = { tracks: loadTracks, empty: isEmpty, metadata: getMetadata, - urimeta: getURIandMetadata + urimeta: getURIandMetadata, + headers: getTokenHeaders, + authenticate: authenticateService } +function getTokenHeaders() { + return null; +}; + +function authenticateService() { + return Promise.resolve(); +} + function getURI(type, id) { if (type == 'album') { return `x-rincon-cpcontainer:0004206calbum%3a${id}`; @@ -37,7 +48,7 @@ function getURI(type, id) { return `x-sonos-http:song%3a${id}.mp4?sid=${sid}&flags=8224&sn=${accountSN}`; } else if (type == 'station') { - return `x-sonosapi-radio:radio%3a${id}?sid=${sid}&flags=8300&sn=${accountSN}`; + return `x-sonosapi-radio:radio%3ara.${id}?sid=${sid}&flags=8300&sn=${accountSN}`; } } @@ -84,7 +95,9 @@ function getMetadata(type, id, name, title) { const parentUri = appleDef.parent[type] + name; const objectType = appleDef.object[type]; - if (type != 'station') { + if (type == 'station') { + title = title + ' Radio'; + } else { title = ''; } @@ -105,8 +118,17 @@ function getURIandMetadata(type, resList) metadata: '' }; - Id = (type=='album')?resList.results[0].collectionId:resList.results[0].id; - Title = (type=='album')?resList.results[0].collectionName:resList.results[0].name; + if (type=='album') { + Id = resList.results[0].collectionId; + Title = resList.results[0].collectionName; + } else + if (type=='station') { + Id = resList.results[0].artistId; + Title = resList.results[0].artistName; + } else { + Id = resList.results[0].id; + Title = resList.results[0].name; + } Name = Title.toLowerCase().replace(' radio','').replace('radio ','').replace("'","'"); MetadataID = appleDef.metastart[type] + encodeURIComponent(Id); diff --git a/lib/music_services/deezerDef.js b/lib/music_services/deezerDef.js index ca4637a6..db1d79e0 100644 --- a/lib/music_services/deezerDef.js +++ b/lib/music_services/deezerDef.js @@ -1,3 +1,4 @@ +'use strict'; const deezerDef = { country: '', search: { @@ -27,8 +28,18 @@ const deezerDef = { tracks: loadTracks, empty: isEmpty, metadata: getMetadata, - urimeta: getURIandMetadata -} + urimeta: getURIandMetadata, + headers: getTokenHeaders, + authenticate: authenticateService +} + +function getTokenHeaders() { + return null; +} + +function authenticateService() { + return Promise.resolve(); +} function getURI(type, id) { if (type == 'album') { diff --git a/lib/music_services/libraryDef.js b/lib/music_services/libraryDef.js index a1351c8e..50ee2b84 100644 --- a/lib/music_services/libraryDef.js +++ b/lib/music_services/libraryDef.js @@ -4,6 +4,7 @@ const fs = require('fs'); const path = require('path'); const settings = require('../../settings'); const libraryPath = path.join(settings.cacheDir, 'library.json'); +const logger = require('sonos-discovery/lib/helpers/logger'); var randomQueueLimit = (settings.library && settings.library.randomQueueLimit !== undefined)?settings.library.randomQueueLimit:50; @@ -48,9 +49,18 @@ const libraryDef = { searchlib: searchLibrary, empty: isEmpty, metadata: getMetadata, - urimeta: getURIandMetadata + urimeta: getURIandMetadata, + headers: getTokenHeaders, + authenticate: authenticateService } +function getTokenHeaders() { + return null; +} + +function authenticateService() { + return Promise.resolve(); +} function setService(player, p_accountId, p_accountSN, p_country) { } @@ -89,7 +99,7 @@ function loadTracks(type, tracksJson) { }; if (tracksJson.length > 0) { - var albumName = tracksJson[0].albumName; + var albumName = tracksJson[0].item.albumName; // Filtered list of tracks to play tracks.queueTracks = tracksJson.reduce(function(tracksArray, track) { @@ -99,18 +109,18 @@ function loadTracks(type, tracksJson) { if (type == 'song') { for (var j = 0; (j < tracksArray.length) && !skip; j++) { // Skip duplicate songs - skip = (track.trackName == tracksArray[j].trackName); + skip = (track.item.trackName == tracksArray[j].trackName); } } else { - skip = (track.albumName != albumName); + skip = (track.item.albumName != albumName); } if (!skip) { tracksArray.push({ - trackName: track.trackName, - artistName: track.artistName, - albumTrackNumber:track.albumTrackNumber, - uri: track.uri, - metadata: track.metadata + trackName: track.item.trackName, + artistName: track.item.artistName, + albumTrackNumber:track.item.albumTrackNumber, + uri: track.item.uri, + metadata: track.item.metadata }); tracks.count++; } @@ -138,7 +148,7 @@ function libIsEmpty() { function loadFuse(items, fuzzyKeys) { return new Promise((resolve) => { - return resolve(new Fuse(items, { keys: fuzzyKeys, threshold: 0.2, distance: 5, maxPatternLength: 100 })); + return resolve(new Fuse(items, { keys: fuzzyKeys, threshold: 0.2, maxPatternLength: 100, ignoreLocation: true })); }); } @@ -151,7 +161,7 @@ function loadLibrary(player) { if (isLoading) { return Promise.resolve('Loading'); } - console.log("Loading Library"); + logger.info('Loading Library'); isLoading = true; let library = { @@ -186,7 +196,7 @@ function loadLibrary(player) { result.numberReturned += chunk.numberReturned; result.totalMatches = chunk.totalMatches; - console.log("Track Count " + result.numberReturned); + logger.info(`Tracks returned: ${result.numberReturned}, Total matches: ${result.totalMatches}`); if (isFinished(chunk)) { return new Promise((resolve, reject) => { @@ -208,7 +218,10 @@ function loadLibrary(player) { } return Promise.resolve(result) - .then(getChunk); + .then(getChunk) + .catch((err) => { + logger.error('Error when recursively trying to load library using browse()', err); + }); } function loadLibrarySearch(player, load) { diff --git a/lib/music_services/spotifyDef.js b/lib/music_services/spotifyDef.js index bec4b97c..85ba833a 100644 --- a/lib/music_services/spotifyDef.js +++ b/lib/music_services/spotifyDef.js @@ -1,43 +1,132 @@ +'use strict'; + +var request = require('request-promise'); +const settings = require('../../settings'); + +var clientId = ""; +var clientSecret = ""; + +if (settings.spotify) { + clientId = settings.spotify.clientId; + clientSecret = settings.spotify.clientSecret; +} + +var clientToken = null; + const spotifyDef = { country: '&market=', search: { album: 'https://api.spotify.com/v1/search?type=album&limit=1&q=album:', song: 'https://api.spotify.com/v1/search?type=track&limit=50&q=', - station: 'https://api.spotify.com/v1/search?type=artist&limit=1&q=' + station: 'https://api.spotify.com/v1/search?type=artist&limit=1&q=', + playlist: 'https://api.spotify.com/v1/search?type=playlist&q=' }, metastart: { album: '0004206cspotify%3aalbum%3a', song: '00032020spotify%3atrack%3a', - station: '000c206cspotify:artistRadio%3a' + station: '000c206cspotify:artistRadio%3a', + playlist: '0004206cspotify%3aplaylist%3a' }, parent: { album: '00020000album:', song: '00020000track:', - station: '00052064spotify%3aartist%3a' + station: '00052064spotify%3aartist%3a', + playlist:'00020000playlist:', }, object: { album: 'container.album.musicAlbum', song: 'item.audioItem.musicTrack', - station: 'item.audioItem.audioBroadcast.#artistRadio' + station: 'item.audioItem.audioBroadcast.#artistRadio', + playlist:'container.playlistContainer', }, - + service: setService, term: getSearchTerm, tracks: loadTracks, empty: isEmpty, metadata: getMetadata, - urimeta: getURIandMetadata -} + urimeta: getURIandMetadata, + headers: getTokenHeaders, + authenticate: authenticateService, +} + +var toBase64 = (string) => Buffer.from(string).toString('base64'); + +const SPOTIFY_TOKEN_URL = 'https://accounts.spotify.com/api/token'; + +const mapResponse = (response) => ({ + accessToken: response.access_token, + tokenType: response.token_type, + expiresIn: response.expires_in, +}); + +const getHeaders = () => { + console.log('spotify', clientId, clientSecret) + if (!clientId || !clientSecret) { + throw new Error('You are missing spotify clientId and secret in settings.json! Please read the README for instructions on how to generate and add them'); + } + const authString = `${clientId}:${clientSecret}`; + return { + Authorization: `Basic ${toBase64(authString)}`, + 'Content-Type': 'application/x-www-form-urlencoded', + }; +}; + +const getOptions = (url) => { + return { + url, + headers: getHeaders(), + json: true, + method: 'POST', + form: { + grant_type: 'client_credentials', + }, + }; +}; + +const auth = () => { + const options = getOptions(SPOTIFY_TOKEN_URL); + return new Promise((resolve, reject) => { + request(options).then((response) => { + const responseMapped = mapResponse(response); + resolve(responseMapped); + }).catch((err) => { + reject(new Error(`Unable to authenticate Spotify with client id: ${clientId}`)); + }) + }); +}; + +function getTokenHeaders() { + if (clientToken == null) { + return null; + } + return { + Authorization: `Bearer ${clientToken}` + }; +} + +function authenticateService() { + return new Promise((resolve, reject) => { + auth().then((response) => { + const accessToken = response.accessToken; + clientToken = accessToken; + resolve(); + }).catch(reject); + }); +} function getURI(type, id) { if (type == 'album') { return `x-rincon-cpcontainer:0004206c${id}`; - } else + } else if (type == 'song') { return `x-sonos-spotify:spotify%3atrack%3a${id}?sid=${sid}&flags=8224&sn=${accountSN}`; } else if (type == 'station') { return `x-sonosapi-radio:spotify%3aartistRadio%3a${id}?sid=${sid}&flags=8300&sn=${accountSN}`; + } else + if (type == 'playlist') { + return `x-rincon-cpcontainer:0006206c${id}`; } } @@ -57,7 +146,7 @@ function setService(player, p_accountId, p_accountSN, p_country) sid = player.system.getServiceId('Spotify'); serviceType = player.system.getServiceType('Spotify'); accountId = p_accountId; - accountSN = p_accountSN; + accountSN = 14; // GACALD: Hack to fix Spotify p_accountSN; country = p_country; } @@ -74,15 +163,15 @@ function getSearchTerm(type, term, artist, album, track) { newTerm += 'track:' + track; } newTerm = encodeURIComponent(newTerm); - + return newTerm; } - + function getMetadata(type, id, name, title) { const token = getServiceToken(); const parentUri = spotifyDef.parent[type] + name; const objectType = spotifyDef.object[type]; - + if (type != 'station') { title = ''; } @@ -105,22 +194,25 @@ function getURIandMetadata(type, resList) }; var items = []; - + if (type == 'album') { items = resList.albums.items; } else if (type == 'station') { items = resList.artists.items; + } else + if (type == 'playlist') { + items = resList.playlists.items; } - + Id = items[0].id; Title = items[0].name + ((type=='station')?' Radio':''); Name = Title.toLowerCase().replace(' radio','').replace('radio ',''); MetadataID = spotifyDef.metastart[type] + encodeURIComponent(Id); - - UaM.metadata = getMetadata(type, MetadataID, (type=='album')?Title.toLowerCase():Id, Title); + + UaM.metadata = getMetadata(type, MetadataID, (type=='album' || type=='playlist')?Title.toLowerCase() : Id, Title); UaM.uri = getURI(type, encodeURIComponent((type=='station')?items[0].id:items[0].uri)); - + return UaM; } @@ -130,18 +222,18 @@ function loadTracks(type, tracksJson) isArtist : false, queueTracks : [] }; - + if (tracksJson.tracks.items.length > 0) { // Filtered list of tracks to play tracks.queueTracks = tracksJson.tracks.items.reduce(function(tracksArray, track) { - if (track.available_markets.indexOf(country) != -1) { + if (track.available_markets == null || track.available_markets.indexOf(country) != -1) { var skip = false; - + for (var j=0; (j < tracksArray.length) && !skip ; j++) { // Skip duplicate songs skip = (track.name == tracksArray[j].trackName); } - + if (!skip) { var metadataID = spotifyDef.metastart['song'] + encodeURIComponent(track.id); var metadata = getMetadata('song', metadataID, track.id, track.name); @@ -150,30 +242,33 @@ function loadTracks(type, tracksJson) tracksArray.push({trackName:track.name, artistName:(track.artists.length>0)?track.artists[0].name:'', uri:uri, metadata:metadata}); tracks.count++; } - } + } return tracksArray; }, []); } - + return tracks; } - + function isEmpty(type, resList) { var count = 0; if (type == 'album') { count = resList.albums.items.length; - } else + } else if (type == 'song') { count = resList.tracks.items.length; } else if (type == 'station') { count = resList.artists.items.length; + } else + if (type == 'playlist') { + count = resList.playlists.items.length; } - + return (count == 0); } - + module.exports = spotifyDef; - + diff --git a/lib/presets-loader.js b/lib/presets-loader.js new file mode 100644 index 00000000..ef8312d4 --- /dev/null +++ b/lib/presets-loader.js @@ -0,0 +1,88 @@ +'use strict'; +const fs = require('fs'); +const util = require('util'); +const path = require('path'); +const logger = require('sonos-discovery/lib/helpers/logger'); +const tryLoadJson = require('./helpers/try-load-json'); +const settings = require('../settings'); + +const PRESETS_PATH = settings.presetDir; +const PRESETS_FILENAME = `${__dirname}/../presets.json`; +const presets = {}; + +function readPresetsFromDir(presets, presetPath) { + let files; + try { + files = fs.readdirSync(presetPath); + } catch (e) { + logger.warn(`Could not find dir ${presetPath}, are you sure it exists?`); + logger.warn(e.message); + return; + } + + files.map((name) => { + let fullPath = path.join(presetPath, name); + return { + name, + fullPath, + stat: fs.statSync(fullPath) + }; + }).filter((file) => { + return !file.stat.isDirectory() && !file.name.startsWith('.') && file.name.endsWith('.json'); + }).forEach((file) => { + const presetName = file.name.replace(/\.json/i, ''); + const preset = tryLoadJson(file.fullPath); + if (Object.keys(preset).length === 0) { + logger.warn(`could not parse preset file ${file.name}, please make sure syntax conforms with JSON5.`); + return; + } + + presets[presetName] = preset; + }); + +} + +function readPresetsFromFile(presets, filename) { + try { + const presetStat = fs.statSync(filename); + if (!presetStat.isFile()) { + return; + } + + const filePresets = require(filename); + Object.keys(filePresets).forEach(presetName => { + presets[presetName] = filePresets[presetName]; + }); + + logger.warn('You are using a presets.json file! ' + + 'Consider migrating your presets into the presets/ ' + + 'folder instead, and enjoy auto-reloading of presets when you change them'); + } catch (err) { + logger.debug(`no presets.json file exists, skipping`); + } +} + +function initPresets() { + Object.keys(presets).forEach(presetName => { + delete presets[presetName]; + }); + readPresetsFromFile(presets, PRESETS_FILENAME); + readPresetsFromDir(presets, PRESETS_PATH); + + logger.info('Presets loaded:', util.inspect(presets, { depth: null })); + +} + +initPresets(); +let watchTimeout; +try { + fs.watch(PRESETS_PATH, { persistent: false }, () => { + clearTimeout(watchTimeout); + watchTimeout = setTimeout(initPresets, 200); + }); +} catch (e) { + logger.warn(`Could not start watching dir ${PRESETS_PATH}, will not auto reload any presets. Make sure the dir exists`); + logger.warn(e.message); +} + +module.exports = presets; diff --git a/lib/sirius-channels.json b/lib/sirius-channels.json index e314d685..27f30ec0 100644 --- a/lib/sirius-channels.json +++ b/lib/sirius-channels.json @@ -1,28 +1,77 @@ [ {"fullTitle":"10 - Pop2K", "channelNum":"10", "title":"Pop2K", "id":"8208", "parentID":"00070044g%3apop"}, + {"fullTitle":"100 - Howard 100", "channelNum":"100", "title":"Howard 100", "id":"howardstern100", "parentID":"00070044g%3ahowardstern"}, + {"fullTitle":"101 - Howard 101", "channelNum":"101", "title":"Howard 101", "id":"howardstern101", "parentID":"00070044g%3ahowardstern"}, + {"fullTitle":"102 - Radio Andy", "channelNum":"102", "title":"Radio Andy", "id":"9409", "parentID":"00070044g%3aentertainment"}, + {"fullTitle":"105 - EW Radio", "channelNum":"105", "title":"EW Radio", "id":"9351", "parentID":"00070044g%3aentertainment"}, + {"fullTitle":"106 - SiriusXM 106", "channelNum":"106", "title":"SiriusXM 106", "id":"siriusoutq", "parentID":"00070044g%3aentertainment"}, + {"fullTitle":"108 - Today Show Radio", "channelNum":"108", "title":"Today Show Radio", "id":"9390", "parentID":"00070044g%3aentertainment"}, + {"fullTitle":"109 - SiriusXM Stars", "channelNum":"109", "title":"SiriusXM Stars", "id":"siriusstars", "parentID":"00070044g%3aentertainment"}, + {"fullTitle":"11 - KIIS-Los Angeles", "channelNum":"11", "title":"KIIS-Los Angeles", "id":"8241", "parentID":"00070044g%3amore"}, + {"fullTitle":"110 - Doctor Radio", "channelNum":"110", "title":"Doctor Radio", "id":"doctorradio", "parentID":"00070044g%3aentertainment"}, + {"fullTitle":"111 - Business Radio", "channelNum":"111", "title":"Business Radio", "id":"9359", "parentID":"00070044g%3aentertainment"}, + {"fullTitle":"112 - CNBC", "channelNum":"112", "title":"CNBC", "id":"cnbc", "parentID":"00070044g%3apublicradio"}, + {"fullTitle":"113 - FOX Business", "channelNum":"113", "title":"FOX Business", "id":"9369", "parentID":"00070044g%3apublicradio"}, + {"fullTitle":"114 - FOX News Channel", "channelNum":"114", "title":"FOX News Channel", "id":"foxnewschannel", "parentID":"00070044g%3apublicradio"}, + {"fullTitle":"115 - FOX News Headlines 24/7", "channelNum":"115", "title":"FOX News Headlines 24/7", "id":"9410", "parentID":"00070044g%3apublicradio"}, + {"fullTitle":"116 - CNN", "channelNum":"116", "title":"CNN", "id":"cnn", "parentID":"00070044g%3apublicradio"}, + {"fullTitle":"117 - HLN", "channelNum":"117", "title":"HLN", "id":"cnnheadlinenews", "parentID":"00070044g%3apublicradio"}, + {"fullTitle":"118 - MSNBC", "channelNum":"118", "title":"MSNBC", "id":"8367", "parentID":"00070044g%3apublicradio"}, + {"fullTitle":"119 - Bloomberg Radio", "channelNum":"119", "title":"Bloomberg Radio", "id":"bloombergradio", "parentID":"00070044g%3apublicradio"}, + {"fullTitle":"12 - Z100/NY", "channelNum":"12", "title":"Z100/NY", "id":"8242", "parentID":"00070044g%3amore"}, + {"fullTitle":"120 - BBC World Service", "channelNum":"120", "title":"BBC World Service", "id":"bbcworld", "parentID":"00070044g%3apublicradio"}, + {"fullTitle":"121 - SiriusXM Insight", "channelNum":"121", "title":"SiriusXM Insight", "id":"8183", "parentID":"00070044g%3apublicradio"}, + {"fullTitle":"122 - NPR Now", "channelNum":"122", "title":"NPR Now", "id":"nprnow", "parentID":"00070044g%3apublicradio"}, + {"fullTitle":"123 - PRX Public Radio", "channelNum":"123", "title":"PRX Public Radio", "id":"8239", "parentID":"00070044g%3apublicradio"}, + {"fullTitle":"124 - POTUS Politics", "channelNum":"124", "title":"POTUS Politics", "id":"indietalk", "parentID":"00070044g%3apolitical"}, + {"fullTitle":"125 - SiriusXM Patriot", "channelNum":"125", "title":"SiriusXM Patriot", "id":"siriuspatriot", "parentID":"00070044g%3apolitical"}, + {"fullTitle":"126 - SiriusXM Urban View", "channelNum":"126", "title":"SiriusXM Urban View", "id":"8238", "parentID":"00070044g%3apolitical"}, + {"fullTitle":"127 - SiriusXM Progress", "channelNum":"127", "title":"SiriusXM Progress", "id":"siriusleft", "parentID":"00070044g%3apolitical"}, + {"fullTitle":"128 - Joel Osteen Radio", "channelNum":"128", "title":"Joel Osteen Radio", "id":"9392", "parentID":"00070044g%3aentertainment"}, + {"fullTitle":"129 - The Catholic Channel", "channelNum":"129", "title":"The Catholic Channel", "id":"thecatholicchannel", "parentID":"00070044g%3areligion"}, {"fullTitle":"13 - Velvet", "channelNum":"13", "title":"Velvet", "id":"9361", "parentID":"00070044g%3apop"}, + {"fullTitle":"130 - EWTN Radio", "channelNum":"130", "title":"EWTN Radio", "id":"ewtnglobal", "parentID":"00070044g%3areligion"}, + {"fullTitle":"131 - Family Talk", "channelNum":"131", "title":"Family Talk", "id":"8307", "parentID":"00070044g%3areligion"}, {"fullTitle":"14 - The Coffee House", "channelNum":"14", "title":"The Coffee House", "id":"coffeehouse", "parentID":"00070044g%3apop"}, + {"fullTitle":"141 - HUR Voices", "channelNum":"141", "title":"HUR Voices", "id":"9129", "parentID":"00070044g%3amore"}, + {"fullTitle":"142 - HBCU", "channelNum":"142", "title":"HBCU", "id":"9130", "parentID":"00070044g%3amore"}, + {"fullTitle":"143 - BYUradio", "channelNum":"143", "title":"BYUradio", "id":"9131", "parentID":"00070044g%3amore"}, + {"fullTitle":"144 - Korea Today", "channelNum":"144", "title":"Korea Today", "id":"9132", "parentID":"00070044g%3amore"}, + {"fullTitle":"146 - Road Dog Trucking", "channelNum":"146", "title":"Road Dog Trucking", "id":"roaddogtrucking", "parentID":"00070044g%3aentertainment"}, + {"fullTitle":"147 - RURAL Radio", "channelNum":"147", "title":"RURAL Radio", "id":"9367", "parentID":"00070044g%3apublicradio"}, + {"fullTitle":"148 - RadioClassics", "channelNum":"148", "title":"RadioClassics", "id":"radioclassics", "parentID":"00070044g%3aentertainment"}, {"fullTitle":"15 - The Pulse", "channelNum":"15", "title":"The Pulse", "id":"thepulse", "parentID":"00070044g%3apop"}, + {"fullTitle":"152 - En Vivo", "channelNum":"152", "title":"En Vivo", "id":"9135", "parentID":"00070044g%3amore"}, + {"fullTitle":"153 - Cristina Radio", "channelNum":"153", "title":"Cristina Radio", "id":"9134", "parentID":"00070044g%3amore"}, + {"fullTitle":"154 - American Latino Radio", "channelNum":"154", "title":"American Latino Radio", "id":"9133", "parentID":"00070044g%3amore"}, + {"fullTitle":"155 - CNN en Español", "channelNum":"155", "title":"CNN en Español", "id":"cnnespanol", "parentID":"00070044g%3apublicradio"}, + {"fullTitle":"157 - ESPN Deportes", "channelNum":"157", "title":"ESPN Deportes", "id":"espndeportes", "parentID":"00070044g%3asportstalk"}, {"fullTitle":"158 - Caliente", "channelNum":"158", "title":"Caliente", "id":"rumbon", "parentID":"00070044g%3apop"}, {"fullTitle":"16 - The Blend", "channelNum":"16", "title":"The Blend", "id":"starlite", "parentID":"00070044g%3apop"}, + {"fullTitle":"162 - CBC Radio 3", "channelNum":"162", "title":"CBC Radio 3", "id":"cbcradio3", "parentID":"00070044g%3acanadian"}, + {"fullTitle":"163 - Ici Musique Chansons", "channelNum":"163", "title":"Ici Musique Chansons", "id":"8245", "parentID":"00070044g%3acanadian"}, + {"fullTitle":"165 - Multicultural Radio", "channelNum":"165", "title":"Multicultural Radio", "id":"9358", "parentID":"00070044g%3acanadian"}, + {"fullTitle":"166 - Ici FrancoCountry", "channelNum":"166", "title":"Ici FrancoCountry", "id":"rockvelours", "parentID":"00070044g%3acanadian"}, + {"fullTitle":"167 - Canada Talks", "channelNum":"167", "title":"Canada Talks", "id":"9172", "parentID":"00070044g%3acanadian"}, + {"fullTitle":"168 - Canada Laughs", "channelNum":"168", "title":"Canada Laughs", "id":"8259", "parentID":"00070044g%3acomedy"}, + {"fullTitle":"169 - CBC Radio One", "channelNum":"169", "title":"CBC Radio One", "id":"cbcradioone", "parentID":"00070044g%3apublicradio"}, {"fullTitle":"17 - SiriusXM Love", "channelNum":"17", "title":"SiriusXM Love", "id":"siriuslove", "parentID":"00070044g%3apop"}, + {"fullTitle":"170 - Ici Première", "channelNum":"170", "title":"Ici Première", "id":"premiereplus", "parentID":"00070044g%3acanadian"}, + {"fullTitle":"171 - CBC Country", "channelNum":"171", "title":"CBC Country", "id":"bandeapart", "parentID":"00070044g%3acanadian"}, + {"fullTitle":"172 - Canada 360 by AMI", "channelNum":"172", "title":"Canada 360 by AMI", "id":"8248", "parentID":"00070044g%3acanadian"}, + {"fullTitle":"173 - The Verge", "channelNum":"173", "title":"The Verge", "id":"8244", "parentID":"00070044g%3acanadian"}, + {"fullTitle":"174 - Influence Franco", "channelNum":"174", "title":"Influence Franco", "id":"8246", "parentID":"00070044g%3acanadian"}, {"fullTitle":"18 - SXM Limited Edition", "channelNum":"18", "title":"SXM Limited Edition", "id":"9138", "parentID":"00070044g%3apop"}, - {"fullTitle":"2 - SiriusXM Hits 1", "channelNum":"2", "title":"SiriusXM Hits 1", "id":"siriushits1", "parentID":"00070044g%3apop"}, - {"fullTitle":"3 - Venus", "channelNum":"3", "title":"Venus", "id":"9389", "parentID":"00070044g%3apop"}, - {"fullTitle":"300 - Poptropolis", "channelNum":"300", "title":"Poptropolis", "id":"9412", "parentID":"00070044g%3apop"}, - {"fullTitle":"301 - Road Trip Radio", "channelNum":"301", "title":"Road Trip Radio", "id":"9415", "parentID":"00070044g%3apop"}, - {"fullTitle":"302 - The Covers Channel", "channelNum":"302", "title":"The Covers Channel", "id":"9416", "parentID":"00070044g%3apop"}, - {"fullTitle":"4 - Pitbulls Globalization", "channelNum":"4", "title":"Pitbulls Globalization", "id":"9406", "parentID":"00070044g%3apop"}, - {"fullTitle":"5 - 50s on 5", "channelNum":"5", "title":"50s on 5", "id":"siriusgold", "parentID":"00070044g%3apop"}, - {"fullTitle":"6 - 60s on 6", "channelNum":"6", "title":"60s on 6", "id":"60svibrations", "parentID":"00070044g%3apop"}, - {"fullTitle":"7 - 70s on 7", "channelNum":"7", "title":"70s on 7", "id":"totally70s", "parentID":"00070044g%3apop"}, - {"fullTitle":"700 - Neil Diamond Radio", "channelNum":"700", "title":"Neil Diamond Radio", "id":"8372", "parentID":"00070044g%3apop"}, - {"fullTitle":"703 - Elevations", "channelNum":"703", "title":"Elevations", "id":"9362", "parentID":"00070044g%3apop"}, - {"fullTitle":"8 - 80s on 8", "channelNum":"8", "title":"80s on 8", "id":"big80s", "parentID":"00070044g%3apop"}, - {"fullTitle":"9 - 90s on 9", "channelNum":"9", "title":"90s on 9", "id":"8206", "parentID":"00070044g%3apop"}, {"fullTitle":"19 - Elvis Radio", "channelNum":"19", "title":"Elvis Radio", "id":"elvisradio", "parentID":"00070044g%3arock"}, + {"fullTitle":"2 - SiriusXM Hits 1", "channelNum":"2", "title":"SiriusXM Hits 1", "id":"siriushits1", "parentID":"00070044g%3apop"}, {"fullTitle":"20 - E Street Radio", "channelNum":"20", "title":"E Street Radio", "id":"estreetradio", "parentID":"00070044g%3arock"}, + {"fullTitle":"205 - NBC Sports Radio", "channelNum":"205", "title":"NBC Sports Radio", "id":"9452", "parentID":"00070044g%3asportstalk"}, + {"fullTitle":"206 - Opie Radio", "channelNum":"206", "title":"Opie Radio", "id":"8184", "parentID":"00070044g%3aentertainment"}, + {"fullTitle":"207 - SiriusXM NBA Radio", "channelNum":"207", "title":"SiriusXM NBA Radio", "id":"9385", "parentID":"00070044g%3asportstalk"}, + {"fullTitle":"208 - SiriusXM PGA TOUR Radio", "channelNum":"208", "title":"SiriusXM PGA TOUR Radio", "id":"8186", "parentID":"00070044g%3asportstalk"}, + {"fullTitle":"209 - MLB Network Radio", "channelNum":"209", "title":"MLB Network Radio", "id":"8333", "parentID":"00070044g%3asportstalk"}, {"fullTitle":"21 - Underground Garage", "channelNum":"21", "title":"Underground Garage", "id":"undergroundgarage", "parentID":"00070044g%3arock"}, + {"fullTitle":"210 - SXM Fantasy Sports Radio", "channelNum":"210", "title":"SXM Fantasy Sports Radio", "id":"8368", "parentID":"00070044g%3asportstalk"}, {"fullTitle":"22 - Pearl Jam Radio", "channelNum":"22", "title":"Pearl Jam Radio", "id":"8370", "parentID":"00070044g%3arock"}, {"fullTitle":"23 - Grateful Dead", "channelNum":"23", "title":"Grateful Dead", "id":"gratefuldead", "parentID":"00070044g%3arock"}, {"fullTitle":"24 - Radio Margaritaville", "channelNum":"24", "title":"Radio Margaritaville", "id":"radiomargaritaville", "parentID":"00070044g%3arock"}, @@ -31,35 +80,48 @@ {"fullTitle":"27 - Deep Tracks", "channelNum":"27", "title":"Deep Tracks", "id":"thevault", "parentID":"00070044g%3arock"}, {"fullTitle":"28 - The Spectrum", "channelNum":"28", "title":"The Spectrum", "id":"thespectrum", "parentID":"00070044g%3arock"}, {"fullTitle":"29 - Jam_ON", "channelNum":"29", "title":"Jam_ON", "id":"jamon", "parentID":"00070044g%3arock"}, + {"fullTitle":"3 - Venus", "channelNum":"3", "title":"Venus", "id":"9389", "parentID":"00070044g%3apop"}, {"fullTitle":"30 - The Loft", "channelNum":"30", "title":"The Loft", "id":"8207", "parentID":"00070044g%3arock"}, + {"fullTitle":"300 - Poptropolis", "channelNum":"300", "title":"Poptropolis", "id":"9412", "parentID":"00070044g%3apop"}, + {"fullTitle":"301 - Road Trip Radio", "channelNum":"301", "title":"Road Trip Radio", "id":"9415", "parentID":"00070044g%3apop"}, + {"fullTitle":"302 - The Covers Channel", "channelNum":"302", "title":"The Covers Channel", "id":"9416", "parentID":"00070044g%3apop"}, {"fullTitle":"31 - Tom Petty Radio", "channelNum":"31", "title":"Tom Petty Radio", "id":"9407", "parentID":"00070044g%3arock"}, + {"fullTitle":"310 - Rock and Roll Hall of Fame Radio", "channelNum":"310", "title":"Rock and Roll Hall of Fame Radio", "id":"9174", "parentID":"00070044g%3arock"}, {"fullTitle":"310 - SXM Rock Hall Radio", "channelNum":"310", "title":"SXM Rock Hall Radio", "id":"9174", "parentID":"00070044g%3arock"}, + {"fullTitle":"311 - Yacht Rock Radio", "channelNum":"311", "title":"Yacht Rock Radio", "id":"9420", "parentID":"00070044g%3arock"}, {"fullTitle":"312 - Pettys Buried Treasure", "channelNum":"312", "title":"Pettys Buried Treasure", "id":"9352", "parentID":"00070044g%3arock"}, {"fullTitle":"313 - RockBar", "channelNum":"313", "title":"RockBar", "id":"9175", "parentID":"00070044g%3arock"}, - {"fullTitle":"314 - SiriusXM Turbo", "channelNum":"314", "title":"SiriusXM Turbo", "id":"9413", "parentID":"00070044g%3arock"}, + {"fullTitle":"314 - Faction Punk", "channelNum":"314", "title":"Faction Punk", "id":"faction", "parentID":"00070044g%3arock"}, {"fullTitle":"316 - SiriusXM Comes Alive!", "channelNum":"316", "title":"SiriusXM Comes Alive!", "id":"9176", "parentID":"00070044g%3arock"}, {"fullTitle":"32 - The Bridge", "channelNum":"32", "title":"The Bridge", "id":"thebridge", "parentID":"00070044g%3arock"}, {"fullTitle":"33 - 1st Wave", "channelNum":"33", "title":"1st Wave", "id":"firstwave", "parentID":"00070044g%3arock"}, + {"fullTitle":"330 - SiriusXM Silk", "channelNum":"330", "title":"SiriusXM Silk", "id":"9364", "parentID":"00070044g%3arandb"}, {"fullTitle":"34 - Lithium", "channelNum":"34", "title":"Lithium", "id":"90salternative", "parentID":"00070044g%3arock"}, + {"fullTitle":"340 - Tiëstos Club Life Radio", "channelNum":"340", "title":"Tiëstos Club Life Radio", "id":"9219", "parentID":"00070044g%3adance"}, {"fullTitle":"35 - SiriusXMU", "channelNum":"35", "title":"SiriusXMU", "id":"leftofcenter", "parentID":"00070044g%3arock"}, + {"fullTitle":"350 - Red White & Booze", "channelNum":"350", "title":"Red White & Booze", "id":"9178", "parentID":"00070044g%3acountry"}, {"fullTitle":"36 - Alt Nation", "channelNum":"36", "title":"Alt Nation", "id":"altnation", "parentID":"00070044g%3arock"}, {"fullTitle":"37 - Octane", "channelNum":"37", "title":"Octane", "id":"octane", "parentID":"00070044g%3arock"}, + {"fullTitle":"370 - SportsCenter", "channelNum":"370", "title":"SportsCenter", "id":"9180", "parentID":"00070044g%3asportstalk"}, {"fullTitle":"38 - Ozzys Boneyard", "channelNum":"38", "title":"Ozzys Boneyard", "id":"buzzsaw", "parentID":"00070044g%3arock"}, {"fullTitle":"39 - Hair Nation", "channelNum":"39", "title":"Hair Nation", "id":"hairnation", "parentID":"00070044g%3arock"}, + {"fullTitle":"4 - Pitbulls Globalization", "channelNum":"4", "title":"Pitbulls Globalization", "id":"9406", "parentID":"00070044g%3apop"}, {"fullTitle":"40 - Liquid Metal", "channelNum":"40", "title":"Liquid Metal", "id":"hardattack", "parentID":"00070044g%3arock"}, - {"fullTitle":"41 - Faction", "channelNum":"41", "title":"Faction", "id":"faction", "parentID":"00070044g%3arock"}, + {"fullTitle":"400 - Carlins Corner", "channelNum":"400", "title":"Carlins Corner", "id":"9181", "parentID":"00070044g%3acomedy"}, + {"fullTitle":"41 - SiriusXM Turbo", "channelNum":"41", "title":"SiriusXM Turbo", "id":"9413", "parentID":"00070044g%3arock"}, + {"fullTitle":"415 - Vivid Radio", "channelNum":"415", "title":"Vivid Radio", "id":"8369", "parentID":"00070044g%3aentertainment"}, {"fullTitle":"42 - The Joint", "channelNum":"42", "title":"The Joint", "id":"reggaerhythms", "parentID":"00070044g%3arock"}, - {"fullTitle":"713 - Jason Ellis", "channelNum":"713", "title":"Jason Ellis", "id":"9363", "parentID":"00070044g%3arock"}, - {"fullTitle":"330 - SiriusXM Silk", "channelNum":"330", "title":"SiriusXM Silk", "id":"9364", "parentID":"00070044g%3arandb"}, - {"fullTitle":"340 - Tiëstos Club Life Radio", "channelNum":"340", "title":"Tiëstos Club Life Radio", "id":"9219", "parentID":"00070044g%3adance"}, - {"fullTitle":"350 - Red White & Booze", "channelNum":"350", "title":"Red White & Booze", "id":"9178", "parentID":"00070044g%3acountry"}, {"fullTitle":"43 - Backspin", "channelNum":"43", "title":"Backspin", "id":"8124", "parentID":"00070044g%3ahiphop"}, {"fullTitle":"44 - Hip-Hop Nation", "channelNum":"44", "title":"Hip-Hop Nation", "id":"hiphopnation", "parentID":"00070044g%3ahiphop"}, {"fullTitle":"45 - Shade 45", "channelNum":"45", "title":"Shade 45", "id":"shade45", "parentID":"00070044g%3ahiphop"}, + {"fullTitle":"450 - FOX News Talk", "channelNum":"450", "title":"FOX News Talk", "id":"9370", "parentID":"00070044g%3apolitical"}, + {"fullTitle":"455 - C-SPAN Radio", "channelNum":"455", "title":"C-SPAN Radio", "id":"8237", "parentID":"00070044g%3apublicradio"}, {"fullTitle":"46 - The Heat", "channelNum":"46", "title":"The Heat", "id":"hotjamz", "parentID":"00070044g%3arandb"}, {"fullTitle":"47 - SiriusXM FLY", "channelNum":"47", "title":"SiriusXM FLY", "id":"9339", "parentID":"00070044g%3arandb"}, + {"fullTitle":"470 - El Paisa", "channelNum":"470", "title":"El Paisa", "id":"9414", "parentID":"00070044g%3amore"}, {"fullTitle":"48 - Heart & Soul", "channelNum":"48", "title":"Heart & Soul", "id":"heartandsoul", "parentID":"00070044g%3arandb"}, {"fullTitle":"49 - Soul Town", "channelNum":"49", "title":"Soul Town", "id":"soultown", "parentID":"00070044g%3arandb"}, + {"fullTitle":"5 - 50s on 5", "channelNum":"5", "title":"50s on 5", "id":"siriusgold", "parentID":"00070044g%3apop"}, {"fullTitle":"50 - The Groove", "channelNum":"50", "title":"The Groove", "id":"8228", "parentID":"00070044g%3arandb"}, {"fullTitle":"51 - BPM", "channelNum":"51", "title":"BPM", "id":"thebeat", "parentID":"00070044g%3adance"}, {"fullTitle":"52 - Electric Area", "channelNum":"52", "title":"Electric Area", "id":"area33", "parentID":"00070044g%3adance"}, @@ -70,16 +132,10 @@ {"fullTitle":"57 - Y2Kountry", "channelNum":"57", "title":"Y2Kountry", "id":"9340", "parentID":"00070044g%3acountry"}, {"fullTitle":"58 - Prime Country", "channelNum":"58", "title":"Prime Country", "id":"primecountry", "parentID":"00070044g%3acountry"}, {"fullTitle":"59 - Willies Roadhouse", "channelNum":"59", "title":"Willies Roadhouse", "id":"theroadhouse", "parentID":"00070044g%3acountry"}, + {"fullTitle":"6 - 60s on 6", "channelNum":"6", "title":"60s on 6", "id":"60svibrations", "parentID":"00070044g%3apop"}, {"fullTitle":"60 - Outlaw Country", "channelNum":"60", "title":"Outlaw Country", "id":"outlawcountry", "parentID":"00070044g%3acountry"}, {"fullTitle":"61 - Bluegrass Junction", "channelNum":"61", "title":"Bluegrass Junction", "id":"bluegrass", "parentID":"00070044g%3acountry"}, {"fullTitle":"62 - No Shoes Radio", "channelNum":"62", "title":"No Shoes Radio", "id":"9418", "parentID":"00070044g%3acountry"}, - {"fullTitle":"715 - SXM Limited Edition 2", "channelNum":"715", "title":"SXM Limited Edition 2", "id":"9139", "parentID":"00070044g%3arock"}, - {"fullTitle":"716 - SXM Limited Edition 3", "channelNum":"716", "title":"SXM Limited Edition 3", "id":"9353", "parentID":"00070044g%3arock"}, - {"fullTitle":"720 - Sways Universe", "channelNum":"720", "title":"Sways Universe", "id":"9397", "parentID":"00070044g%3ahiphop"}, - {"fullTitle":"721 - SXM Limited Edition 4", "channelNum":"721", "title":"SXM Limited Edition 4", "id":"9398", "parentID":"00070044g%3ahiphop"}, - {"fullTitle":"726 - SXM Limited Edition 5", "channelNum":"726", "title":"SXM Limited Edition 5", "id":"9399", "parentID":"00070044g%3arandb"}, - {"fullTitle":"730 - SXM Limited Edition 6", "channelNum":"730", "title":"SXM Limited Edition 6", "id":"9400", "parentID":"00070044g%3adance"}, - {"fullTitle":"741 - The Village", "channelNum":"741", "title":"The Village", "id":"8227", "parentID":"00070044g%3acountry"}, {"fullTitle":"63 - The Message", "channelNum":"63", "title":"The Message", "id":"spirit", "parentID":"00070044g%3achristian"}, {"fullTitle":"64 - Kirk Franklins Praise", "channelNum":"64", "title":"Kirk Franklins Praise", "id":"praise", "parentID":"00070044g%3achristian"}, {"fullTitle":"65 - enLighten", "channelNum":"65", "title":"enLighten", "id":"8229", "parentID":"00070044g%3achristian"}, @@ -87,11 +143,28 @@ {"fullTitle":"67 - Real Jazz", "channelNum":"67", "title":"Real Jazz", "id":"purejazz", "parentID":"00070044g%3ajazz"}, {"fullTitle":"68 - Spa", "channelNum":"68", "title":"Spa", "id":"spa73", "parentID":"00070044g%3ajazz"}, {"fullTitle":"69 - Escape", "channelNum":"69", "title":"Escape", "id":"8215", "parentID":"00070044g%3ajazz"}, + {"fullTitle":"7 - 70s on 7", "channelNum":"7", "title":"70s on 7", "id":"totally70s", "parentID":"00070044g%3apop"}, {"fullTitle":"70 - BB Kings Bluesville", "channelNum":"70", "title":"BB Kings Bluesville", "id":"siriusblues", "parentID":"00070044g%3ajazz"}, + {"fullTitle":"700 - Neil Diamond Radio", "channelNum":"700", "title":"Neil Diamond Radio", "id":"8372", "parentID":"00070044g%3apop"}, + {"fullTitle":"702 - Elevations", "channelNum":"702", "title":"Elevations", "id":"9362", "parentID":"00070044g%3apop"}, + {"fullTitle":"703 - Oldies Party", "channelNum":"703", "title":"Oldies Party", "id":"9378", "parentID":"00070044g%3aparty"}, + {"fullTitle":"704 - 70s/80s Pop", "channelNum":"770", "title":"70s/80s Pop", "id":"9372", "parentID":"00070044g%3aparty"}, + {"fullTitle":"705 - 80s/90s Pop", "channelNum":"771", "title":"80s/90s Pop", "id":"9373", "parentID":"00070044g%3aparty"}, {"fullTitle":"71 - Siriusly Sinatra", "channelNum":"71", "title":"Siriusly Sinatra", "id":"siriuslysinatra", "parentID":"00070044g%3ajazz"}, + {"fullTitle":"712 - Tom Pettys Buried Treasure", "channelNum":"712", "title":"Tom Pettys Buried Treasure", "id":"9352", "parentID":"00070044g%3arock"}, + {"fullTitle":"713 - The Emo Project", "channelNum":"713", "title":"The Emo Project", "id":"9447", "parentID":"00070044g%3arock"}, + {"fullTitle":"714 - Indie 1.0", "channelNum":"714", "title":"Indie 1.0", "id":"9451", "parentID":"00070044g%3arock"}, + {"fullTitle":"715 - Classic Rock Party", "channelNum":"715", "title":"Classic Rock Party", "id":"9375", "parentID":"00070044g%3aparty"}, + {"fullTitle":"716 - SXM Limited Edition 2", "channelNum":"716", "title":"SXM Limited Edition 2", "id":"9139", "parentID":"00070044g%3arock"}, + {"fullTitle":"717 - SXM Limited Edition 3", "channelNum":"717", "title":"SXM Limited Edition 3", "id":"9353", "parentID":"00070044g%3arock"}, {"fullTitle":"72 - On Broadway", "channelNum":"72", "title":"On Broadway", "id":"broadwaysbest", "parentID":"00070044g%3ajazz"}, + {"fullTitle":"720 - Sways Universe", "channelNum":"720", "title":"Sways Universe", "id":"9397", "parentID":"00070044g%3ahiphop"}, + {"fullTitle":"721 - SXM Limited Edition 4", "channelNum":"721", "title":"SXM Limited Edition 4", "id":"9398", "parentID":"00070044g%3ahiphop"}, + {"fullTitle":"726 - SXM Limited Edition 5", "channelNum":"726", "title":"SXM Limited Edition 5", "id":"9399", "parentID":"00070044g%3arandb"}, {"fullTitle":"73 - 40s Junction", "channelNum":"73", "title":"40s Junction", "id":"8205", "parentID":"00070044g%3ajazz"}, + {"fullTitle":"730 - SXM Limited Edition 6", "channelNum":"730", "title":"SXM Limited Edition 6", "id":"9400", "parentID":"00070044g%3adance"}, {"fullTitle":"74 - Met Opera Radio", "channelNum":"74", "title":"Met Opera Radio", "id":"metropolitanopera", "parentID":"00070044g%3aclassical"}, + {"fullTitle":"741 - The Village", "channelNum":"741", "title":"The Village", "id":"8227", "parentID":"00070044g%3acountry"}, {"fullTitle":"742 - SXM Limited Edition 7", "channelNum":"742", "title":"SXM Limited Edition 7", "id":"9401", "parentID":"00070044g%3acountry"}, {"fullTitle":"745 - SXM Limited Edition 8", "channelNum":"745", "title":"SXM Limited Edition 8", "id":"9402", "parentID":"00070044g%3achristian"}, {"fullTitle":"750 - Cinemagic", "channelNum":"750", "title":"Cinemagic", "id":"8211", "parentID":"00070044g%3ajazz"}, @@ -99,6 +172,8 @@ {"fullTitle":"752 - SXM Limited Edition 9", "channelNum":"752", "title":"SXM Limited Edition 9", "id":"9403", "parentID":"00070044g%3ajazz"}, {"fullTitle":"755 - SiriusXM Pops", "channelNum":"755", "title":"SiriusXM Pops", "id":"siriuspops", "parentID":"00070044g%3aclassical"}, {"fullTitle":"756 - SXM Limited Edition 10", "channelNum":"756", "title":"SXM Limited Edition 10", "id":"9404", "parentID":"00070044g%3aclassical"}, + {"fullTitle":"758 - Iceberg", "channelNum":"758", "title":"Iceberg", "id":"icebergradio", "parentID":"00070044g%3acanadian"}, + {"fullTitle":"759 - Attitude Franco", "channelNum":"759", "title":"Attitude Franco", "id":"energie2", "parentID":"00070044g%3acanadian"}, {"fullTitle":"76 - Symphony Hall", "channelNum":"76", "title":"Symphony Hall", "id":"symphonyhall", "parentID":"00070044g%3aclassical"}, {"fullTitle":"761 - Águila", "channelNum":"761", "title":"Águila", "id":"9186", "parentID":"00070044g%3aworld"}, {"fullTitle":"762 - Caricia", "channelNum":"762", "title":"Caricia", "id":"9188", "parentID":"00070044g%3aworld"}, @@ -108,24 +183,25 @@ {"fullTitle":"766 - Luna", "channelNum":"766", "title":"Luna", "id":"9189", "parentID":"00070044g%3aworld"}, {"fullTitle":"767 - Rumbón", "channelNum":"767", "title":"Rumbón", "id":"9190", "parentID":"00070044g%3aworld"}, {"fullTitle":"768 - La Kueva", "channelNum":"768", "title":"La Kueva", "id":"9191", "parentID":"00070044g%3aworld"}, - {"fullTitle":"157 - ESPN Deportes", "channelNum":"157", "title":"ESPN Deportes", "id":"espndeportes", "parentID":"00070044g%3asportstalk"}, - {"fullTitle":"207 - SiriusXM NBA Radio", "channelNum":"207", "title":"SiriusXM NBA Radio", "id":"9385", "parentID":"00070044g%3asportstalk"}, - {"fullTitle":"208 - SiriusXM PGA TOUR Radio", "channelNum":"208", "title":"SiriusXM PGA TOUR Radio", "id":"8186", "parentID":"00070044g%3asportstalk"}, - {"fullTitle":"209 - MLB Network Radio", "channelNum":"209", "title":"MLB Network Radio", "id":"8333", "parentID":"00070044g%3asportstalk"}, - {"fullTitle":"210 - SXM Fantasy Sports Radio", "channelNum":"210", "title":"SXM Fantasy Sports Radio", "id":"8368", "parentID":"00070044g%3asportstalk"}, - {"fullTitle":"370 - SportsCenter", "channelNum":"370", "title":"SportsCenter", "id":"9180", "parentID":"00070044g%3asportstalk"}, - {"fullTitle":"770 - 70s/80s Pop", "channelNum":"770", "title":"70s/80s Pop", "id":"9372", "parentID":"00070044g%3aparty"}, - {"fullTitle":"771 - 80s/90s Pop", "channelNum":"771", "title":"80s/90s Pop", "id":"9373", "parentID":"00070044g%3aparty"}, - {"fullTitle":"772 - 90s/2K Pop", "channelNum":"772", "title":"90s/2K Pop", "id":"9374", "parentID":"00070044g%3aparty"}, - {"fullTitle":"773 - Classic Rock Party", "channelNum":"773", "title":"Classic Rock Party", "id":"9375", "parentID":"00070044g%3aparty"}, - {"fullTitle":"774 - Rockin Frat Party", "channelNum":"774", "title":"Rockin Frat Party", "id":"9376", "parentID":"00070044g%3aparty"}, - {"fullTitle":"775 - Hip-Hop Party", "channelNum":"775", "title":"Hip-Hop Party", "id":"9377", "parentID":"00070044g%3aparty"}, - {"fullTitle":"776 - Oldies Party", "channelNum":"776", "title":"Oldies Party", "id":"9378", "parentID":"00070044g%3aparty"}, - {"fullTitle":"777 - Pop Party Mix", "channelNum":"777", "title":"Pop Party Mix", "id":"9379", "parentID":"00070044g%3aparty"}, - {"fullTitle":"778 - Punk Party", "channelNum":"778", "title":"Punk Party", "id":"9380", "parentID":"00070044g%3aparty"}, - {"fullTitle":"779 - New Wave Dance Party", "channelNum":"779", "title":"New Wave Dance Party", "id":"9381", "parentID":"00070044g%3aparty"}, + {"fullTitle":"77 - KIDZ BOP Radio", "channelNum":"77", "title":"KIDZ BOP Radio", "id":"9366", "parentID":"00070044g%3akids"}, + {"fullTitle":"78 - Kids Place Live", "channelNum":"78", "title":"Kids Place Live", "id":"8216", "parentID":"00070044g%3akids"}, {"fullTitle":"780 - The Girls Room", "channelNum":"780", "title":"The Girls Room", "id":"9382", "parentID":"00070044g%3aparty"}, {"fullTitle":"781 - Holly", "channelNum":"781", "title":"Holly", "id":"9343", "parentID":"00070044g%3aparty"}, + {"fullTitle":"782 - Holiday Traditions", "channelNum":"782", "title":"Holiday Traditions", "id":"9342", "parentID":"00070044g%3aparty"}, + {"fullTitle":"783 - Holiday Pops", "channelNum":"783", "title":"Holiday Pops", "id":"9344", "parentID":"00070044g%3aparty"}, + {"fullTitle":"784 - Country Christmas", "channelNum":"784", "title":"Country Christmas", "id":"9345", "parentID":"00070044g%3aparty"}, + {"fullTitle":"785 - Navidad", "channelNum":"785", "title":"Navidad", "id":"9348", "parentID":"00070044g%3aparty"}, + {"fullTitle":"786 - Holiday Soul", "channelNum":"786", "title":"Holiday Soul", "id":"9346", "parentID":"00070044g%3aparty"}, + {"fullTitle":"787 - Radio Hanukkah", "channelNum":"787", "title":"Radio Hanukkah", "id":"9349", "parentID":"00070044g%3aparty"}, + {"fullTitle":"79 - Radio Disney", "channelNum":"79", "title":"Radio Disney", "id":"radiodisney", "parentID":"00070044g%3akids"}, + {"fullTitle":"790 - SXM Limited Edition 11", "channelNum":"790", "title":"SXM Limited Edition 11", "id":"9405", "parentID":"00070044g%3aentertainment"}, + {"fullTitle":"791 - Jason Ellis", "channelNum":"791", "title":"Jason Ellis", "id":"9363", "parentID":"00070044g%3arock"}, + {"fullTitle":"794 - SiriusXM Preview", "channelNum":"794", "title":"SiriusXM Preview", "id":"0000", "parentID":"00070044g%3aentertainment"}, + {"fullTitle":"795 - France 24", "channelNum":"795", "title":"France 24", "id":"9417", "parentID":"00070044g%3apublicradio"}, + {"fullTitle":"796 - TheBlaze Radio Network", "channelNum":"796", "title":"TheBlaze Radio Network", "id":"9355", "parentID":"00070044g%3apolitical"}, + {"fullTitle":"797 - SiriusXM Patriot Plus", "channelNum":"797", "title":"SiriusXM Patriot Plus", "id":"8235", "parentID":"00070044g%3apolitical"}, + {"fullTitle":"798 - SiriusXM Progress Plus", "channelNum":"798", "title":"SiriusXM Progress Plus", "id":"9137", "parentID":"00070044g%3apolitical"}, + {"fullTitle":"8 - 80s on 8", "channelNum":"8", "title":"80s on 8", "id":"big80s", "parentID":"00070044g%3apop"}, {"fullTitle":"80 - ESPN Radio", "channelNum":"80", "title":"ESPN Radio", "id":"espnradio", "parentID":"00070044g%3asportstalk"}, {"fullTitle":"81 - ESPN Xtra", "channelNum":"81", "title":"ESPN Xtra", "id":"8254", "parentID":"00070044g%3asportstalk"}, {"fullTitle":"82 - Mad Dog Sports Radio", "channelNum":"82", "title":"Mad Dog Sports Radio", "id":"8213", "parentID":"00070044g%3asportstalk"}, @@ -133,84 +209,14 @@ {"fullTitle":"84 - College Sports Nation", "channelNum":"84", "title":"College Sports Nation", "id":"siriussportsaction", "parentID":"00070044g%3asportstalk"}, {"fullTitle":"85 - SiriusXM FC", "channelNum":"85", "title":"SiriusXM FC", "id":"9341", "parentID":"00070044g%3asportstalk"}, {"fullTitle":"88 - SiriusXM NFL Radio", "channelNum":"88", "title":"SiriusXM NFL Radio", "id":"siriusnflradio", "parentID":"00070044g%3asportstalk"}, + {"fullTitle":"9 - 90s on 9", "channelNum":"9", "title":"90s on 9", "id":"8206", "parentID":"00070044g%3apop"}, {"fullTitle":"90 - SiriusXM NASCAR Radio", "channelNum":"90", "title":"SiriusXM NASCAR Radio", "id":"siriusnascarradio", "parentID":"00070044g%3asportstalk"}, {"fullTitle":"91 - SXM NHL Network Radio", "channelNum":"91", "title":"SXM NHL Network Radio", "id":"8185", "parentID":"00070044g%3asportstalk"}, {"fullTitle":"93 - SiriusXM Rush", "channelNum":"93", "title":"SiriusXM Rush", "id":"8230", "parentID":"00070044g%3asportstalk"}, - {"fullTitle":"100 - Howard 100", "channelNum":"100", "title":"Howard 100", "id":"howardstern100", "parentID":"00070044g%3ahowardstern"}, - {"fullTitle":"101 - Howard 101", "channelNum":"101", "title":"Howard 101", "id":"howardstern101", "parentID":"00070044g%3ahowardstern"}, - {"fullTitle":"112 - CNBC", "channelNum":"112", "title":"CNBC", "id":"cnbc", "parentID":"00070044g%3apublicradio"}, - {"fullTitle":"113 - FOX Business", "channelNum":"113", "title":"FOX Business", "id":"9369", "parentID":"00070044g%3apublicradio"}, - {"fullTitle":"114 - FOX News Channel", "channelNum":"114", "title":"FOX News Channel", "id":"foxnewschannel", "parentID":"00070044g%3apublicradio"}, - {"fullTitle":"115 - FOX News Headlines 24/7", "channelNum":"115", "title":"FOX News Headlines 24/7", "id":"9410", "parentID":"00070044g%3apublicradio"}, - {"fullTitle":"116 - CNN", "channelNum":"116", "title":"CNN", "id":"cnn", "parentID":"00070044g%3apublicradio"}, - {"fullTitle":"117 - HLN", "channelNum":"117", "title":"HLN", "id":"cnnheadlinenews", "parentID":"00070044g%3apublicradio"}, - {"fullTitle":"118 - MSNBC", "channelNum":"118", "title":"MSNBC", "id":"8367", "parentID":"00070044g%3apublicradio"}, - {"fullTitle":"119 - Bloomberg Radio", "channelNum":"119", "title":"Bloomberg Radio", "id":"bloombergradio", "parentID":"00070044g%3apublicradio"}, - {"fullTitle":"120 - BBC World Service", "channelNum":"120", "title":"BBC World Service", "id":"bbcworld", "parentID":"00070044g%3apublicradio"}, - {"fullTitle":"121 - SiriusXM Insight", "channelNum":"121", "title":"SiriusXM Insight", "id":"8183", "parentID":"00070044g%3apublicradio"}, - {"fullTitle":"122 - NPR Now", "channelNum":"122", "title":"NPR Now", "id":"nprnow", "parentID":"00070044g%3apublicradio"}, - {"fullTitle":"123 - PRX Public Radio", "channelNum":"123", "title":"PRX Public Radio", "id":"8239", "parentID":"00070044g%3apublicradio"}, - {"fullTitle":"124 - POTUS Politics", "channelNum":"124", "title":"POTUS Politics", "id":"indietalk", "parentID":"00070044g%3apolitical"}, - {"fullTitle":"125 - SiriusXM Patriot", "channelNum":"125", "title":"SiriusXM Patriot", "id":"siriuspatriot", "parentID":"00070044g%3apolitical"}, - {"fullTitle":"126 - SiriusXM Urban View", "channelNum":"126", "title":"SiriusXM Urban View", "id":"8238", "parentID":"00070044g%3apolitical"}, - {"fullTitle":"127 - SiriusXM Progress", "channelNum":"127", "title":"SiriusXM Progress", "id":"siriusleft", "parentID":"00070044g%3apolitical"}, - {"fullTitle":"147 - RURAL Radio", "channelNum":"147", "title":"RURAL Radio", "id":"9367", "parentID":"00070044g%3apublicradio"}, - {"fullTitle":"155 - CNN en Español", "channelNum":"155", "title":"CNN en Español", "id":"cnnespanol", "parentID":"00070044g%3apublicradio"}, - {"fullTitle":"169 - CBC Radio One", "channelNum":"169", "title":"CBC Radio One", "id":"cbcradioone", "parentID":"00070044g%3apublicradio"}, - {"fullTitle":"450 - FOX News Talk", "channelNum":"450", "title":"FOX News Talk", "id":"9370", "parentID":"00070044g%3apolitical"}, - {"fullTitle":"455 - C-SPAN Radio", "channelNum":"455", "title":"C-SPAN Radio", "id":"8237", "parentID":"00070044g%3apublicradio"}, - {"fullTitle":"77 - KIDZ BOP Radio", "channelNum":"77", "title":"KIDZ BOP Radio", "id":"9366", "parentID":"00070044g%3akids"}, - {"fullTitle":"78 - Kids Place Live", "channelNum":"78", "title":"Kids Place Live", "id":"8216", "parentID":"00070044g%3akids"}, - {"fullTitle":"79 - Radio Disney", "channelNum":"79", "title":"Radio Disney", "id":"radiodisney", "parentID":"00070044g%3akids"}, - {"fullTitle":"795 - France 24", "channelNum":"795", "title":"France 24", "id":"9417", "parentID":"00070044g%3apublicradio"}, - {"fullTitle":"796 - TheBlaze Radio Network", "channelNum":"796", "title":"TheBlaze Radio Network", "id":"9355", "parentID":"00070044g%3apolitical"}, - {"fullTitle":"797 - SiriusXM Patriot Plus", "channelNum":"797", "title":"SiriusXM Patriot Plus", "id":"8235", "parentID":"00070044g%3apolitical"}, - {"fullTitle":"798 - SiriusXM Progress Plus", "channelNum":"798", "title":"SiriusXM Progress Plus", "id":"9137", "parentID":"00070044g%3apolitical"}, - {"fullTitle":"102 - Radio Andy", "channelNum":"102", "title":"Radio Andy", "id":"9409", "parentID":"00070044g%3aentertainment"}, - {"fullTitle":"105 - EW Radio", "channelNum":"105", "title":"EW Radio", "id":"9351", "parentID":"00070044g%3aentertainment"}, - {"fullTitle":"106 - SiriusXM 106", "channelNum":"106", "title":"SiriusXM 106", "id":"siriusoutq", "parentID":"00070044g%3aentertainment"}, - {"fullTitle":"108 - Today Show Radio", "channelNum":"108", "title":"Today Show Radio", "id":"9390", "parentID":"00070044g%3aentertainment"}, - {"fullTitle":"109 - SiriusXM Stars", "channelNum":"109", "title":"SiriusXM Stars", "id":"siriusstars", "parentID":"00070044g%3aentertainment"}, - {"fullTitle":"11 - KIIS-Los Angeles", "channelNum":"11", "title":"KIIS-Los Angeles", "id":"8241", "parentID":"00070044g%3amore"}, - {"fullTitle":"110 - Doctor Radio", "channelNum":"110", "title":"Doctor Radio", "id":"doctorradio", "parentID":"00070044g%3aentertainment"}, - {"fullTitle":"111 - Business Radio", "channelNum":"111", "title":"Business Radio", "id":"9359", "parentID":"00070044g%3aentertainment"}, - {"fullTitle":"12 - Z100/NY", "channelNum":"12", "title":"Z100/NY", "id":"8242", "parentID":"00070044g%3amore"}, - {"fullTitle":"128 - Joel Osteen Radio", "channelNum":"128", "title":"Joel Osteen Radio", "id":"9392", "parentID":"00070044g%3aentertainment"}, - {"fullTitle":"129 - The Catholic Channel", "channelNum":"129", "title":"The Catholic Channel", "id":"thecatholicchannel", "parentID":"00070044g%3areligion"}, - {"fullTitle":"130 - EWTN Radio", "channelNum":"130", "title":"EWTN Radio", "id":"ewtnglobal", "parentID":"00070044g%3areligion"}, - {"fullTitle":"131 - Family Talk", "channelNum":"131", "title":"Family Talk", "id":"8307", "parentID":"00070044g%3areligion"}, - {"fullTitle":"141 - HUR Voices", "channelNum":"141", "title":"HUR Voices", "id":"9129", "parentID":"00070044g%3amore"}, - {"fullTitle":"142 - HBCU", "channelNum":"142", "title":"HBCU", "id":"9130", "parentID":"00070044g%3amore"}, - {"fullTitle":"143 - BYUradio", "channelNum":"143", "title":"BYUradio", "id":"9131", "parentID":"00070044g%3amore"}, - {"fullTitle":"146 - Road Dog Trucking", "channelNum":"146", "title":"Road Dog Trucking", "id":"roaddogtrucking", "parentID":"00070044g%3aentertainment"}, - {"fullTitle":"148 - RadioClassics", "channelNum":"148", "title":"RadioClassics", "id":"radioclassics", "parentID":"00070044g%3aentertainment"}, - {"fullTitle":"168 - Canada Laughs", "channelNum":"168", "title":"Canada Laughs", "id":"8259", "parentID":"00070044g%3acomedy"}, - {"fullTitle":"206 - Opie Radio", "channelNum":"206", "title":"Opie Radio", "id":"8184", "parentID":"00070044g%3aentertainment"}, - {"fullTitle":"400 - Carlins Corner", "channelNum":"400", "title":"Carlins Corner", "id":"9181", "parentID":"00070044g%3acomedy"}, - {"fullTitle":"790 - SXM Limited Edition 11", "channelNum":"790", "title":"SXM Limited Edition 11", "id":"9405", "parentID":"00070044g%3aentertainment"}, - {"fullTitle":"791 - Vivid Radio", "channelNum":"791", "title":"Vivid Radio", "id":"8369", "parentID":"00070044g%3aentertainment"}, - {"fullTitle":"794 - SiriusXM Preview", "channelNum":"794", "title":"SiriusXM Preview", "id":"0000", "parentID":"00070044g%3aentertainment"}, {"fullTitle":"94 - SiriusXM Comedy Greats", "channelNum":"94", "title":"SiriusXM Comedy Greats", "id":"9408", "parentID":"00070044g%3acomedy"}, {"fullTitle":"95 - Comedy Central Radio", "channelNum":"95", "title":"Comedy Central Radio", "id":"9356", "parentID":"00070044g%3acomedy"}, {"fullTitle":"96 - The Foxxhole", "channelNum":"96", "title":"The Foxxhole", "id":"thefoxxhole", "parentID":"00070044g%3acomedy"}, {"fullTitle":"97 - Comedy Roundup", "channelNum":"97", "title":"Comedy Roundup", "id":"bluecollarcomedy", "parentID":"00070044g%3acomedy"}, {"fullTitle":"98 - Laugh USA", "channelNum":"98", "title":"Laugh USA", "id":"laughbreak", "parentID":"00070044g%3acomedy"}, - {"fullTitle":"99 - Raw Dog Comedy Hits", "channelNum":"99", "title":"Raw Dog Comedy Hits", "id":"rawdog", "parentID":"00070044g%3acomedy"}, - {"fullTitle":"144 - Korea Today", "channelNum":"144", "title":"Korea Today", "id":"9132", "parentID":"00070044g%3amore"}, - {"fullTitle":"152 - En Vivo", "channelNum":"152", "title":"En Vivo", "id":"9135", "parentID":"00070044g%3amore"}, - {"fullTitle":"153 - Cristina Radio", "channelNum":"153", "title":"Cristina Radio", "id":"9134", "parentID":"00070044g%3amore"}, - {"fullTitle":"154 - American Latino Radio", "channelNum":"154", "title":"American Latino Radio", "id":"9133", "parentID":"00070044g%3amore"}, - {"fullTitle":"162 - CBC Radio 3", "channelNum":"162", "title":"CBC Radio 3", "id":"cbcradio3", "parentID":"00070044g%3acanadian"}, - {"fullTitle":"163 - Ici Musique Chansons", "channelNum":"163", "title":"Ici Musique Chansons", "id":"8245", "parentID":"00070044g%3acanadian"}, - {"fullTitle":"165 - Multicultural Radio", "channelNum":"165", "title":"Multicultural Radio", "id":"9358", "parentID":"00070044g%3acanadian"}, - {"fullTitle":"166 - Ici FrancoCountry", "channelNum":"166", "title":"Ici FrancoCountry", "id":"rockvelours", "parentID":"00070044g%3acanadian"}, - {"fullTitle":"167 - Canada Talks", "channelNum":"167", "title":"Canada Talks", "id":"9172", "parentID":"00070044g%3acanadian"}, - {"fullTitle":"170 - Ici Première", "channelNum":"170", "title":"Ici Première", "id":"premiereplus", "parentID":"00070044g%3acanadian"}, - {"fullTitle":"171 - CBC Country", "channelNum":"171", "title":"CBC Country", "id":"bandeapart", "parentID":"00070044g%3acanadian"}, - {"fullTitle":"172 - Canada 360 by AMI", "channelNum":"172", "title":"Canada 360 by AMI", "id":"8248", "parentID":"00070044g%3acanadian"}, - {"fullTitle":"173 - The Verge", "channelNum":"173", "title":"The Verge", "id":"8244", "parentID":"00070044g%3acanadian"}, - {"fullTitle":"174 - Influence Franco", "channelNum":"174", "title":"Influence Franco", "id":"8246", "parentID":"00070044g%3acanadian"}, - {"fullTitle":"470 - El Paisa", "channelNum":"470", "title":"El Paisa", "id":"9414", "parentID":"00070044g%3amore"}, - {"fullTitle":"758 - Iceberg", "channelNum":"758", "title":"Iceberg", "id":"icebergradio", "parentID":"00070044g%3acanadian"}, - {"fullTitle":"759 - Attitude Franco", "channelNum":"759", "title":"Attitude Franco", "id":"energie2", "parentID":"00070044g%3acanadian"} -] \ No newline at end of file + {"fullTitle":"99 - Raw Dog Comedy Hits", "channelNum":"99", "title":"Raw Dog Comedy Hits", "id":"rawdog", "parentID":"00070044g%3acomedy"} +] diff --git a/lib/sonos-http-api.js b/lib/sonos-http-api.js index 2bf2e885..fbd09f0f 100644 --- a/lib/sonos-http-api.js +++ b/lib/sonos-http-api.js @@ -3,12 +3,14 @@ const requireDir = require('./helpers/require-dir'); const path = require('path'); const request = require('sonos-discovery/lib/helpers/request'); const logger = require('sonos-discovery/lib/helpers/logger'); +const HttpEventServer = require('./helpers/http-event-server'); function HttpAPI(discovery, settings) { const port = settings.port; const webroot = settings.webroot; const actions = {}; + const events = new HttpEventServer(); this.getWebRoot = function () { return webroot; @@ -52,6 +54,11 @@ function HttpAPI(discovery, settings) { return; } + if (req.url === '/events') { + events.addClient(res); + return; + } + if (discovery.zones.length === 0) { const msg = 'No system has yet been discovered. Please see https://github.com/jishi/node-sonos-http-api/issues/77 if it doesn\'t resolve itself in a few seconds.'; logger.error(msg); @@ -61,7 +68,14 @@ function HttpAPI(discovery, settings) { const params = req.url.substring(1).split('/'); - let player = discovery.getPlayer(decodeURIComponent(params[0])); + // parse decode player name considering decode errors + let player; + try { + player = discovery.getPlayer(decodeURIComponent(params[0])); + } catch (error) { + logger.error(`Unable to parse supplied URI component (${params[0]})`, error); + return sendResponse(500, { status: 'error', error: error.message, stack: error.stack }); + } const opt = {}; @@ -79,12 +93,12 @@ function HttpAPI(discovery, settings) { res.statusCode = code; res.setHeader('Content-Length', Buffer.byteLength(jsonResponse)); res.setHeader('Content-Type', 'application/json;charset=utf-8'); - res.write(new Buffer(jsonResponse)); + res.write(Buffer.from(jsonResponse)); res.end(); } opt.player = player; - handleAction(opt) + Promise.resolve(handleAction(opt)) .then((response) => { if (!response || response.constructor.name === 'IncomingMessage') { response = { status: 'success' }; @@ -113,22 +127,35 @@ function HttpAPI(discovery, settings) { } function invokeWebhook(type, data) { - if (!settings.webhook) return; + var typeName = "type"; + var dataName = "data"; + + if (settings.webhookType) { typeName = settings.webhookType; } + if (settings.webhookData) { dataName = settings.webhookData; } const jsonBody = JSON.stringify({ - type: type, - data: data + [typeName]: type, + [dataName]: data }); - const body = new Buffer(jsonBody, 'utf8'); + events.sendEvent(jsonBody); + + if (!settings.webhook) return; + + const body = Buffer.from(jsonBody, 'utf8'); + + var headers = { + 'Content-Type': 'application/json', + 'Content-Length': body.length + } + if (settings.webhookHeaderName && settings.webhookHeaderContents) { + headers[settings.webhookHeaderName] = settings.webhookHeaderContents; + } request({ method: 'POST', uri: settings.webhook, - headers: { - 'Content-Type': 'application/json', - 'Content-Length': body.length - }, + headers: headers, body }) .catch(function (err) { diff --git a/lib/tts-providers/aws-polly.js b/lib/tts-providers/aws-polly.js new file mode 100644 index 00000000..aab95e63 --- /dev/null +++ b/lib/tts-providers/aws-polly.js @@ -0,0 +1,73 @@ +'use strict'; +const crypto = require('crypto'); +const fs = require('fs'); +const http = require('http'); +const path = require('path'); +const AWS = require('aws-sdk'); +const fileDuration = require('../helpers/file-duration'); +const settings = require('../../settings'); +const logger = require('sonos-discovery/lib/helpers/logger'); + +const DEFAULT_SETTINGS = { + OutputFormat: 'mp3', + VoiceId: 'Joanna', + TextType: 'text' +}; + +function polly(phrase, voiceName) { + if (!settings.aws) { + return Promise.resolve(); + + } + + // Construct a filesystem neutral filename + const dynamicParameters = { Text: phrase }; + const synthesizeParameters = Object.assign({}, DEFAULT_SETTINGS, dynamicParameters); + if (settings.aws.name) { + synthesizeParameters.VoiceId = settings.aws.name; + } + if (voiceName) { + synthesizeParameters.VoiceId = voiceName; + } + if (synthesizeParameters.VoiceId.endsWith('Neural')) { + synthesizeParameters.Engine = 'neural'; + synthesizeParameters.VoiceId = synthesizeParameters.VoiceId.slice(0, -6); + } + + const phraseHash = crypto.createHash('sha1').update(phrase).digest('hex'); + const filename = `polly-${phraseHash}-${synthesizeParameters.VoiceId}.mp3`; + const filepath = path.resolve(settings.webroot, 'tts', filename); + + const expectedUri = `/tts/${filename}`; + try { + fs.accessSync(filepath, fs.R_OK); + return fileDuration(filepath) + .then((duration) => { + return { + duration, + uri: expectedUri + }; + }); + } catch (err) { + logger.info(`announce file for phrase "${phrase}" does not seem to exist, downloading`); + } + + const constructorParameters = Object.assign({ apiVersion: '2016-06-10' }, settings.aws.credentials); + + const polly = new AWS.Polly(constructorParameters); + + return polly.synthesizeSpeech(synthesizeParameters) + .promise() + .then((data) => { + fs.writeFileSync(filepath, data.AudioStream); + return fileDuration(filepath); + }) + .then((duration) => { + return { + duration, + uri: expectedUri + }; + }); +} + +module.exports = polly; diff --git a/lib/tts-providers/default/google.js b/lib/tts-providers/default/google.js new file mode 100644 index 00000000..4b9a2f32 --- /dev/null +++ b/lib/tts-providers/default/google.js @@ -0,0 +1,72 @@ +'use strict'; +const crypto = require('crypto'); +const fs = require('fs'); +const http = require('http'); +const path = require('path'); +const fileDuration = require('../../helpers/file-duration'); +const settings = require('../../../settings'); +const logger = require('sonos-discovery/lib/helpers/logger'); + +function google(phrase, language) { + if (!language) { + language = 'en'; + } + + // Use Google tts translation service to create a mp3 file + const ttsRequestUrl = 'http://translate.google.com/translate_tts?client=tw-ob&tl=' + language + '&q=' + encodeURIComponent(phrase); + + // Construct a filesystem neutral filename + const phraseHash = crypto.createHash('sha1').update(phrase).digest('hex'); + const filename = `google-${phraseHash}-${language}.mp3`; + const filepath = path.resolve(settings.webroot, 'tts', filename); + + const expectedUri = `/tts/${filename}`; + try { + fs.accessSync(filepath, fs.R_OK); + return fileDuration(filepath) + .then((duration) => { + return { + duration, + uri: expectedUri + }; + }); + } catch (err) { + logger.info(`announce file for phrase "${phrase}" does not seem to exist, downloading`); + } + + return new Promise((resolve, reject) => { + const file = fs.createWriteStream(filepath); + const options = { + "headers": { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.106 Safari/537.36" }, + "host": "translate.google.com", + "path": "/translate_tts?client=tw-ob&tl=" + language + "&q=" + encodeURIComponent(phrase) + } + const callback = function (response) { + if (response.statusCode < 300 && response.statusCode >= 200) { + response.pipe(file); + file.on('finish', function () { + file.end(); + resolve(expectedUri); + }); + } else { + reject(new Error(`Download from google TTS failed with status ${response.statusCode}, ${response.message}`)); + + } + } + + http.request(options, callback).on('error', function (err) { + reject(err); + }).end(); + }) + .then(() => { + return fileDuration(filepath); + }) + .then((duration) => { + return { + duration, + uri: expectedUri + }; + }); +} + +module.exports = google; diff --git a/lib/tts-providers/elevenlabs.js b/lib/tts-providers/elevenlabs.js new file mode 100644 index 00000000..a0341f6c --- /dev/null +++ b/lib/tts-providers/elevenlabs.js @@ -0,0 +1,80 @@ +'use strict'; +const crypto = require('crypto'); +const fs = require('fs'); +const http = require('http'); +const path = require('path'); +const ElevenLabs = require('elevenlabs-node'); +const fileDuration = require('../helpers/file-duration'); +const settings = require('../../settings'); +const logger = require('sonos-discovery/lib/helpers/logger'); + +const DEFAULT_SETTINGS = { + stability: 0.5, + similarityBoost: 0.5, + speakerBoost: true, + style: 1, + modelId: "eleven_multilingual_v2" +}; + +// Provider developed based on structure from aws-polly.js. +// In this tts provider language argument from uri is used to inject custom voiceId +function eleven(phrase, voiceId) { + if (!settings.elevenlabs) { + return Promise.resolve(); + } + + // Construct a filesystem neutral filename + const dynamicParameters = { textInput: phrase }; + const synthesizeParameters = Object.assign({}, DEFAULT_SETTINGS, dynamicParameters, settings.elevenlabs.config); + + if (voiceId) { + synthesizeParameters.voiceId = voiceId; + } + + if (!synthesizeParameters.voiceId) { + console.log('Voice ID not found neither in settings.elevenlabs.config nor in request!') + return Promise.resolve(); + } + + const phraseHash = crypto.createHash('sha1').update(phrase).digest('hex'); + const filename = `elevenlabs-${phraseHash}-${synthesizeParameters.voiceId}.mp3`; + const filepath = path.resolve(settings.webroot, 'tts', filename); + + synthesizeParameters.fileName = filepath; + + const expectedUri = `/tts/${filename}`; + try { + fs.accessSync(filepath, fs.R_OK); + return fileDuration(filepath) + .then((duration) => { + return { + duration, + uri: expectedUri + }; + }); + } catch (err) { + logger.info(`announce file for phrase "${phrase}" does not seem to exist, downloading`); + } + + const voice = new ElevenLabs( + { + apiKey: settings.elevenlabs.auth.apiKey + } + ); + + return voice.textToSpeech(synthesizeParameters) + .then((res) => { + console.log('Elevenlabs TTS generated new audio file.'); + }) + .then(() => { + return fileDuration(filepath); + }) + .then((duration) => { + return { + duration, + uri: expectedUri + }; + }); +} + +module.exports = eleven; diff --git a/lib/tts-providers/mac-os.js b/lib/tts-providers/mac-os.js new file mode 100644 index 00000000..53e82686 --- /dev/null +++ b/lib/tts-providers/mac-os.js @@ -0,0 +1,85 @@ +'use strict'; +const crypto = require('crypto'); +const fs = require('fs'); +const http = require('http'); +const path = require('path'); +const fileDuration = require('../helpers/file-duration'); +const settings = require('../../settings'); +const logger = require('sonos-discovery/lib/helpers/logger'); +var exec = require('child_process').exec; + +function macSay(phrase, voice) { + if (!settings.macSay) { + return Promise.resolve(); + } + + var selcetedRate = settings.macSay.rate; + if( !selcetedRate ) { + selcetedRate = "default"; + } + var selectedVoice = settings.macSay.voice; + if( voice ) { + selectedVoice = voice; + } + + // Construct a filesystem neutral filename + const phraseHash = crypto.createHash('sha1').update(phrase).digest('hex'); + const filename = `macSay-${phraseHash}-${selcetedRate}-${selectedVoice}.m4a`; + const filepath = path.resolve(settings.webroot, 'tts', filename); + + const expectedUri = `/tts/${filename}`; + + try { + fs.accessSync(filepath, fs.R_OK); + return fileDuration(filepath) + .then((duration) => { + return { + duration, + uri: expectedUri + }; + }); + } catch (err) { + logger.info(`announce file for phrase "${phrase}" does not seem to exist, downloading`); + } + + return new Promise((resolve, reject) => { + // + // For more information on the "say" command, type "man say" in Terminal + // or go to + // https://developer.apple.com/legacy/library/documentation/Darwin/Reference/ManPages/man1/say.1.html + // + // The list of available voices can be configured in + // System Preferences -> Accessibility -> Speech -> System Voice + // + + var execCommand = `say "${phrase}" -o ${filepath}`; + if( selectedVoice && selcetedRate != "default" ) { + execCommand = `say -r ${selcetedRate} -v ${selectedVoice} "${phrase}" -o ${filepath}`; + } else if ( selectedVoice ) { + execCommand = `say -v ${selectedVoice} "${phrase}" -o ${filepath}`; + } else if ( selcetedRate != "default" ) { + execCommand = `say -r ${selcetedRate} "${phrase}" -o ${filepath}`; + } + + exec(execCommand, + function (error, stdout, stderr) { + if (error !== null) { + reject(error); + } else { + resolve(expectedUri); + } + }); + + }) + .then(() => { + return fileDuration(filepath); + }) + .then((duration) => { + return { + duration, + uri: expectedUri + }; + }); +} + +module.exports = macSay; diff --git a/lib/tts-providers/microsoft.js b/lib/tts-providers/microsoft.js new file mode 100644 index 00000000..e24803ee --- /dev/null +++ b/lib/tts-providers/microsoft.js @@ -0,0 +1,285 @@ +'use strict'; +const crypto = require('crypto'); +const fs = require('fs'); +const path = require('path'); +const fileDuration = require('../helpers/file-duration'); +const request = require('sonos-discovery/lib/helpers/request'); +const logger = require('sonos-discovery/lib/helpers/logger'); +const globalSettings = require('../../settings'); +const XmlEntities = require('html-entities').XmlEntities; + +const xmlEntities = new XmlEntities(); + +const APP_ID = '9aa44d9e6ec14da99231a9166fd50b0f'; +const INSTANCE_ID = crypto.randomBytes(16).toString('hex'); +const TOKEN_EXPIRATION = 590000; // 9:50 minutes in ms +const DEFAULT_SETTINGS = { + name: 'ZiraRUS' +}; + +let bearerToken; +let bearerExpires = Date.now(); + +function generateBearerToken(apiKey) { + return request({ + uri: 'https://api.cognitive.microsoft.com/sts/v1.0/issueToken', + method: 'POST', + type: 'raw', + headers: { + 'Ocp-Apim-Subscription-Key': apiKey, + 'Content-Length': 0 + } + }) + .then((body) => { + logger.debug(`Bearer token: body`); + bearerToken = body; + bearerExpires = Date.now() + TOKEN_EXPIRATION; + }); +} + +function format(lang, gender, name, text) { + const escapedText = xmlEntities.encodeNonUTF(text); + return `${escapedText}`; +} + +function microsoft(phrase, voiceName) { + if (!globalSettings.microsoft || !globalSettings.microsoft.key) { + return Promise.resolve(); + } + + const settings = Object.assign({}, DEFAULT_SETTINGS, globalSettings.microsoft); + + if (voiceName) { + settings.name = voiceName; + } + + const phraseHash = crypto.createHash('sha1').update(phrase).digest('hex'); + const filename = `microsoft-${phraseHash}-${settings.name}.wav`; + const filepath = path.resolve(globalSettings.webroot, 'tts', filename); + + const expectedUri = `/tts/${filename}`; + try { + fs.accessSync(filepath, fs.R_OK); + return fileDuration(filepath) + .then((duration) => { + return { + duration, + uri: expectedUri + }; + }); + + } catch (err) { + logger.info(`announce file for phrase "${phrase}" does not seem to exist, downloading`); + } + + let promise = Promise.resolve(); + if (bearerExpires < Date.now()) { + // Refresh token + promise = generateBearerToken(settings.key); + } + + return promise.then(() => { + const voice = VOICE[settings.name]; + if (!voice) { + throw new Error(`Voice name ${settings.name} could not be located in the list of valid voice names`); + } + + const ssml = format(voice.language, voice.gender, voice.font, phrase); + return request({ + uri: 'https://speech.platform.bing.com/synthesize', + method: 'POST', + type: 'stream', + headers: { + Authorization: `Bearer ${bearerToken}`, + 'Content-Type': 'application/ssml+xml', + 'X-Microsoft-OutputFormat': 'riff-16khz-16bit-mono-pcm', + 'X-Search-AppId': APP_ID, + 'X-Search-ClientID': INSTANCE_ID, + 'User-Agent': 'node-sonos-http-api', + 'Content-Length': ssml.length + }, + body: ssml + }) + .then(res => { + return new Promise((resolve) => { + + const file = fs.createWriteStream(filepath); + res.pipe(file); + + file.on('close', () => { + resolve(); + }) + }) + + }) + .then(() => { + return fileDuration(filepath); + }) + .then((duration) => { + return { + duration, + uri: expectedUri + }; + }) + .catch((err) => { + logger.error(err); + throw err; + }); + }); +} + +const VOICE = { + Hoda: { language: 'ar-EG', gender: 'Female', font: 'Microsoft Server Speech Text to Speech Voice (ar-EG, Hoda)' }, + Naayf: { language: 'ar-SA', gender: 'Male', font: 'Microsoft Server Speech Text to Speech Voice (ar-SA, Naayf)' }, + Ivan: { language: 'bg-BG', gender: 'Male', font: 'Microsoft Server Speech Text to Speech Voice (bg-BG, Ivan)' }, + HerenaRUS: { language: 'ca-ES', gender: 'Female', font: 'Microsoft Server Speech Text to Speech Voice (ca-ES, HerenaRUS)' }, + Jakub: { language: 'cs-CZ', gender: 'Male', font: 'Microsoft Server Speech Text to Speech Voice (cs-CZ, Jakub)' }, + Vit: { language: 'cs-CZ', gender: 'Male', font: 'Microsoft Server Speech Text to Speech Voice (cs-CZ, Vit)' }, + HelleRUS: { language: 'da-DK', gender: 'Female', font: 'Microsoft Server Speech Text to Speech Voice (da-DK, HelleRUS)' }, + Michael: { language: 'de-AT', gender: 'Male', font: 'Microsoft Server Speech Text to Speech Voice (de-AT, Michael)' }, + Karsten: { language: 'de-CH', gender: 'Male', font: 'Microsoft Server Speech Text to Speech Voice (de-CH, Karsten)' }, + Hedda: { language: 'de-DE', gender: 'Female', font: 'Microsoft Server Speech Text to Speech Voice (de-DE, Hedda)' }, + Stefan: { + language: 'de-DE', + gender: 'Male', + font: 'Microsoft Server Speech Text to Speech Voice (de-DE, Stefan, Apollo)' + }, + Catherine: { + language: 'en-AU', + gender: 'Female', + font: 'Microsoft Server Speech Text to Speech Voice (en-AU, Catherine)' + }, + Linda: { language: 'en-CA', gender: 'Female', font: 'Microsoft Server Speech Text to Speech Voice (en-CA, Linda)' }, + Susan: { + language: 'en-GB', + gender: 'Female', + font: 'Microsoft Server Speech Text to Speech Voice (en-GB, Susan, Apollo)' + }, + George: { + language: 'en-GB', + gender: 'Male', + font: 'Microsoft Server Speech Text to Speech Voice (en-GB, George, Apollo)' + }, + Ravi: { + language: 'en-IN', + gender: 'Male', + font: 'Microsoft Server Speech Text to Speech Voice (en-IN, Ravi, Apollo)' + }, + ZiraRUS: { + language: 'en-US', + gender: 'Female', + font: 'Microsoft Server Speech Text to Speech Voice (en-US, ZiraRUS)' + }, + BenjaminRUS: { + language: 'en-US', + gender: 'Male', + font: 'Microsoft Server Speech Text to Speech Voice (en-US, BenjaminRUS)' + }, + Laura: { + language: 'es-ES', + gender: 'Female', + font: 'Microsoft Server Speech Text to Speech Voice (es-ES, Laura, Apollo)' + }, + Pablo: { + language: 'es-ES', + gender: 'Male', + font: 'Microsoft Server Speech Text to Speech Voice (es-ES, Pablo, Apollo)' + }, + Raul: { + language: 'es-MX', + gender: 'Male', + font: 'Microsoft Server Speech Text to Speech Voice (es-MX, Raul, Apollo)' + }, + Caroline: { + language: 'fr-CA', + gender: 'Female', + font: 'Microsoft Server Speech Text to Speech Voice (fr-CA, Caroline)' + }, + Julie: { + language: 'fr-FR', + gender: 'Female', + font: 'Microsoft Server Speech Text to Speech Voice (fr-FR, Julie, Apollo)' + }, + Paul: { + language: 'fr-FR', + gender: 'Male', + font: 'Microsoft Server Speech Text to Speech Voice (fr-FR, Paul, Apollo)' + }, + Cosimo: { + language: 'it-IT', + gender: 'Male', + font: 'Microsoft Server Speech Text to Speech Voice (it-IT, Cosimo, Apollo)' + }, + Ayumi: { + language: 'ja-JP', + gender: 'Female', + font: 'Microsoft Server Speech Text to Speech Voice (ja-JP, Ayumi, Apollo)' + }, + Ichiro: { + language: 'ja-JP', + gender: 'Male', + font: 'Microsoft Server Speech Text to Speech Voice (ja-JP, Ichiro, Apollo)' + }, + Daniel: { + language: 'pt-BR', + gender: 'Male', + font: 'Microsoft Server Speech Text to Speech Voice (pt-BR, Daniel, Apollo)' + }, + Andrei: { + language: 'ro-RO', + gender: 'Male', + font: 'Microsoft Server Speech Text to Speech Voice (ro-RO, Andrei)', + }, + Irina: { + language: 'ru-RU', + gender: 'Female', + font: 'Microsoft Server Speech Text to Speech Voice (ru-RU, Irina, Apollo)' + }, + Pavel: { + language: 'ru-RU', + gender: 'Male', + font: 'Microsoft Server Speech Text to Speech Voice (ru-RU, Pavel, Apollo)' + }, + HuihuiRUS: { + language: 'zh-CN', + gender: 'Female', + font: 'Microsoft Server Speech Text to Speech Voice (zh-CN, HuihuiRUS)' + }, + Yaoyao: { + language: 'zh-CN', + gender: 'Female', + font: 'Microsoft Server Speech Text to Speech Voice (zh-CN, Yaoyao, Apollo)' + }, + Kangkang: { + language: 'zh-CN', + gender: 'Male', + font: 'Microsoft Server Speech Text to Speech Voice (zh-CN, Kangkang, Apollo)' + }, + Tracy: { + language: 'zh-HK', + gender: 'Female', + font: 'Microsoft Server Speech Text to Speech Voice (zh-HK, Tracy, Apollo)' + }, + Danny: { + language: 'zh-HK', + gender: 'Male', + font: 'Microsoft Server Speech Text to Speech Voice (zh-HK, Danny, Apollo)' + }, + Yating: { + language: 'zh-TW', + gender: 'Female', + font: 'Microsoft Server Speech Text to Speech Voice (zh-TW, Yating, Apollo)' + }, + Zhiwei: { + language: 'zh-TW', + gender: 'Male', + font: 'Microsoft Server Speech Text to Speech Voice (zh-TW, Zhiwei, Apollo)' + }, + JessaNeural: { + language: 'en-US', + gender: 'Female', + font: 'Microsoft Server Speech Text to Speech Voice (en-US, JessaNeural)' + } +}; + +module.exports = microsoft; diff --git a/lib/tts-providers/voicerss.js b/lib/tts-providers/voicerss.js new file mode 100644 index 00000000..6b677dde --- /dev/null +++ b/lib/tts-providers/voicerss.js @@ -0,0 +1,70 @@ +'use strict'; +const crypto = require('crypto'); +const fs = require('fs'); +const http = require('http'); +const path = require('path'); +const fileDuration = require('../helpers/file-duration'); +const settings = require('../../settings'); + +function voicerss(phrase, language) { + if (!settings.voicerss) { + return Promise.resolve(); + + } + + if (!language) { + language = 'en-gb'; + } + // Use voicerss tts translation service to create a mp3 file + // Option "c=MP3" added. Otherwise a WAV file is created that won't play on Sonos. + const ttsRequestUrl = `http://api.voicerss.org/?key=${settings.voicerss}&f=22khz_16bit_mono&hl=${language}&src=${encodeURIComponent(phrase)}&c=MP3`; + + // Construct a filesystem neutral filename + const phraseHash = crypto.createHash('sha1').update(phrase).digest('hex'); + const filename = `voicerss-${phraseHash}-${language}.mp3`; + const filepath = path.resolve(settings.webroot, 'tts', filename); + + const expectedUri = `/tts/${filename}`; + try { + fs.accessSync(filepath, fs.R_OK); + return fileDuration(filepath) + .then((duration) => { + return { + duration, + uri: expectedUri + }; + }); + } catch (err) { + console.log(`announce file for phrase "${phrase}" does not seem to exist, downloading`); + } + + return new Promise((resolve, reject) => { + var file = fs.createWriteStream(filepath); + http.get(ttsRequestUrl, function (response) { + if (response.statusCode < 300 && response.statusCode >= 200) { + response.pipe(file); + file.on('finish', function () { + file.end(); + resolve(expectedUri); + }); + } else { + reject(new Error(`Download from voicerss failed with status ${response.statusCode}, ${response.message}`)); + + } + }).on('error', function (err) { + fs.unlink(dest); + reject(err); + }); + }) + .then(() => { + return fileDuration(filepath); + }) + .then((duration) => { + return { + duration, + uri: expectedUri + }; + }); +} + +module.exports = voicerss; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..de333814 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,5416 @@ +{ + "name": "sonos-http-api", + "version": "1.6.9", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "sonos-http-api", + "version": "1.6.9", + "license": "MIT", + "dependencies": { + "anesidora": "^1.2.0", + "aws-sdk": "^2.1299.0", + "basic-auth": "~1.1.0", + "elevenlabs-node": "2.0.1", + "fuse.js": "^6.4.1", + "html-entities": "^1.2.1", + "json5": "^0.5.1", + "mime": "^1.4.1", + "music-metadata": "^1.1.0", + "request-promise": "~1.0.2", + "serve-static": "^1.15.0", + "sonos-discovery": "https://github.com/jishi/node-sonos-discovery/archive/v1.8.0.tar.gz", + "wav-file-info": "0.0.8" + }, + "devDependencies": { + "eslint": "^4.8.0", + "eslint-config-airbnb-base": "^12.0.1", + "eslint-plugin-import": "^2.7.0" + }, + "engines": { + "node": ">=4 <23" + } + }, + "node_modules/acorn": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.1.tgz", + "integrity": "sha512-d+nbxBUGKg7Arpsvbnlq61mc12ek3EY8EQldM3GPAhWJ1UVxC6TDGbIvUMNU6obBX3i1+ptCIzV4vq0gFPEGVQ==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-3.0.1.tgz", + "integrity": "sha1-r9+UiPsezvyDSPb7IvRk4ypYs2s=", + "dev": true, + "dependencies": { + "acorn": "^3.0.4" + } + }, + "node_modules/acorn-jsx/node_modules/acorn": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz", + "integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo=", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ajv": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", + "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", + "dependencies": { + "co": "^4.6.0", + "fast-deep-equal": "^1.0.0", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.3.0" + } + }, + "node_modules/ajv-keywords": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-2.1.1.tgz", + "integrity": "sha1-YXmX/F9gV2iUxDX5QNgZ4TW4B2I=", + "dev": true, + "peerDependencies": { + "ajv": "^5.0.0" + } + }, + "node_modules/anesidora": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/anesidora/-/anesidora-1.2.1.tgz", + "integrity": "sha1-/ZWdrLiPx6im5xE+OA8/rQmTipo=", + "dependencies": { + "request": "^2.64.0", + "underscore": "^1.8.3" + } + }, + "node_modules/ansi-escapes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.1.0.tgz", + "integrity": "sha512-UgAb8H9D41AQnu/PbWlCofQVcnV4Gs2bBJi9eZPxfU/hgglFh3SMDMENRIqdr7H6XFnXdoknctFByVsCOotTVw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/array-union": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", + "dev": true, + "dependencies": { + "array-uniq": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array-uniq": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", + "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/asn1": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", + "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=" + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "node_modules/available-typed-arrays": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", + "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/aws-sdk": { + "version": "2.1299.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1299.0.tgz", + "integrity": "sha512-xTh6pmCUEJljkFfTM3sE8UozDxal80uX/5WZl8GcjQ+NbrGeQEdvL6wFWBwEEVbhR0VBVuU37cKPuQlfENbRYA==", + "dependencies": { + "buffer": "4.9.2", + "events": "1.1.1", + "ieee754": "1.1.13", + "jmespath": "0.16.0", + "querystring": "0.2.0", + "sax": "1.2.1", + "url": "0.10.3", + "util": "^0.12.4", + "uuid": "8.0.0", + "xml2js": "0.4.19" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/aws-sdk/node_modules/uuid": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.0.0.tgz", + "integrity": "sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", + "engines": { + "node": "*" + } + }, + "node_modules/aws4": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.7.0.tgz", + "integrity": "sha512-32NDda82rhwD9/JBCCkB+MRYDp0oSvlo2IL6rQWA10PQi7tDUM3eqMSltXmY+Oyl/7N3P3qNtAlv7X0d9bI28w==" + }, + "node_modules/axios": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz", + "integrity": "sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==", + "dependencies": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/axios/node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/axios/node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/babel-code-frame": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", + "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", + "dev": true, + "dependencies": { + "chalk": "^1.1.3", + "esutils": "^2.0.2", + "js-tokens": "^3.0.2" + } + }, + "node_modules/babel-code-frame/node_modules/chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "dependencies": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/babel-code-frame/node_modules/strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/basic-auth": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-1.1.0.tgz", + "integrity": "sha1-RSIe5Cn37h5QNb4/UVM/HN/SmIQ=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "optional": true, + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, + "node_modules/bluebird": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-2.11.0.tgz", + "integrity": "sha1-U0uQM8AiyVecVro7Plpcqvu2UOE=" + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/buffer": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", + "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", + "dependencies": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", + "dev": true + }, + "node_modules/builtin-modules": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", + "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dependencies": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/caller-path": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-0.1.0.tgz", + "integrity": "sha1-lAhe9jWB7NPaqSREqP6U6CV3dR8=", + "dev": true, + "dependencies": { + "callsites": "^0.2.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/callsites": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-0.2.0.tgz", + "integrity": "sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + }, + "node_modules/chalk": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", + "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chalk/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", + "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chardet": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.4.2.tgz", + "integrity": "sha1-tUc7M9yXxCTl2Y3IfVXU2KKci/I=", + "dev": true + }, + "node_modules/circular-json": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.3.3.tgz", + "integrity": "sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==", + "deprecated": "CircularJSON is in maintenance only, flatted is its successor.", + "dev": true + }, + "node_modules/cli-cursor": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", + "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", + "dev": true, + "dependencies": { + "restore-cursor": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cli-width": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", + "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=", + "dev": true + }, + "node_modules/cls-bluebird": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/cls-bluebird/-/cls-bluebird-1.1.3.tgz", + "integrity": "sha1-syY8EaCJsDlhhaG3q5BNkPAq1Cg=", + "dependencies": { + "is-bluebird": "^1.0.1", + "shimmer": "^1.1.0" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/color-convert": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.2.tgz", + "integrity": "sha512-3NUJZdhMhcdPn8vJ9v2UQJoH0qqoGUkYTgFEPZaPjEtwmmKUfNV46zZmgB2M5M4DCEQHMaCfWHCxiBflLm04Tg==", + "dev": true, + "dependencies": { + "color-name": "1.1.1" + } + }, + "node_modules/color-name": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.1.tgz", + "integrity": "sha1-SxQVMEz1ACjqgWQ2Q72C6gWANok=", + "dev": true + }, + "node_modules/combined-stream": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", + "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "node_modules/concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "dev": true, + "engines": [ + "node >= 0.8" + ], + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/contains-path": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/contains-path/-/contains-path-0.1.0.tgz", + "integrity": "sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "node_modules/cross-spawn": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", + "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", + "dev": true, + "dependencies": { + "lru-cache": "^4.0.1", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "node_modules/dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "dependencies": { + "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "dev": true + }, + "node_modules/del": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/del/-/del-2.2.2.tgz", + "integrity": "sha1-wSyYHQZ4RshLyvhiz/kw2Qf/0ag=", + "dev": true, + "dependencies": { + "globby": "^5.0.0", + "is-path-cwd": "^1.0.0", + "is-path-in-cwd": "^1.0.0", + "object-assign": "^4.0.1", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0", + "rimraf": "^2.2.8" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "optional": true, + "dependencies": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/elevenlabs-node": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/elevenlabs-node/-/elevenlabs-node-2.0.1.tgz", + "integrity": "sha512-q6vUznhudS1yxYxTVP3sT3xWzSzoXH+y3T+4GE1X8gjGwJw1Inmko/paQfl4d+CpTQXGNWuACAlFUdIk96xebw==", + "dependencies": { + "axios": "^1.4.0", + "fs-extra": "^11.1.1" + } + }, + "node_modules/elevenlabs-node/node_modules/fs-extra": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", + "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/elevenlabs-node/node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/elevenlabs-node/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es6-promise": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==" + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/eslint": { + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-4.19.1.tgz", + "integrity": "sha512-bT3/1x1EbZB7phzYu7vCr1v3ONuzDtX8WjuM9c0iYxe+cq+pwcKEoQjl7zd3RpC6YOLgnSy3cTN58M2jcoPDIQ==", + "dev": true, + "dependencies": { + "ajv": "^5.3.0", + "babel-code-frame": "^6.22.0", + "chalk": "^2.1.0", + "concat-stream": "^1.6.0", + "cross-spawn": "^5.1.0", + "debug": "^3.1.0", + "doctrine": "^2.1.0", + "eslint-scope": "^3.7.1", + "eslint-visitor-keys": "^1.0.0", + "espree": "^3.5.4", + "esquery": "^1.0.0", + "esutils": "^2.0.2", + "file-entry-cache": "^2.0.0", + "functional-red-black-tree": "^1.0.1", + "glob": "^7.1.2", + "globals": "^11.0.1", + "ignore": "^3.3.3", + "imurmurhash": "^0.1.4", + "inquirer": "^3.0.6", + "is-resolvable": "^1.0.0", + "js-yaml": "^3.9.1", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.3.0", + "lodash": "^4.17.4", + "minimatch": "^3.0.2", + "mkdirp": "^0.5.1", + "natural-compare": "^1.4.0", + "optionator": "^0.8.2", + "path-is-inside": "^1.0.2", + "pluralize": "^7.0.0", + "progress": "^2.0.0", + "regexpp": "^1.0.1", + "require-uncached": "^1.0.3", + "semver": "^5.3.0", + "strip-ansi": "^4.0.0", + "strip-json-comments": "~2.0.1", + "table": "4.0.2", + "text-table": "~0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-config-airbnb-base": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-12.1.0.tgz", + "integrity": "sha512-/vjm0Px5ZCpmJqnjIzcFb9TKZrKWz0gnuG/7Gfkt0Db1ELJR51xkZth+t14rYdqWgX836XbuxtArbIHlVhbLBA==", + "dev": true, + "dependencies": { + "eslint-restricted-globals": "^0.1.1" + }, + "engines": { + "node": ">= 4" + }, + "peerDependencies": { + "eslint": "^4.9.0", + "eslint-plugin-import": "^2.7.0" + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.2.tgz", + "integrity": "sha512-sfmTqJfPSizWu4aymbPr4Iidp5yKm8yDkHp+Ir3YiTHiiDfxh69mOUsmiqW6RZ9zRXFaF64GtYmN7e+8GHBv6Q==", + "dev": true, + "dependencies": { + "debug": "^2.6.9", + "resolve": "^1.5.0" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/eslint-module-utils": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.2.0.tgz", + "integrity": "sha1-snA2LNiLGkitMIl2zn+lTphBF0Y=", + "dev": true, + "dependencies": { + "debug": "^2.6.8", + "pkg-dir": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.13.0.tgz", + "integrity": "sha512-t6hGKQDMIt9N8R7vLepsYXgDfeuhp6ZJSgtrLEDxonpSubyxUZHjhm6LsAaZX8q6GYVxkbT3kTsV9G5mBCFR6A==", + "dev": true, + "dependencies": { + "contains-path": "^0.1.0", + "debug": "^2.6.8", + "doctrine": "1.5.0", + "eslint-import-resolver-node": "^0.3.1", + "eslint-module-utils": "^2.2.0", + "has": "^1.0.1", + "lodash": "^4.17.4", + "minimatch": "^3.0.3", + "read-pkg-up": "^2.0.0", + "resolve": "^1.6.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "2.x - 5.x" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/eslint-plugin-import/node_modules/doctrine": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-1.5.0.tgz", + "integrity": "sha1-N53Ocw9hZvds76TmcHoVmwLFpvo=", + "dev": true, + "dependencies": { + "esutils": "^2.0.2", + "isarray": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-restricted-globals": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/eslint-restricted-globals/-/eslint-restricted-globals-0.1.1.tgz", + "integrity": "sha1-NfDVy8ZMLj7WLpO0saevBbp+1Nc=", + "dev": true + }, + "node_modules/eslint-scope": { + "version": "3.7.3", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-3.7.3.tgz", + "integrity": "sha512-W+B0SvF4gamyCTmUc+uITPY0989iXVfKvhwtmJocTaYoc/3khEHmEmvfY/Gn9HA9VV75jrQECsHizkNw1b68FA==", + "dev": true, + "dependencies": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", + "integrity": "sha512-qzm/XxIbxm/FHyH341ZrbnMUpe+5Bocte9xkmFMzPMjRaZMcXww+MpBptFvtU+79L362nqiLhekCxCxDPaUMBQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/espree": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/espree/-/espree-3.5.4.tgz", + "integrity": "sha512-yAcIQxtmMiB/jL32dzEp2enBeidsB7xWPLNiw3IIkpVds1P+h7qF9YwJq1yUNzp2OKXgAprs4F61ih66UsoD1A==", + "dev": true, + "dependencies": { + "acorn": "^5.5.0", + "acorn-jsx": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.0.1.tgz", + "integrity": "sha512-SmiyZ5zIWH9VM+SRUReLS5Q8a7GxtRdxEBVZpm98rJM7Sb+A9DVCndXfkeFUd3byderg+EbDkfnevfCwynWaNA==", + "dev": true, + "dependencies": { + "estraverse": "^4.0.0" + }, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/esrecurse": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", + "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", + "dev": true, + "dependencies": { + "estraverse": "^4.1.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", + "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/esutils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/events": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", + "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=", + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "node_modules/external-editor": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-2.2.0.tgz", + "integrity": "sha512-bSn6gvGxKt+b7+6TKEv1ZycHleA7aHhRHyAqJyp5pbUFuYYNIzpZnQDk7AsYckyWdEnTeAnay0aCy2aV6iTk9A==", + "dev": true, + "dependencies": { + "chardet": "^0.4.0", + "iconv-lite": "^0.4.17", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", + "engines": [ + "node >=0.6.0" + ] + }, + "node_modules/fast-deep-equal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", + "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "node_modules/figures": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", + "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", + "dev": true, + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/file-entry-cache": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-2.0.0.tgz", + "integrity": "sha1-w5KZDD5oR4PYOLjISkXYoEhFg2E=", + "dev": true, + "dependencies": { + "flat-cache": "^1.2.1", + "object-assign": "^4.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/find-up": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", + "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", + "dev": true, + "dependencies": { + "path-exists": "^2.0.0", + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/flat-cache": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-1.3.0.tgz", + "integrity": "sha1-0wMLMrOBVPTjt+nHCfSQ9++XxIE=", + "dev": true, + "dependencies": { + "circular-json": "^0.3.1", + "del": "^2.0.2", + "graceful-fs": "^4.1.2", + "write": "^0.2.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.3", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz", + "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dependencies": { + "is-callable": "^1.1.3" + } + }, + "node_modules/forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "engines": { + "node": "*" + } + }, + "node_modules/form-data": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.2.tgz", + "integrity": "sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-extra": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-6.0.1.tgz", + "integrity": "sha512-GnyIkKhhzXZUWFCaJzvyDLEEgDkPfb4/TPvJCJVuS8MWZgoSsErf++QpiAlDnKFcqhRlm+tIOcencCjyJE6ZCA==", + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "node_modules/functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", + "dev": true + }, + "node_modules/fuse.js": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-6.4.1.tgz", + "integrity": "sha512-+hAS7KYgLXontDh/vqffs7wIBw0ceb9Sx8ywZQhOsiQGcSO5zInGhttWOUYQYlvV/yYMJOacQ129Xs3mP3+oZQ==", + "engines": { + "node": ">=10" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz", + "integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==", + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "dependencies": { + "assert-plus": "^1.0.0" + } + }, + "node_modules/glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/globals": { + "version": "11.7.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.7.0.tgz", + "integrity": "sha512-K8BNSPySfeShBQXsahYB/AbbWruVOTyVpgoIDnl8odPpeSfP2J5QO2oLFFdl2j7GfDCtZj2bMKar2T49itTPCg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/globby": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-5.0.0.tgz", + "integrity": "sha1-69hGZ8oNuzMLmbz8aOrCvFQ3Dg0=", + "dev": true, + "dependencies": { + "array-union": "^1.0.1", + "arrify": "^1.0.0", + "glob": "^7.0.3", + "object-assign": "^4.0.1", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "node_modules/har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", + "engines": { + "node": ">=4" + } + }, + "node_modules/har-validator": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz", + "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=", + "deprecated": "this library is no longer supported", + "dependencies": { + "ajv": "^5.1.0", + "har-schema": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dev": true, + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hosted-git-info": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.7.1.tgz", + "integrity": "sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w==", + "dev": true + }, + "node_modules/html-entities": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-1.2.1.tgz", + "integrity": "sha1-DfKTUfByEWNRXfueVUPl9u7VFi8=", + "engines": [ + "node >= 0.4.0" + ] + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "dependencies": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + }, + "engines": { + "node": ">=0.8", + "npm": ">=1.3.7" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz", + "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" + }, + "node_modules/ignore": { + "version": "3.3.10", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.10.tgz", + "integrity": "sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==", + "dev": true + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/inquirer": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-3.3.0.tgz", + "integrity": "sha512-h+xtnyk4EwKvFWHrUYsWErEVR+igKtLdchu+o0Z1RL7VU/jVMFbYir2bp6bAj8efFNxWqHX0dIss6fJQ+/+qeQ==", + "dev": true, + "dependencies": { + "ansi-escapes": "^3.0.0", + "chalk": "^2.0.0", + "cli-cursor": "^2.1.0", + "cli-width": "^2.0.0", + "external-editor": "^2.0.4", + "figures": "^2.0.0", + "lodash": "^4.3.0", + "mute-stream": "0.0.7", + "run-async": "^2.2.0", + "rx-lite": "^4.0.8", + "rx-lite-aggregates": "^4.0.8", + "string-width": "^2.1.0", + "strip-ansi": "^4.0.0", + "through": "^2.3.6" + } + }, + "node_modules/is-arguments": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", + "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true + }, + "node_modules/is-bluebird": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-bluebird/-/is-bluebird-1.0.2.tgz", + "integrity": "sha1-CWQ5Bg9KpBGr7hkUOoTWpVNG1uI=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-builtin-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz", + "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=", + "dev": true, + "dependencies": { + "builtin-modules": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/is-generator-function": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", + "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-path-cwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz", + "integrity": "sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-path-in-cwd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-1.0.1.tgz", + "integrity": "sha512-FjV1RTW48E7CWM7eE/J2NJvAEEVektecDBVBE5Hh3nM1Jd0kvhHtX68Pr3xsDf857xt3Y4AkwVULK1Vku62aaQ==", + "dev": true, + "dependencies": { + "is-path-inside": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-path-inside": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz", + "integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=", + "dev": true, + "dependencies": { + "path-is-inside": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-promise": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", + "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=", + "dev": true + }, + "node_modules/is-resolvable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.1.0.tgz", + "integrity": "sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==", + "dev": true + }, + "node_modules/is-typed-array": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.10.tgz", + "integrity": "sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==", + "dependencies": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "node_modules/isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + }, + "node_modules/jmespath": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.16.0.tgz", + "integrity": "sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/js-tokens": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", + "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", + "dev": true + }, + "node_modules/js-yaml": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.12.0.tgz", + "integrity": "sha512-PIt2cnwmPfL4hKNwqeiuz4bKfnzHTBv6HyVgjahA6mPLwPDzjDWrplJBMjHUFxku/N3FlmrbyPclad+I+4mJ3A==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "optional": true + }, + "node_modules/json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" + }, + "node_modules/json-schema-traverse": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", + "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "dev": true + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + }, + "node_modules/json5": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", + "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=", + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "engines": [ + "node >=0.6.0" + ], + "dependencies": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, + "node_modules/levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "dev": true, + "dependencies": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/load-json-file": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", + "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "dev": true, + "dependencies": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/locate-path/node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/lodash": { + "version": "4.17.10", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz", + "integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==", + "dev": true + }, + "node_modules/lru-cache": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.3.tgz", + "integrity": "sha512-fFEhvcgzuIoJVUF8fYr5KR0YqxD238zgObTps31YdADwPPAp82a4M8TrckkWyx7ekNlf9aBcVn81cFwwXngrJA==", + "dev": true, + "dependencies": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.35.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.35.0.tgz", + "integrity": "sha512-JWT/IcCTsB0Io3AhWUMjRqucrHSPsSf2xKLaRldJVULioggvkJvggZ3VXNNSRkCddE6D+BUI4HEIZIA2OjwIvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.19", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.19.tgz", + "integrity": "sha512-P1tKYHVSZ6uFo26mtnve4HQFE3koh1UWVkp8YUC+ESBHe945xWSoXuHHiGarDqcEZ+whpCDnlNw5LON0kLo+sw==", + "dependencies": { + "mime-db": "~1.35.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", + "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "deprecated": "Legacy versions of mkdirp are no longer supported. Please update to mkdirp 1.x. (Note that the API surface has changed to use Promises in 1.x.)", + "dev": true, + "dependencies": { + "minimist": "0.0.8" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/mkdirp/node_modules/minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "node_modules/music-metadata": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/music-metadata/-/music-metadata-1.1.0.tgz", + "integrity": "sha512-HTIWhewHyLwVQByhh45BGtmxrEDpq4RI4ZwUKYBwj7uPheE+MmudlnRc22emq99GRLGmb16/Q7RgM+3O0Wi2wg==", + "dependencies": { + "bluebird": "^3.5.1", + "debug": "^3.1.0", + "fs-extra": "^6.0.1", + "strtok3": "^1.4.2", + "then-read-stream": "^1.1.3", + "token-types": "^0.9.4" + }, + "engines": { + "node": "*" + } + }, + "node_modules/music-metadata/node_modules/bluebird": { + "version": "3.5.5", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.5.tgz", + "integrity": "sha512-5am6HnnfN+urzt4yfg7IgTbotDjIT/u8AJpEt0sIU9FtXfVeezXAPKswrG+xKUCOYAINpSdgZVDU6QFh+cuH3w==" + }, + "node_modules/mute-stream": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", + "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=", + "dev": true + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "dev": true + }, + "node_modules/normalize-package-data": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz", + "integrity": "sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==", + "dev": true, + "dependencies": { + "hosted-git-info": "^2.1.4", + "is-builtin-module": "^1.0.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/oauth-sign": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", + "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=", + "engines": { + "node": "*" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", + "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", + "dev": true, + "dependencies": { + "mimic-fn": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/optionator": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", + "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", + "dev": true, + "dependencies": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.4", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "wordwrap": "~1.0.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/optionator/node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", + "dev": true + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dev": true, + "dependencies": { + "p-try": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "dev": true, + "dependencies": { + "p-limit": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", + "dev": true, + "dependencies": { + "error-ex": "^1.2.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", + "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", + "dev": true, + "dependencies": { + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", + "dev": true + }, + "node_modules/path-parse": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.5.tgz", + "integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=", + "dev": true + }, + "node_modules/path-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", + "integrity": "sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=", + "dev": true, + "dependencies": { + "pify": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "dev": true, + "dependencies": { + "pinkie": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pkg-dir": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-1.0.0.tgz", + "integrity": "sha1-ektQio1bstYp1EcFb/TpyTFM89Q=", + "dev": true, + "dependencies": { + "find-up": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pluralize": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-7.0.0.tgz", + "integrity": "sha512-ARhBOdzS3e41FbkW/XWrTEtukqqLoK5+Z/4UeDaLuSW+39JPeFgs4gCGqsrJHVZX0fUrx//4OF0K1CUGwlIFow==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", + "dev": true + }, + "node_modules/progress": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.0.tgz", + "integrity": "sha1-ihvjZr+Pwj2yvSPxDG/pILQ4nR8=", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "node_modules/pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", + "dev": true + }, + "node_modules/punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" + }, + "node_modules/qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", + "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.", + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/read-pkg": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", + "integrity": "sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=", + "dev": true, + "dependencies": { + "load-json-file": "^2.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/read-pkg-up": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz", + "integrity": "sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4=", + "dev": true, + "dependencies": { + "find-up": "^2.0.0", + "read-pkg": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/read-pkg-up/node_modules/find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true, + "dependencies": { + "locate-path": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/regexpp": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-1.1.0.tgz", + "integrity": "sha512-LOPw8FpgdQF9etWMaAfG/WRthIdXJGYp4mJ2Jgn/2lpkbod9jPn0t9UqN7AxBOKNfzRbYyVfgc7Vk4t/MpnXgw==", + "dev": true, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/request": { + "version": "2.87.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.87.0.tgz", + "integrity": "sha512-fcogkm7Az5bsS6Sl0sibkbhcKsnyon/jV1kF3ajGmF0c8HrttdKTPRT9hieOaQHA5HEq6r8OyWOo/o781C1tNw==", + "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", + "dependencies": { + "aws-sign2": "~0.7.0", + "aws4": "^1.6.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.5", + "extend": "~3.0.1", + "forever-agent": "~0.6.1", + "form-data": "~2.3.1", + "har-validator": "~5.0.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.17", + "oauth-sign": "~0.8.2", + "performance-now": "^2.1.0", + "qs": "~6.5.1", + "safe-buffer": "^5.1.1", + "tough-cookie": "~2.3.3", + "tunnel-agent": "^0.6.0", + "uuid": "^3.1.0" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/request-promise": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/request-promise/-/request-promise-1.0.2.tgz", + "integrity": "sha1-FV9BBgjZJXwInB0LJvjY96iqhqE=", + "deprecated": "request-promise has been deprecated because it extends the now deprecated request package, see https://github.com/request/request/issues/3142", + "dependencies": { + "bluebird": "^2.3", + "cls-bluebird": "^1.0.1", + "lodash": "^3.10.0", + "request": "^2.34" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/request-promise/node_modules/lodash": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", + "integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=" + }, + "node_modules/require-uncached": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/require-uncached/-/require-uncached-1.0.3.tgz", + "integrity": "sha1-Tg1W1slmL9MeQwEcS5WqSZVUIdM=", + "dev": true, + "dependencies": { + "caller-path": "^0.1.0", + "resolve-from": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.8.1.tgz", + "integrity": "sha512-AicPrAC7Qu1JxPCZ9ZgCZlY35QgFnNqc+0LtbRNxnVw4TXvjQ72wnuL9JQcEBgXkI9JM8MsT9kaQoHcpCRJOYA==", + "dev": true, + "dependencies": { + "path-parse": "^1.0.5" + } + }, + "node_modules/resolve-from": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-1.0.1.tgz", + "integrity": "sha1-Jsv+k10a7uq7Kbw/5a6wHpPUQiY=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/restore-cursor": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", + "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", + "dev": true, + "dependencies": { + "onetime": "^2.0.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/rimraf": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", + "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", + "dev": true, + "dependencies": { + "glob": "^7.0.5" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/run-async": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz", + "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=", + "dev": true, + "dependencies": { + "is-promise": "^2.1.0" + }, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/rx-lite": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/rx-lite/-/rx-lite-4.0.8.tgz", + "integrity": "sha1-Cx4Rr4vESDbwSmQH6S2kJGe3lEQ=", + "dev": true + }, + "node_modules/rx-lite-aggregates": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/rx-lite-aggregates/-/rx-lite-aggregates-4.0.8.tgz", + "integrity": "sha1-dTuHqJoRyVRnxKwWJsTvxOBcZ74=", + "dev": true, + "dependencies": { + "rx-lite": "*" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/sax": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", + "integrity": "sha1-e45lYZCyKOgaZq6nSEgNgozS03o=" + }, + "node_modules/semver": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", + "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "dev": true, + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/shimmer": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shimmer/-/shimmer-1.2.0.tgz", + "integrity": "sha512-xTCx2vohXC2EWWDqY/zb4+5Mu28D+HYNSOuFzsyRDRvI/e1ICb69afwaUwfjr+25ZXldbOLyp+iDUZHq8UnTag==" + }, + "node_modules/signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", + "dev": true + }, + "node_modules/slice-ansi": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-1.0.0.tgz", + "integrity": "sha512-POqxBK6Lb3q6s047D/XsDVNPnF9Dl8JSaqe9h9lURl0OdNqy/ujDrOiIHtsqXMGbWWTIomRzAMaTyawAU//Reg==", + "dev": true, + "dependencies": { + "is-fullwidth-code-point": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/sonos-discovery": { + "version": "1.8.0", + "resolved": "https://github.com/jishi/node-sonos-discovery/archive/v1.8.0.tar.gz", + "integrity": "sha512-ZDJoW3kcDYMEfGwaxyyk1Tg+Max/FewyLU4YEKaEhBrf7df3eAdrhJADRsZy5UmmKT/ZxavMiShx1IUjE/PYUw==", + "dependencies": { + "html-entities": "1.0.x", + "xml-flow": "1.0.2" + }, + "engines": { + "node": ">=4 <23" + } + }, + "node_modules/sonos-discovery/node_modules/html-entities": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-1.0.10.tgz", + "integrity": "sha1-DepZEw3VDfFU6CxM9x0Iwsnclos=", + "engines": [ + "node >= 0.4.0" + ] + }, + "node_modules/spdx-correct": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.0.0.tgz", + "integrity": "sha512-N19o9z5cEyc8yQQPukRCZ9EUmb4HUpnrmaL/fxS2pBo2jbfcFRVuFZ/oFC+vZz0MNNk0h80iMn5/S6qGZOL5+g==", + "dev": true, + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.1.0.tgz", + "integrity": "sha512-4K1NsmrlCU1JJgUrtgEeTVyfx8VaYea9J9LvARxhbHtVtohPs/gFGG5yy49beySjlIMhhXZ4QqujIZEfS4l6Cg==", + "dev": true + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", + "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", + "dev": true, + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.0.tgz", + "integrity": "sha512-2+EPwgbnmOIl8HjGBXXMd9NAu02vLjOO1nWw4kmeRDFyHn+M/ETfHxQUK0oXg8ctgVnl9t3rosNVsZ1jG61nDA==", + "dev": true + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "node_modules/sshpk": { + "version": "1.14.2", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.2.tgz", + "integrity": "sha1-xvxhZIo9nE52T9P8306hBeSSupg=", + "dependencies": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "dashdash": "^1.12.0", + "getpass": "^0.1.1", + "safer-buffer": "^2.0.2" + }, + "bin": { + "sshpk-conv": "bin/sshpk-conv", + "sshpk-sign": "bin/sshpk-sign", + "sshpk-verify": "bin/sshpk-verify" + }, + "engines": { + "node": ">=0.10.0" + }, + "optionalDependencies": { + "bcrypt-pbkdf": "^1.0.0", + "ecc-jsbn": "~0.1.1", + "jsbn": "~0.1.0", + "tweetnacl": "~0.14.0" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "dependencies": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "dependencies": { + "ansi-regex": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-ansi/node_modules/ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strtok3": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-1.6.0.tgz", + "integrity": "sha512-BDr7b5RJSfvX1wBjJDwpJOGZ0EhfhZaMCm+YaOVAcvFgo8/+nJq/Vurzh7JykAgiW+EOo5kkrfVffBFFp3u8TQ==", + "dependencies": { + "debug": "^3.1.0", + "es6-promise": "^4.2.4", + "then-read-stream": "^1.2.1", + "token-types": "^0.10.0" + }, + "engines": { + "node": ">=0.1.98" + } + }, + "node_modules/strtok3/node_modules/token-types": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-0.10.0.tgz", + "integrity": "sha512-26A0816VoHW8h64OT47dcclsH5M9iwo9zi3KoCPz1NrKoI9T2dlVkisgzTaGK4dPjCbP3ugf7cYqhWOiAzVHgw==", + "engines": { + "node": ">=0.1.98" + } + }, + "node_modules/supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/table": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/table/-/table-4.0.2.tgz", + "integrity": "sha512-UUkEAPdSGxtRpiV9ozJ5cMTtYiqz7Ni1OGqLXRCynrvzdtR1p+cfOWe2RJLwvUG8hNanaSRjecIqwOjqeatDsA==", + "dev": true, + "dependencies": { + "ajv": "^5.2.3", + "ajv-keywords": "^2.1.0", + "chalk": "^2.1.0", + "lodash": "^4.17.4", + "slice-ansi": "1.0.0", + "string-width": "^2.1.1" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, + "node_modules/then-read-stream": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/then-read-stream/-/then-read-stream-1.5.1.tgz", + "integrity": "sha512-I+iiemYWhp1ysJQEioqpEICgvHlqHS5WrQGZkboFLs7Jm350Kvq4cN3qRCzHpETUuq5+NsdrdWEg6M0NFxtwtQ==", + "deprecated": "Package renamed to peak-readable.", + "engines": { + "node": ">=8" + } + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true + }, + "node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/token-types": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-0.9.4.tgz", + "integrity": "sha512-KSl/Q1GJ4FoxbqKCLhTiIowVzom2cP0fgWGXKsJupbJQqfnCDmxkdMopIrt+y5Ak6YAGdy9TKpplWDioaBsbEw==", + "engines": { + "node": ">=0.1.98" + } + }, + "node_modules/tough-cookie": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz", + "integrity": "sha512-TZ6TTfI5NtZnuyy/Kecv+CnoROnyXn2DN97LontgQpCwsX2XyLYCC0ENhYkehSOwAp8rTQKc/NUIF7BkQ5rKLA==", + "dependencies": { + "punycode": "^1.4.1" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "optional": true + }, + "node_modules/type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "dev": true, + "dependencies": { + "prelude-ls": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", + "dev": true + }, + "node_modules/underscore": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.9.1.tgz", + "integrity": "sha512-5/4etnCkd9c8gwgowi5/om/mYO5ajCaOgdzj/oW+0eQV9WxKBDZw5+ycmKmeaTXjInS/W0BzpGLo2xR2aBwZdg==" + }, + "node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/url": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", + "integrity": "sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ=", + "dependencies": { + "punycode": "1.3.2", + "querystring": "0.2.0" + } + }, + "node_modules/url/node_modules/punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=" + }, + "node_modules/util": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "dependencies": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + }, + "node_modules/uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "bin": { + "uuid": "bin/uuid" + } + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.3.tgz", + "integrity": "sha512-63ZOUnL4SIXj4L0NixR3L1lcjO38crAbgrTpl28t8jjrfuiOBL5Iygm+60qPs/KsZGzPNg6Smnc/oY16QTjF0g==", + "dev": true, + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "engines": [ + "node >=0.6.0" + ], + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "node_modules/wav-file-info": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/wav-file-info/-/wav-file-info-0.0.8.tgz", + "integrity": "sha1-aAp160w0a34/RX55AqexmDUG5N4=" + }, + "node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.9.tgz", + "integrity": "sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==", + "dependencies": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.0", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "node_modules/write": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/write/-/write-0.2.1.tgz", + "integrity": "sha1-X8A4KOJkzqP+kUVUdvejxWbLB1c=", + "dev": true, + "dependencies": { + "mkdirp": "^0.5.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/xml-flow": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/xml-flow/-/xml-flow-1.0.2.tgz", + "integrity": "sha512-TfV52Su2083n8w61l1hGI5K3ZAkn4hmaon3oJHc+TRQo0QkKLMPAPdMpGMJd+5Aik6ZUicPDjBINO8QxLkZGgg==", + "dependencies": { + "sax": "^1.2.4" + } + }, + "node_modules/xml-flow/node_modules/sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" + }, + "node_modules/xml2js": { + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz", + "integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~9.0.1" + } + }, + "node_modules/xmlbuilder": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", + "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", + "dev": true + } + }, + "dependencies": { + "acorn": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.1.tgz", + "integrity": "sha512-d+nbxBUGKg7Arpsvbnlq61mc12ek3EY8EQldM3GPAhWJ1UVxC6TDGbIvUMNU6obBX3i1+ptCIzV4vq0gFPEGVQ==", + "dev": true + }, + "acorn-jsx": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-3.0.1.tgz", + "integrity": "sha1-r9+UiPsezvyDSPb7IvRk4ypYs2s=", + "dev": true, + "requires": { + "acorn": "^3.0.4" + }, + "dependencies": { + "acorn": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz", + "integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo=", + "dev": true + } + } + }, + "ajv": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", + "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", + "requires": { + "co": "^4.6.0", + "fast-deep-equal": "^1.0.0", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.3.0" + } + }, + "ajv-keywords": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-2.1.1.tgz", + "integrity": "sha1-YXmX/F9gV2iUxDX5QNgZ4TW4B2I=", + "dev": true, + "requires": {} + }, + "anesidora": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/anesidora/-/anesidora-1.2.1.tgz", + "integrity": "sha1-/ZWdrLiPx6im5xE+OA8/rQmTipo=", + "requires": { + "request": "^2.64.0", + "underscore": "^1.8.3" + } + }, + "ansi-escapes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.1.0.tgz", + "integrity": "sha512-UgAb8H9D41AQnu/PbWlCofQVcnV4Gs2bBJi9eZPxfU/hgglFh3SMDMENRIqdr7H6XFnXdoknctFByVsCOotTVw==", + "dev": true + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "array-union": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", + "dev": true, + "requires": { + "array-uniq": "^1.0.1" + } + }, + "array-uniq": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", + "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", + "dev": true + }, + "arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", + "dev": true + }, + "asn1": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", + "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=" + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "available-typed-arrays": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", + "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==" + }, + "aws-sdk": { + "version": "2.1299.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1299.0.tgz", + "integrity": "sha512-xTh6pmCUEJljkFfTM3sE8UozDxal80uX/5WZl8GcjQ+NbrGeQEdvL6wFWBwEEVbhR0VBVuU37cKPuQlfENbRYA==", + "requires": { + "buffer": "4.9.2", + "events": "1.1.1", + "ieee754": "1.1.13", + "jmespath": "0.16.0", + "querystring": "0.2.0", + "sax": "1.2.1", + "url": "0.10.3", + "util": "^0.12.4", + "uuid": "8.0.0", + "xml2js": "0.4.19" + }, + "dependencies": { + "uuid": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.0.0.tgz", + "integrity": "sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==" + } + } + }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" + }, + "aws4": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.7.0.tgz", + "integrity": "sha512-32NDda82rhwD9/JBCCkB+MRYDp0oSvlo2IL6rQWA10PQi7tDUM3eqMSltXmY+Oyl/7N3P3qNtAlv7X0d9bI28w==" + }, + "axios": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz", + "integrity": "sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==", + "requires": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + }, + "dependencies": { + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + } + } + }, + "babel-code-frame": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", + "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", + "dev": true, + "requires": { + "chalk": "^1.1.3", + "esutils": "^2.0.2", + "js-tokens": "^3.0.2" + }, + "dependencies": { + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + } + } + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" + }, + "basic-auth": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-1.1.0.tgz", + "integrity": "sha1-RSIe5Cn37h5QNb4/UVM/HN/SmIQ=" + }, + "bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "optional": true, + "requires": { + "tweetnacl": "^0.14.3" + } + }, + "bluebird": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-2.11.0.tgz", + "integrity": "sha1-U0uQM8AiyVecVro7Plpcqvu2UOE=" + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "buffer": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", + "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", + "dev": true + }, + "builtin-modules": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", + "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", + "dev": true + }, + "call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + } + }, + "caller-path": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-0.1.0.tgz", + "integrity": "sha1-lAhe9jWB7NPaqSREqP6U6CV3dR8=", + "dev": true, + "requires": { + "callsites": "^0.2.0" + } + }, + "callsites": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-0.2.0.tgz", + "integrity": "sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo=", + "dev": true + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + }, + "chalk": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", + "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "supports-color": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", + "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "chardet": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.4.2.tgz", + "integrity": "sha1-tUc7M9yXxCTl2Y3IfVXU2KKci/I=", + "dev": true + }, + "circular-json": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.3.3.tgz", + "integrity": "sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==", + "dev": true + }, + "cli-cursor": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", + "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", + "dev": true, + "requires": { + "restore-cursor": "^2.0.0" + } + }, + "cli-width": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", + "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=", + "dev": true + }, + "cls-bluebird": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/cls-bluebird/-/cls-bluebird-1.1.3.tgz", + "integrity": "sha1-syY8EaCJsDlhhaG3q5BNkPAq1Cg=", + "requires": { + "is-bluebird": "^1.0.1", + "shimmer": "^1.1.0" + } + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" + }, + "color-convert": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.2.tgz", + "integrity": "sha512-3NUJZdhMhcdPn8vJ9v2UQJoH0qqoGUkYTgFEPZaPjEtwmmKUfNV46zZmgB2M5M4DCEQHMaCfWHCxiBflLm04Tg==", + "dev": true, + "requires": { + "color-name": "1.1.1" + } + }, + "color-name": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.1.tgz", + "integrity": "sha1-SxQVMEz1ACjqgWQ2Q72C6gWANok=", + "dev": true + }, + "combined-stream": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", + "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "contains-path": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/contains-path/-/contains-path-0.1.0.tgz", + "integrity": "sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo=", + "dev": true + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "cross-spawn": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", + "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", + "dev": true, + "requires": { + "lru-cache": "^4.0.1", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "requires": { + "assert-plus": "^1.0.0" + } + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + }, + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "dev": true + }, + "del": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/del/-/del-2.2.2.tgz", + "integrity": "sha1-wSyYHQZ4RshLyvhiz/kw2Qf/0ag=", + "dev": true, + "requires": { + "globby": "^5.0.0", + "is-path-cwd": "^1.0.0", + "is-path-in-cwd": "^1.0.0", + "object-assign": "^4.0.1", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0", + "rimraf": "^2.2.8" + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + }, + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" + }, + "destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==" + }, + "doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "optional": true, + "requires": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "elevenlabs-node": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/elevenlabs-node/-/elevenlabs-node-2.0.1.tgz", + "integrity": "sha512-q6vUznhudS1yxYxTVP3sT3xWzSzoXH+y3T+4GE1X8gjGwJw1Inmko/paQfl4d+CpTQXGNWuACAlFUdIk96xebw==", + "requires": { + "axios": "^1.4.0", + "fs-extra": "^11.1.1" + }, + "dependencies": { + "fs-extra": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", + "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, + "jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" + } + }, + "universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==" + } + } + }, + "encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==" + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "es6-promise": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==" + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "eslint": { + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-4.19.1.tgz", + "integrity": "sha512-bT3/1x1EbZB7phzYu7vCr1v3ONuzDtX8WjuM9c0iYxe+cq+pwcKEoQjl7zd3RpC6YOLgnSy3cTN58M2jcoPDIQ==", + "dev": true, + "requires": { + "ajv": "^5.3.0", + "babel-code-frame": "^6.22.0", + "chalk": "^2.1.0", + "concat-stream": "^1.6.0", + "cross-spawn": "^5.1.0", + "debug": "^3.1.0", + "doctrine": "^2.1.0", + "eslint-scope": "^3.7.1", + "eslint-visitor-keys": "^1.0.0", + "espree": "^3.5.4", + "esquery": "^1.0.0", + "esutils": "^2.0.2", + "file-entry-cache": "^2.0.0", + "functional-red-black-tree": "^1.0.1", + "glob": "^7.1.2", + "globals": "^11.0.1", + "ignore": "^3.3.3", + "imurmurhash": "^0.1.4", + "inquirer": "^3.0.6", + "is-resolvable": "^1.0.0", + "js-yaml": "^3.9.1", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.3.0", + "lodash": "^4.17.4", + "minimatch": "^3.0.2", + "mkdirp": "^0.5.1", + "natural-compare": "^1.4.0", + "optionator": "^0.8.2", + "path-is-inside": "^1.0.2", + "pluralize": "^7.0.0", + "progress": "^2.0.0", + "regexpp": "^1.0.1", + "require-uncached": "^1.0.3", + "semver": "^5.3.0", + "strip-ansi": "^4.0.0", + "strip-json-comments": "~2.0.1", + "table": "4.0.2", + "text-table": "~0.2.0" + } + }, + "eslint-config-airbnb-base": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-12.1.0.tgz", + "integrity": "sha512-/vjm0Px5ZCpmJqnjIzcFb9TKZrKWz0gnuG/7Gfkt0Db1ELJR51xkZth+t14rYdqWgX836XbuxtArbIHlVhbLBA==", + "dev": true, + "requires": { + "eslint-restricted-globals": "^0.1.1" + } + }, + "eslint-import-resolver-node": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.2.tgz", + "integrity": "sha512-sfmTqJfPSizWu4aymbPr4Iidp5yKm8yDkHp+Ir3YiTHiiDfxh69mOUsmiqW6RZ9zRXFaF64GtYmN7e+8GHBv6Q==", + "dev": true, + "requires": { + "debug": "^2.6.9", + "resolve": "^1.5.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + } + } + }, + "eslint-module-utils": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.2.0.tgz", + "integrity": "sha1-snA2LNiLGkitMIl2zn+lTphBF0Y=", + "dev": true, + "requires": { + "debug": "^2.6.8", + "pkg-dir": "^1.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + } + } + }, + "eslint-plugin-import": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.13.0.tgz", + "integrity": "sha512-t6hGKQDMIt9N8R7vLepsYXgDfeuhp6ZJSgtrLEDxonpSubyxUZHjhm6LsAaZX8q6GYVxkbT3kTsV9G5mBCFR6A==", + "dev": true, + "requires": { + "contains-path": "^0.1.0", + "debug": "^2.6.8", + "doctrine": "1.5.0", + "eslint-import-resolver-node": "^0.3.1", + "eslint-module-utils": "^2.2.0", + "has": "^1.0.1", + "lodash": "^4.17.4", + "minimatch": "^3.0.3", + "read-pkg-up": "^2.0.0", + "resolve": "^1.6.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "doctrine": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-1.5.0.tgz", + "integrity": "sha1-N53Ocw9hZvds76TmcHoVmwLFpvo=", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "isarray": "^1.0.0" + } + } + } + }, + "eslint-restricted-globals": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/eslint-restricted-globals/-/eslint-restricted-globals-0.1.1.tgz", + "integrity": "sha1-NfDVy8ZMLj7WLpO0saevBbp+1Nc=", + "dev": true + }, + "eslint-scope": { + "version": "3.7.3", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-3.7.3.tgz", + "integrity": "sha512-W+B0SvF4gamyCTmUc+uITPY0989iXVfKvhwtmJocTaYoc/3khEHmEmvfY/Gn9HA9VV75jrQECsHizkNw1b68FA==", + "dev": true, + "requires": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + } + }, + "eslint-visitor-keys": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", + "integrity": "sha512-qzm/XxIbxm/FHyH341ZrbnMUpe+5Bocte9xkmFMzPMjRaZMcXww+MpBptFvtU+79L362nqiLhekCxCxDPaUMBQ==", + "dev": true + }, + "espree": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/espree/-/espree-3.5.4.tgz", + "integrity": "sha512-yAcIQxtmMiB/jL32dzEp2enBeidsB7xWPLNiw3IIkpVds1P+h7qF9YwJq1yUNzp2OKXgAprs4F61ih66UsoD1A==", + "dev": true, + "requires": { + "acorn": "^5.5.0", + "acorn-jsx": "^3.0.0" + } + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, + "esquery": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.0.1.tgz", + "integrity": "sha512-SmiyZ5zIWH9VM+SRUReLS5Q8a7GxtRdxEBVZpm98rJM7Sb+A9DVCndXfkeFUd3byderg+EbDkfnevfCwynWaNA==", + "dev": true, + "requires": { + "estraverse": "^4.0.0" + } + }, + "esrecurse": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", + "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", + "dev": true, + "requires": { + "estraverse": "^4.1.0" + } + }, + "estraverse": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", + "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=", + "dev": true + }, + "esutils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", + "dev": true + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" + }, + "events": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", + "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=" + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "external-editor": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-2.2.0.tgz", + "integrity": "sha512-bSn6gvGxKt+b7+6TKEv1ZycHleA7aHhRHyAqJyp5pbUFuYYNIzpZnQDk7AsYckyWdEnTeAnay0aCy2aV6iTk9A==", + "dev": true, + "requires": { + "chardet": "^0.4.0", + "iconv-lite": "^0.4.17", + "tmp": "^0.0.33" + } + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" + }, + "fast-deep-equal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", + "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=" + }, + "fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "figures": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", + "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5" + } + }, + "file-entry-cache": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-2.0.0.tgz", + "integrity": "sha1-w5KZDD5oR4PYOLjISkXYoEhFg2E=", + "dev": true, + "requires": { + "flat-cache": "^1.2.1", + "object-assign": "^4.0.1" + } + }, + "find-up": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", + "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", + "dev": true, + "requires": { + "path-exists": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + }, + "flat-cache": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-1.3.0.tgz", + "integrity": "sha1-0wMLMrOBVPTjt+nHCfSQ9++XxIE=", + "dev": true, + "requires": { + "circular-json": "^0.3.1", + "del": "^2.0.2", + "graceful-fs": "^4.1.2", + "write": "^0.2.1" + } + }, + "follow-redirects": { + "version": "1.15.3", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz", + "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==" + }, + "for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "requires": { + "is-callable": "^1.1.3" + } + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" + }, + "form-data": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.2.tgz", + "integrity": "sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "1.0.6", + "mime-types": "^2.1.12" + } + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==" + }, + "fs-extra": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-6.0.1.tgz", + "integrity": "sha512-GnyIkKhhzXZUWFCaJzvyDLEEgDkPfb4/TPvJCJVuS8MWZgoSsErf++QpiAlDnKFcqhRlm+tIOcencCjyJE6ZCA==", + "requires": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", + "dev": true + }, + "fuse.js": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-6.4.1.tgz", + "integrity": "sha512-+hAS7KYgLXontDh/vqffs7wIBw0ceb9Sx8ywZQhOsiQGcSO5zInGhttWOUYQYlvV/yYMJOacQ129Xs3mP3+oZQ==" + }, + "get-intrinsic": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz", + "integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==", + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" + } + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "requires": { + "assert-plus": "^1.0.0" + } + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "globals": { + "version": "11.7.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.7.0.tgz", + "integrity": "sha512-K8BNSPySfeShBQXsahYB/AbbWruVOTyVpgoIDnl8odPpeSfP2J5QO2oLFFdl2j7GfDCtZj2bMKar2T49itTPCg==", + "dev": true + }, + "globby": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-5.0.0.tgz", + "integrity": "sha1-69hGZ8oNuzMLmbz8aOrCvFQ3Dg0=", + "dev": true, + "requires": { + "array-union": "^1.0.1", + "arrify": "^1.0.0", + "glob": "^7.0.3", + "object-assign": "^4.0.1", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + }, + "gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "requires": { + "get-intrinsic": "^1.1.3" + } + }, + "graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" + }, + "har-validator": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz", + "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=", + "requires": { + "ajv": "^5.1.0", + "har-schema": "^2.0.0" + } + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" + }, + "has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "requires": { + "has-symbols": "^1.0.2" + } + }, + "hosted-git-info": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.7.1.tgz", + "integrity": "sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w==", + "dev": true + }, + "html-entities": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-1.2.1.tgz", + "integrity": "sha1-DfKTUfByEWNRXfueVUPl9u7VFi8=" + }, + "http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "requires": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + } + }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, + "iconv-lite": { + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz", + "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ieee754": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" + }, + "ignore": { + "version": "3.3.10", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.10.tgz", + "integrity": "sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==", + "dev": true + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "inquirer": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-3.3.0.tgz", + "integrity": "sha512-h+xtnyk4EwKvFWHrUYsWErEVR+igKtLdchu+o0Z1RL7VU/jVMFbYir2bp6bAj8efFNxWqHX0dIss6fJQ+/+qeQ==", + "dev": true, + "requires": { + "ansi-escapes": "^3.0.0", + "chalk": "^2.0.0", + "cli-cursor": "^2.1.0", + "cli-width": "^2.0.0", + "external-editor": "^2.0.4", + "figures": "^2.0.0", + "lodash": "^4.3.0", + "mute-stream": "0.0.7", + "run-async": "^2.2.0", + "rx-lite": "^4.0.8", + "rx-lite-aggregates": "^4.0.8", + "string-width": "^2.1.0", + "strip-ansi": "^4.0.0", + "through": "^2.3.6" + } + }, + "is-arguments": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", + "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true + }, + "is-bluebird": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-bluebird/-/is-bluebird-1.0.2.tgz", + "integrity": "sha1-CWQ5Bg9KpBGr7hkUOoTWpVNG1uI=" + }, + "is-builtin-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz", + "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=", + "dev": true, + "requires": { + "builtin-modules": "^1.0.0" + } + }, + "is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==" + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "is-generator-function": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", + "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-path-cwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz", + "integrity": "sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0=", + "dev": true + }, + "is-path-in-cwd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-1.0.1.tgz", + "integrity": "sha512-FjV1RTW48E7CWM7eE/J2NJvAEEVektecDBVBE5Hh3nM1Jd0kvhHtX68Pr3xsDf857xt3Y4AkwVULK1Vku62aaQ==", + "dev": true, + "requires": { + "is-path-inside": "^1.0.0" + } + }, + "is-path-inside": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz", + "integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=", + "dev": true, + "requires": { + "path-is-inside": "^1.0.1" + } + }, + "is-promise": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", + "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=", + "dev": true + }, + "is-resolvable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.1.0.tgz", + "integrity": "sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==", + "dev": true + }, + "is-typed-array": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.10.tgz", + "integrity": "sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==", + "requires": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.0" + } + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + }, + "jmespath": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.16.0.tgz", + "integrity": "sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==" + }, + "js-tokens": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", + "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", + "dev": true + }, + "js-yaml": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.12.0.tgz", + "integrity": "sha512-PIt2cnwmPfL4hKNwqeiuz4bKfnzHTBv6HyVgjahA6mPLwPDzjDWrplJBMjHUFxku/N3FlmrbyPclad+I+4mJ3A==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "optional": true + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" + }, + "json-schema-traverse": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", + "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=" + }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "dev": true + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + }, + "json5": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", + "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=" + }, + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "requires": { + "graceful-fs": "^4.1.6" + } + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, + "load-json-file": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", + "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "strip-bom": "^3.0.0" + } + }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "dev": true, + "requires": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + }, + "dependencies": { + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + } + } + }, + "lodash": { + "version": "4.17.10", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz", + "integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==", + "dev": true + }, + "lru-cache": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.3.tgz", + "integrity": "sha512-fFEhvcgzuIoJVUF8fYr5KR0YqxD238zgObTps31YdADwPPAp82a4M8TrckkWyx7ekNlf9aBcVn81cFwwXngrJA==", + "dev": true, + "requires": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" + }, + "mime-db": { + "version": "1.35.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.35.0.tgz", + "integrity": "sha512-JWT/IcCTsB0Io3AhWUMjRqucrHSPsSf2xKLaRldJVULioggvkJvggZ3VXNNSRkCddE6D+BUI4HEIZIA2OjwIvg==" + }, + "mime-types": { + "version": "2.1.19", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.19.tgz", + "integrity": "sha512-P1tKYHVSZ6uFo26mtnve4HQFE3koh1UWVkp8YUC+ESBHe945xWSoXuHHiGarDqcEZ+whpCDnlNw5LON0kLo+sw==", + "requires": { + "mime-db": "~1.35.0" + } + }, + "mimic-fn": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", + "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + }, + "dependencies": { + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + } + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "music-metadata": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/music-metadata/-/music-metadata-1.1.0.tgz", + "integrity": "sha512-HTIWhewHyLwVQByhh45BGtmxrEDpq4RI4ZwUKYBwj7uPheE+MmudlnRc22emq99GRLGmb16/Q7RgM+3O0Wi2wg==", + "requires": { + "bluebird": "^3.5.1", + "debug": "^3.1.0", + "fs-extra": "^6.0.1", + "strtok3": "^1.4.2", + "then-read-stream": "^1.1.3", + "token-types": "^0.9.4" + }, + "dependencies": { + "bluebird": { + "version": "3.5.5", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.5.tgz", + "integrity": "sha512-5am6HnnfN+urzt4yfg7IgTbotDjIT/u8AJpEt0sIU9FtXfVeezXAPKswrG+xKUCOYAINpSdgZVDU6QFh+cuH3w==" + } + } + }, + "mute-stream": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", + "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=", + "dev": true + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "dev": true + }, + "normalize-package-data": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz", + "integrity": "sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==", + "dev": true, + "requires": { + "hosted-git-info": "^2.1.4", + "is-builtin-module": "^1.0.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "oauth-sign": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", + "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=" + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "dev": true + }, + "on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "requires": { + "ee-first": "1.1.1" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "onetime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", + "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", + "dev": true, + "requires": { + "mimic-fn": "^1.0.0" + } + }, + "optionator": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", + "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", + "dev": true, + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.4", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "wordwrap": "~1.0.0" + }, + "dependencies": { + "wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", + "dev": true + } + } + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "dev": true + }, + "p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dev": true, + "requires": { + "p-try": "^1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "dev": true, + "requires": { + "p-limit": "^1.1.0" + } + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", + "dev": true + }, + "parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", + "dev": true, + "requires": { + "error-ex": "^1.2.0" + } + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + }, + "path-exists": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", + "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", + "dev": true, + "requires": { + "pinkie-promise": "^2.0.0" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", + "dev": true + }, + "path-parse": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.5.tgz", + "integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=", + "dev": true + }, + "path-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", + "integrity": "sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=", + "dev": true, + "requires": { + "pify": "^2.0.0" + } + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + }, + "pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", + "dev": true + }, + "pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "dev": true, + "requires": { + "pinkie": "^2.0.0" + } + }, + "pkg-dir": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-1.0.0.tgz", + "integrity": "sha1-ektQio1bstYp1EcFb/TpyTFM89Q=", + "dev": true, + "requires": { + "find-up": "^1.0.0" + } + }, + "pluralize": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-7.0.0.tgz", + "integrity": "sha512-ARhBOdzS3e41FbkW/XWrTEtukqqLoK5+Z/4UeDaLuSW+39JPeFgs4gCGqsrJHVZX0fUrx//4OF0K1CUGwlIFow==", + "dev": true + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "dev": true + }, + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", + "dev": true + }, + "progress": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.0.tgz", + "integrity": "sha1-ihvjZr+Pwj2yvSPxDG/pILQ4nR8=", + "dev": true + }, + "proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", + "dev": true + }, + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" + }, + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" + }, + "querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" + }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" + }, + "read-pkg": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", + "integrity": "sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=", + "dev": true, + "requires": { + "load-json-file": "^2.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^2.0.0" + } + }, + "read-pkg-up": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz", + "integrity": "sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4=", + "dev": true, + "requires": { + "find-up": "^2.0.0", + "read-pkg": "^2.0.0" + }, + "dependencies": { + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true, + "requires": { + "locate-path": "^2.0.0" + } + } + } + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "regexpp": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-1.1.0.tgz", + "integrity": "sha512-LOPw8FpgdQF9etWMaAfG/WRthIdXJGYp4mJ2Jgn/2lpkbod9jPn0t9UqN7AxBOKNfzRbYyVfgc7Vk4t/MpnXgw==", + "dev": true + }, + "request": { + "version": "2.87.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.87.0.tgz", + "integrity": "sha512-fcogkm7Az5bsS6Sl0sibkbhcKsnyon/jV1kF3ajGmF0c8HrttdKTPRT9hieOaQHA5HEq6r8OyWOo/o781C1tNw==", + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.6.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.5", + "extend": "~3.0.1", + "forever-agent": "~0.6.1", + "form-data": "~2.3.1", + "har-validator": "~5.0.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.17", + "oauth-sign": "~0.8.2", + "performance-now": "^2.1.0", + "qs": "~6.5.1", + "safe-buffer": "^5.1.1", + "tough-cookie": "~2.3.3", + "tunnel-agent": "^0.6.0", + "uuid": "^3.1.0" + } + }, + "request-promise": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/request-promise/-/request-promise-1.0.2.tgz", + "integrity": "sha1-FV9BBgjZJXwInB0LJvjY96iqhqE=", + "requires": { + "bluebird": "^2.3", + "cls-bluebird": "^1.0.1", + "lodash": "^3.10.0", + "request": "^2.34" + }, + "dependencies": { + "lodash": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", + "integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=" + } + } + }, + "require-uncached": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/require-uncached/-/require-uncached-1.0.3.tgz", + "integrity": "sha1-Tg1W1slmL9MeQwEcS5WqSZVUIdM=", + "dev": true, + "requires": { + "caller-path": "^0.1.0", + "resolve-from": "^1.0.0" + } + }, + "resolve": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.8.1.tgz", + "integrity": "sha512-AicPrAC7Qu1JxPCZ9ZgCZlY35QgFnNqc+0LtbRNxnVw4TXvjQ72wnuL9JQcEBgXkI9JM8MsT9kaQoHcpCRJOYA==", + "dev": true, + "requires": { + "path-parse": "^1.0.5" + } + }, + "resolve-from": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-1.0.1.tgz", + "integrity": "sha1-Jsv+k10a7uq7Kbw/5a6wHpPUQiY=", + "dev": true + }, + "restore-cursor": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", + "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", + "dev": true, + "requires": { + "onetime": "^2.0.0", + "signal-exit": "^3.0.2" + } + }, + "rimraf": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", + "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", + "dev": true, + "requires": { + "glob": "^7.0.5" + } + }, + "run-async": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz", + "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=", + "dev": true, + "requires": { + "is-promise": "^2.1.0" + } + }, + "rx-lite": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/rx-lite/-/rx-lite-4.0.8.tgz", + "integrity": "sha1-Cx4Rr4vESDbwSmQH6S2kJGe3lEQ=", + "dev": true + }, + "rx-lite-aggregates": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/rx-lite-aggregates/-/rx-lite-aggregates-4.0.8.tgz", + "integrity": "sha1-dTuHqJoRyVRnxKwWJsTvxOBcZ74=", + "dev": true, + "requires": { + "rx-lite": "*" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "sax": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", + "integrity": "sha1-e45lYZCyKOgaZq6nSEgNgozS03o=" + }, + "semver": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", + "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", + "dev": true + }, + "send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "requires": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + }, + "dependencies": { + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + } + } + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + } + } + }, + "serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "requires": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + } + }, + "setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "dev": true, + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "dev": true + }, + "shimmer": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shimmer/-/shimmer-1.2.0.tgz", + "integrity": "sha512-xTCx2vohXC2EWWDqY/zb4+5Mu28D+HYNSOuFzsyRDRvI/e1ICb69afwaUwfjr+25ZXldbOLyp+iDUZHq8UnTag==" + }, + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", + "dev": true + }, + "slice-ansi": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-1.0.0.tgz", + "integrity": "sha512-POqxBK6Lb3q6s047D/XsDVNPnF9Dl8JSaqe9h9lURl0OdNqy/ujDrOiIHtsqXMGbWWTIomRzAMaTyawAU//Reg==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0" + } + }, + "sonos-discovery": { + "version": "https://github.com/jishi/node-sonos-discovery/archive/v1.8.0.tar.gz", + "integrity": "sha512-ZDJoW3kcDYMEfGwaxyyk1Tg+Max/FewyLU4YEKaEhBrf7df3eAdrhJADRsZy5UmmKT/ZxavMiShx1IUjE/PYUw==", + "requires": { + "html-entities": "1.0.x", + "xml-flow": "1.0.2" + }, + "dependencies": { + "html-entities": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-1.0.10.tgz", + "integrity": "sha1-DepZEw3VDfFU6CxM9x0Iwsnclos=" + } + } + }, + "spdx-correct": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.0.0.tgz", + "integrity": "sha512-N19o9z5cEyc8yQQPukRCZ9EUmb4HUpnrmaL/fxS2pBo2jbfcFRVuFZ/oFC+vZz0MNNk0h80iMn5/S6qGZOL5+g==", + "dev": true, + "requires": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.1.0.tgz", + "integrity": "sha512-4K1NsmrlCU1JJgUrtgEeTVyfx8VaYea9J9LvARxhbHtVtohPs/gFGG5yy49beySjlIMhhXZ4QqujIZEfS4l6Cg==", + "dev": true + }, + "spdx-expression-parse": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", + "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", + "dev": true, + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.0.tgz", + "integrity": "sha512-2+EPwgbnmOIl8HjGBXXMd9NAu02vLjOO1nWw4kmeRDFyHn+M/ETfHxQUK0oXg8ctgVnl9t3rosNVsZ1jG61nDA==", + "dev": true + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "sshpk": { + "version": "1.14.2", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.2.tgz", + "integrity": "sha1-xvxhZIo9nE52T9P8306hBeSSupg=", + "requires": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + } + }, + "statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + } + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "dev": true + }, + "strtok3": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-1.6.0.tgz", + "integrity": "sha512-BDr7b5RJSfvX1wBjJDwpJOGZ0EhfhZaMCm+YaOVAcvFgo8/+nJq/Vurzh7JykAgiW+EOo5kkrfVffBFFp3u8TQ==", + "requires": { + "debug": "^3.1.0", + "es6-promise": "^4.2.4", + "then-read-stream": "^1.2.1", + "token-types": "^0.10.0" + }, + "dependencies": { + "token-types": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-0.10.0.tgz", + "integrity": "sha512-26A0816VoHW8h64OT47dcclsH5M9iwo9zi3KoCPz1NrKoI9T2dlVkisgzTaGK4dPjCbP3ugf7cYqhWOiAzVHgw==" + } + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + }, + "table": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/table/-/table-4.0.2.tgz", + "integrity": "sha512-UUkEAPdSGxtRpiV9ozJ5cMTtYiqz7Ni1OGqLXRCynrvzdtR1p+cfOWe2RJLwvUG8hNanaSRjecIqwOjqeatDsA==", + "dev": true, + "requires": { + "ajv": "^5.2.3", + "ajv-keywords": "^2.1.0", + "chalk": "^2.1.0", + "lodash": "^4.17.4", + "slice-ansi": "1.0.0", + "string-width": "^2.1.1" + } + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, + "then-read-stream": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/then-read-stream/-/then-read-stream-1.5.1.tgz", + "integrity": "sha512-I+iiemYWhp1ysJQEioqpEICgvHlqHS5WrQGZkboFLs7Jm350Kvq4cN3qRCzHpETUuq5+NsdrdWEg6M0NFxtwtQ==" + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true + }, + "tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "requires": { + "os-tmpdir": "~1.0.2" + } + }, + "toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" + }, + "token-types": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-0.9.4.tgz", + "integrity": "sha512-KSl/Q1GJ4FoxbqKCLhTiIowVzom2cP0fgWGXKsJupbJQqfnCDmxkdMopIrt+y5Ak6YAGdy9TKpplWDioaBsbEw==" + }, + "tough-cookie": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz", + "integrity": "sha512-TZ6TTfI5NtZnuyy/Kecv+CnoROnyXn2DN97LontgQpCwsX2XyLYCC0ENhYkehSOwAp8rTQKc/NUIF7BkQ5rKLA==", + "requires": { + "punycode": "^1.4.1" + } + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "optional": true + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2" + } + }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", + "dev": true + }, + "underscore": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.9.1.tgz", + "integrity": "sha512-5/4etnCkd9c8gwgowi5/om/mYO5ajCaOgdzj/oW+0eQV9WxKBDZw5+ycmKmeaTXjInS/W0BzpGLo2xR2aBwZdg==" + }, + "universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==" + }, + "url": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", + "integrity": "sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ=", + "requires": { + "punycode": "1.3.2", + "querystring": "0.2.0" + }, + "dependencies": { + "punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=" + } + } + }, + "util": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "requires": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + }, + "uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" + }, + "validate-npm-package-license": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.3.tgz", + "integrity": "sha512-63ZOUnL4SIXj4L0NixR3L1lcjO38crAbgrTpl28t8jjrfuiOBL5Iygm+60qPs/KsZGzPNg6Smnc/oY16QTjF0g==", + "dev": true, + "requires": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "wav-file-info": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/wav-file-info/-/wav-file-info-0.0.8.tgz", + "integrity": "sha1-aAp160w0a34/RX55AqexmDUG5N4=" + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "which-typed-array": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.9.tgz", + "integrity": "sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==", + "requires": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.0", + "is-typed-array": "^1.1.10" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "write": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/write/-/write-0.2.1.tgz", + "integrity": "sha1-X8A4KOJkzqP+kUVUdvejxWbLB1c=", + "dev": true, + "requires": { + "mkdirp": "^0.5.1" + } + }, + "xml-flow": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/xml-flow/-/xml-flow-1.0.2.tgz", + "integrity": "sha512-TfV52Su2083n8w61l1hGI5K3ZAkn4hmaon3oJHc+TRQo0QkKLMPAPdMpGMJd+5Aik6ZUicPDjBINO8QxLkZGgg==", + "requires": { + "sax": "^1.2.4" + }, + "dependencies": { + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" + } + } + }, + "xml2js": { + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz", + "integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==", + "requires": { + "sax": ">=0.6.0", + "xmlbuilder": "~9.0.1" + } + }, + "xmlbuilder": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", + "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=" + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", + "dev": true + } + } +} diff --git a/package.json b/package.json index 3f521f43..aa8326c2 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,10 @@ { "name": "sonos-http-api", - "version": "1.2.1", + "version": "1.7.0", "description": "A simple node app for controlling a Sonos system with basic HTTP requests", "scripts": { - "start": "node server.js" + "start": "node server.js", + "lint": "eslint lib" }, "author": "Jimmy Shimizu ", "repository": { @@ -11,17 +12,28 @@ "url": "https://github.com/jishi/node-sonos-http-api.git" }, "dependencies": { - "basic-auth": "~1.0.3", - "fuse.js": "^2.2.0", "anesidora": "^1.2.0", - "node-static": "~0.7.0", + "aws-sdk": "^2.1299.0", + "basic-auth": "~1.1.0", + "fuse.js": "^6.4.1", + "html-entities": "^1.2.1", + "json5": "^0.5.1", + "mime": "^1.4.1", + "music-metadata": "^1.1.0", + "serve-static": "^1.15.0", "request-promise": "~1.0.2", - "sonos-discovery": "https://github.com/jishi/node-sonos-discovery/archive/v1.2.1.tar.gz" + "sonos-discovery": "https://github.com/jishi/node-sonos-discovery/archive/v1.8.0.tar.gz", + "wav-file-info": "0.0.8", + "elevenlabs-node": "2.0.1" }, "engines": { - "node": ">=4.0.0", - "npm": "^2.0.0" + "node": ">=4 <23" }, "main": "lib/sonos-http-api.js", - "license": "MIT" + "license": "MIT", + "devDependencies": { + "eslint": "^4.8.0", + "eslint-config-airbnb-base": "^12.0.1", + "eslint-plugin-import": "^2.7.0" + } } diff --git a/presets/example.json b/presets/example.json index ec7afcde..420c2540 100644 --- a/presets/example.json +++ b/presets/example.json @@ -27,5 +27,5 @@ "crossfade": false }, "pauseOthers": false, - "favorite": "My example favorite" + } diff --git a/server.js b/server.js index ca427e00..f59d6abd 100644 --- a/server.js +++ b/server.js @@ -3,25 +3,25 @@ const http = require('http'); const https = require('https'); const fs = require('fs'); const auth = require('basic-auth'); -const SonosDiscovery = require('sonos-discovery'); +const SonosSystem = require('sonos-discovery'); const logger = require('sonos-discovery/lib/helpers/logger'); const SonosHttpAPI = require('./lib/sonos-http-api.js'); -const nodeStatic = require('node-static'); +const serveStatic = require('serve-static'); const settings = require('./settings'); -const fileServer = new nodeStatic.Server(settings.webroot); -const discovery = new SonosDiscovery(settings); +const serve = new serveStatic(settings.webroot); +const discovery = new SonosSystem(settings); const api = new SonosHttpAPI(discovery, settings); var requestHandler = function (req, res) { req.addListener('end', function () { - fileServer.serve(req, res, function (err) { + serve(req, res, function (err) { // If error, route it. // This bypasses authentication on static files! - if (!err) { - return; - } + //if (!err) { + // return; + //} if (settings.auth) { var credentials = auth(req); @@ -80,8 +80,9 @@ process.on('unhandledRejection', (err) => { logger.error(err); }); -server.listen(settings.port, function () { - logger.info('http server listening on port', settings.port); +let host = settings.ip; +server.listen(settings.port, host, function () { + logger.info('http server listening on', host, 'port', settings.port); }); server.on('error', (err) => { diff --git a/settings.js b/settings.js index 422072b5..f2531e2d 100644 --- a/settings.js +++ b/settings.js @@ -2,6 +2,7 @@ const fs = require('fs'); const path = require('path'); const logger = require('sonos-discovery/lib/helpers/logger'); +const tryLoadJson = require('./lib/helpers/try-load-json'); function merge(target, source) { Object.keys(source).forEach((key) => { @@ -15,6 +16,7 @@ function merge(target, source) { var settings = { port: 5005, + ip: "0.0.0.0", securePort: 5006, cacheDir: path.resolve(__dirname, 'cache'), webroot: path.resolve(__dirname, 'static'), @@ -23,12 +25,9 @@ var settings = { }; // load user settings -try { - const userSettings = require(path.resolve(__dirname, 'settings.json')); - merge(settings, userSettings); -} catch (e) { - logger.info('no settings file found, will only use default settings'); -} +const settingsFileFullPath = path.resolve(__dirname, 'settings.json'); +const userSettings = tryLoadJson(settingsFileFullPath); +merge(settings, userSettings); logger.debug(settings); diff --git a/static/index.html b/static/index.html index fb2b2846..7bfc06db 100644 --- a/static/index.html +++ b/static/index.html @@ -11,10 +11,14 @@ .logo { display: block; margin: auto } .content { margin: 20px 25%; } .docs { background-color: #f4f4f4; border-radius: 4px; padding: 10px; margin-bottom: 20px;} - .docs p { padding: 0; margin: 0; line-height: 28px; font-size: 14px; font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace; } + .docs p, .docs li { padding: 0; margin: 0; line-height: 28px; font-size: 14px; font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace; } .docs .method { background-color: #dbdbdb; padding: 4px; border-radius: 2px; margin-right: 4px;} .docs h2 { margin-bottom: 8px;} .docs h2:first-child { margin-top: 0;} + .docs h4 { margin-left: 10px; } + .docs ul { list-style-type: none; padding-left: 20px; } + .docs .explanation { margin-left: 20px; font-family: 'Helvetica Neue', Helvetica, 'Segoe UI', Arial, freesans, sans-serif; font-size: 14px; } + .docs .experimental { background-color: red; border-radius: 3px; color: white; font-size: 10px; line-height: 12px; padding: 2px 6px; font-weight: bold; } .footer { text-align: center; font-size: 12px; margin: 10px 0 50px 0;} @@ -34,8 +38,8 @@

Info

Global Control

GET /lockvolumes

GET /unlockvolumes

-

GET /pauseall

-

GET /resumeall

+

GET /pauseall/{timeout in minutes (optional)}

+

GET /resumeall/{timeout in minutes (optional)}

GET /reindex

GET /sleep/{timeout in seconds or timestamp HH:MM:SS or off}

GET /preset/{JSON preset}

@@ -47,30 +51,65 @@

Zone Control

GET /{zone name}/{action}[/{parameter}]

Actions

+ +

Playback

+ + +

Volume

+ + +

Playback Settings

+ + +

Queue

+ + +

Room Grouping

+ + +

Other

+ + +

Internals

diff --git a/test_endpoint.js b/test_endpoint.js index 2fd72545..eedab434 100644 --- a/test_endpoint.js +++ b/test_endpoint.js @@ -2,6 +2,7 @@ const http = require('http'); let server = http.createServer((req, res) => { + console.log(req.method, req.url); for (let header in req.headers) { console.log(header + ':', req.headers[header]); } @@ -14,9 +15,11 @@ let server = http.createServer((req, res) => { req.on('end', () => { res.end(); - const json = JSON.parse(buffer.join('')); - console.dir(json, {depth: 10}); - console.log(''); + try { + const json = JSON.parse(buffer.join('')); + console.dir(json, { depth: 10 }); + console.log(''); + } catch (e) {} }); });