diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..03c1ba7 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,2 @@ +static/* +coverage/* \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..9c81269 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,225 @@ +{ + "env": { + "es6": true, + "node": true + }, + "extends": "eslint:recommended", + "parserOptions": { + "sourceType": "module" + }, + "rules": { + // possible errors + "no-extra-parens": "error", + "no-prototype-builtins": "error", + "no-template-curly-in-string": "error", + "no-unsafe-negation": "error", + + // best practices + "accessor-pairs": "error", + "array-callback-return": "error", + "block-scoped-var": "error", + "consistent-return": "error", + "curly" : "error", + "default-case": "error", + "dot-notation": "error", + "eqeqeq": "error", + "no-else-return": "error", + "no-empty-function": "error", + "no-eq-null": "error", + "no-eval": "error", + "no-extend-native": "error", + "no-global-assign": "error", + "no-implicit-coercion": "error", + "no-implicit-globals": "error", + "no-implied-eval": "error", + "no-invalid-this": "error", + "no-iterator": "error", + "no-labels": "error", + "no-lone-blocks": "error", + "no-loop-func": "error", + "no-multi-spaces": "error", + "no-multi-str": "error", + "no-new-func": "error", + "no-new-wrappers": "error", + "no-new": "error", + "no-param-reassign": "error", + "no-proto": "error", + "no-return-assign": "error", + "no-return-await": "error", + "no-self-compare": "error", + "no-sequences": "error", + "no-throw-literal": "error", + "no-unmodified-loop-condition": "error", + "no-unused-expressions": "error", + "no-useless-call": "error", + "no-useless-concat": "error", + "no-useless-escape": "error", + "no-void": "error", + "no-warning-comments": "error", + "no-with": "error", + "require-await": "error", + "vars-on-top": "error", + "wrap-iife": "error", + "yoda": "error", + + // variables + "no-catch-shadow": "error", + "no-label-var": "error", + "no-restricted-globals": "error", + "no-shadow-restricted-names": "error", + "no-shadow": "error", + "no-undef-init": "error", + "no-undefined": "error", + "no-use-before-define": "error", + + // node specific + "callback-return": "error", + "global-require": "error", + "handle-callback-err": "error", + "no-mixed-requires": "error", + "no-new-require": "error", + "no-path-concat": "error", + "no-process-env": "error", + "no-process-exit": "error", + "no-restricted-modules": "error", + // change this + "no-sync": "off", + + // style + "array-bracket-spacing": "error", + "block-spacing": "error", + "brace-style": "error", + "camelcase": "error", + "capitalized-comments": "error", + "comma-dangle": "error", + "comma-spacing": "error", + "comma-style": "error", + "computed-property-spacing": "error", + "consistent-this": "error", + "eol-last": "error", + "func-call-spacing": "error", + "func-names": "error", + "func-style": [ + "error", + "declaration" + ], + "id-blacklist": "error", + "id-length": [ + "error", + { + "exceptions": ["_"] + } + ], + "id-match": "error", + "indent": [ + "error", + 4, + { "SwitchCase": 1 } + ], + "key-spacing": "error", + "keyword-spacing": "error", + "lines-around-comment": "error", + "max-depth": "error", + "max-lines": "error", + "max-nested-callbacks": "error", + "max-params": [ + "error", + { "max": 7 } + ], + "max-statements-per-line": "error", + "max-statements": [ + "error", + { "max": 50 } + ], + "new-cap": "error", + "new-parens": "error", + "newline-after-var": "error", + "newline-before-return": "error", + "newline-per-chained-call": "error", + "no-array-constructor": "error", + "no-bitwise": "error", + "no-continue": "error", + "no-inline-comments": "error", + "no-lonely-if": "error", + "no-mixed-operators": "error", + "no-mixed-spaces-and-tabs": "error", + "no-multiple-empty-lines": "error", + "no-negated-condition": "error", + "no-nested-ternary": "error", + "no-new-object": "error", + "no-plusplus": "error", + "no-restricted-syntax": "error", + "no-tabs": "error", + "no-trailing-spaces": "error", + "no-underscore-dangle": "error", + "no-unneeded-ternary": "error", + "no-whitespace-before-property": "error", + "object-curly-newline": [ + "error", + { "minProperties": 1 } + ], + "object-curly-spacing": "error", + "object-property-newline": "error", + "one-var-declaration-per-line": "error", + "one-var": [ + "error", + "never" + ], + "operator-assignment": "error", + "operator-linebreak": "error", + "padded-blocks": [ + "error", + "never" + ], + "quote-props": [ + "error", + "as-needed" + ], + "quotes": [ + "error", + "single" + ], + "semi-spacing": "error", + "semi": [ + "error", + "always" + ], + "sort-vars": "error", + "space-before-blocks": "error", + "space-before-function-paren": [ + "error", + "never" + ], + "space-in-parens": "error", + "space-infix-ops": "error", + "space-unary-ops": "error", + "spaced-comment": "error", + "wrap-regex": "error", + + // ecmascript 6 + "arrow-body-style": [ + "error", + "always" + ], + "arrow-parens": "error", + "arrow-spacing": "error", + "generator-star-spacing": "error", + "no-confusing-arrow": "error", + "no-duplicate-imports": "error", + "no-restricted-imports": "error", + "no-useless-computed-key": "error", + "no-useless-constructor": "error", + "no-useless-rename": "error", + "no-var": "error", + "object-shorthand": "error", + "prefer-arrow-callback": "error", + "prefer-const": "error", + "prefer-rest-params": "error", + "prefer-spread": "error", + "prefer-template": "error", + "rest-spread-spacing": "error", + "sort-imports": "error", + "template-curly-spacing": "error", + "yield-star-spacing": "error" + } +} \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..53dc54c --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Set the default behavior, in case people don't have core.autocrlf set. +* auto diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ef6f304 --- /dev/null +++ b/.gitignore @@ -0,0 +1,20 @@ +node_modules +presets.json +presets/ +cache +settings.json +static/tts +.idea +.ntvs_analysis.dat +obj/ +*.njsproj +*.sln +*.pem +*.crt +*.pfx +*.key +.vscode/ +static/docs/webRootSetting.js +api/swagger/production.swagger.yaml +coverage/* +localDatabase/* \ No newline at end of file diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..790eba1 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2013 Jimmy Shimizu + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..99cf66a --- /dev/null +++ b/README.md @@ -0,0 +1,964 @@ +SWAGGER SONOS API +================= + +** This project was inspired by [https://github.com/jishi/node-sonos-http-api](https://github.com/jishi/node-sonos-http-api). +However it is implemented in a very different way and its usage is different, so it is not compatable with node-sonos-http-api. +Many thanks to [jishi](https://github.com/jishi) for the excellent work on node-sonos-http-api, sonos-discovery, and the initial idea of a REST API for interacting with sonos + +**This application requires node 4.0.0 or higher!** + +# Table of contents +- [Introduction](#introduction) +- [Installation instructions](#installation-instructions) +- [Configuration](#configuration) +- [Swagger](#swagger) +- [Players and zones](#players-and-zones) +- [API usage](#api-usage) + - [/players](#players) + - [/players/{playerName}](#playersplayername) + - [/players/{playerName}/state](#playersplayernamestate) + - [/players/{playerName}/nowplaying](#playersplayernamenowplaying) + - [/players/{playerName}/queue](#playersplayernamequeue) + - [/zones](#zones) + - [/zones/{zoneName}](#zoneszonename) + - [/zones/{zoneName}/state](#zoneszonenamestate) + - [/zones/{zoneName}/nowplaying](#zoneszonenamenowplaying) + - [/zones/{zoneName}/queue](#zoneszonenamequeue) + - [/zones/{zoneName}/members](#zoneszonenamemembers) + - [/zones/{zoneName}/members/{roomName}](#zoneszonenamemembersroomname) + - [/search](#search) + - [/favourites](#favourites) + - [/favourites/{favourite}](#favouritesfavourite) + - [/swagger](#swagger-1) + +Introduction +------------ +This is a REST API for interacting with a sonos system. It uses a swagger (or open api specification as it is now known) file to document the api as well as control behaviour in the code. +It tries to follow a design pattern for the api where the URL describes the resource being acted on, and you use different http verbs on the URL to control what happens when the it is called. + +For instance when interacting with the queue on the bedroom player +When you want to view the queue you would call +GET /players/bedroom/queue +When you want to add something to the queue you would call +PATCH /players/bedroom/queue +When you want to replace the whole queue you would call +POST /players/bedroom/queue +When you want to delete the queue you would call +DELETE /players/bedroom/queue + +By default, all calls are synchronous, so when you make a call to change something, it will only return once the change has been made. +This can be overridden by passing a paramater async=true in the query string. +Obviously if you pass this, then you will not know if the call has worked or not, but you will get a response back + +It includes [swagger-ui](https://github.com/swagger-api/swagger-ui) to provide interactive documentation when the server is running + + +Installation instructions +------------------------- + +Once you have cloned this repository, start by installing the dependencies. +Run the following command from the directory where this has been cloned to: + +`npm install --production` + +This will download the necessary dependencies. + +The program writes to the local filesystem while it is running, so the following directories must be writable by the user running the program + +./localDatabase +./api/swagger +./static/tts + +You can start the server by running + +`npm start` + +When it is running, you can see full documentation for the API and try different calls by going to http://localhost:10010/docs/ + +Configuration +------------- +You can override the default configuration and set custom values by copying settings.json.example to setting.json and putting values in there. +Available options are +- port - this is the port the service runs on +- staticWebRootPath - this is a full path to a static site that will be served and anouncements will be downloaded to +- ttsProvider - which text to speach provider to use - currently available ones are google and voicerss +- webRoot - this is the full URL that content will be served from. This needs to be set for announcemnts and swagger-ui to work correctly and should be set to your ip address eg http://192.168.1.17:10010/ +- databasePath - path to keep database files. Currently only used by iplayer search +- voicerssApiKey - this is an api key that is used to access [http://www.voicerss.org/](http://www.voicerss.org/). You must sign up for a key on that website for access + +Example: +``` +{ + "settings": { + "webRoot": "http://192.168.1.17:10010", + } +} +``` + +Swagger +------- +[Swagger](http://swagger.io/) is a framework for documenting APIs. It is now officially known as open api specificaton so you may also see reference to that in places + +This program has a swagger file in ./api/swagger/swagger.yaml. +You can see the swagger file when it is running by going to [http://localhost:10010/swagger/](http://localhost:10010/swagger/) + +If you want to edit a swagger file, you can use [swagger editor](http://editor.swagger.io) or another tool like [stoplight](https://stoplight.io/). +Each endpoint in the file has an x-swagger-router-controller attribute that defines which controller file (in ./api/controllers) is associated with the endpoint. +The operationId attribute specifies which function in the controller file is called when the endpoint is called. + +This all happens using the [swagger-restify-mw](https://github.com/apigee-127/swagger-restify) framework + +All documentation is included in the swagger file, and even most of this readme is generated from it using [swagger-markdown](https://github.com/syroegkin/swagger-markdown) + +Players and zones +----------------- +Within this API, players refer to individual sonos players and zones refer to a group of one or more players. +Most operations behave the same on players and zones - ie if you change whats playing on a player, then it also changes what is playing in the zone. +The exception to this is volume where you can change the volume on an individual player or for the whole zone. + + +# API Usage + +SONOS SWAGGER API +================= + + +**Version:** 0.9 + +### /players +--- +##### ***GET*** +**Summary:** get all players + +**Description:** This gets information about all players currently discovered + +Example call +``` +curl -X GET 'http://localhost:10010/players' +``` + +**Responses** + +| Code | Description | +| ---- | ----------- | +| 200 | successful result | +| default | error result | + +### /players/{playerName} +--- +##### ***GET*** +**Summary:** get individual player + +**Description:** This gets the details of an individual player + +Example call +``` +curl -X GET 'http://localhost:10010/players/bedroom' +``` + +**Parameters** + +| Name | Located in | Description | Required | Type | +| ---- | ---------- | ----------- | -------- | ---- | +| playerName | path | The player name | Yes | string | + +**Responses** + +| Code | Description | +| ---- | ----------- | +| 200 | successful result | +| default | error result | + +### /players/{playerName}/state +--- +##### ***GET*** +**Summary:** get player state + +**Description:** This gets the status of an individual player + +Example call +``` +curl -X GET 'http://localhost:10010/players/bedroom/state' +``` + +**Parameters** + +| Name | Located in | Description | Required | Type | +| ---- | ---------- | ----------- | -------- | ---- | +| playerName | path | The player name | Yes | string | + +**Responses** + +| Code | Description | +| ---- | ----------- | +| 200 | successful result | +| default | error result | + +##### ***PUT*** +**Summary:** set player state + +**Description:** This endpoint is used to to change the status of a player. The available states you can change are + +* volume - can either set the volume to an absolute value by passing a number, or relative by passing in a string prefixed by + or - +``` +curl -X PUT -H "Content-Type: application/json" -d '{"volume": 10}' "http://localhost:10010/players/bedroom/state" +curl -X PUT -H "Content-Type: application/json" -d '{"volume": "+5"}' "http://localhost:10010/players/bedroom/state" +``` + +* mute - can be either mute on or mute off +``` +curl -X PUT -H "Content-Type: application/json -d '{"mute": "mute on"}' "http://localhost:10010/players/bedroom/state" +``` +* trackNo - used to skip to a specific track +``` +curl -X PUT -H "Content-Type: application/json -d '{"trackNo": 5}' "http://localhost:10010/players/bedroom/state" +``` +* elapsedTime - used to skip to a time in the current track +``` +curl -X PUT -H "Content-Type: application/json -d '{"elapsedTime": 5}' "http://localhost:10010/players/bedroom/state" +``` +* playbackState - used to set playback state - can be either play, pause or toggle +``` +curl -X PUT -H "Content-Type: application/json -d '{"playbackState": "play"}' "http://localhost:10010/players/bedroom/state" +``` +* repeat - used to set repeat mode - can be either all, one or none +``` +curl -X PUT -H "Content-Type: application/json" -d ' {"playMode": {"repeat": "none"}}' "http://localhost:10010/players/bedroom/state" +``` +* shuffle - used to set shuffle mode - can be either shuffle on or shuffle off +``` +curl -X PUT -H "Content-Type: application/json" -d ' {"playMode": {"shuffle": "shuffle on"}}' "http://localhost:10010/players/bedroom/state" +``` +* crossfade - used to set crossfade mode - can be either crossfade on or crossfade off +``` +curl -X PUT -H "Content-Type: application/json" -d ' {"playMode": {"crossfade": "crossfade on"}}' "http://localhost:10010/players/bedroom/state" +``` +* currentTrack/favourite - used to play a sonos favourite. Returns a 404 error if favourite not found +``` +curl -X PUT -H "Content-Type: application/json" -d ' {"currentTrack": {"favourite": "BBC Radio 1"}}' "http://localhost:10010/players/bedroom/state" +``` +* currentTrack/playlist - used to play a sonos playlist. Returns a 404 error if playlist not found +``` +curl -X PUT -H "Content-Type: application/json" -d ' {"currentTrack": {"playlist": "test playlist"}}' "http://localhost:10010/players/bedroom/state" +``` +* currentTrack/clip - plays a clip and then resumes playback (apart from when playing from spotify connect). The clip must be in the directory static/clips/ +``` +curl -X PUT -H "Content-Type: application/json" -d ' {"currentTrack": {"clip":"http://192.168.1.17:10010/static/clips/sample_clip.mp3"}}' "http://localhost:10010/players/bedroom/state" +``` +* currentTrack/text - says some text and then resumes playback (apart from when playing from spotify connect) +``` +curl -X PUT -H "Content-Type: application/json" -d ' {"currentTrack": {"text":"hello world"}}' "http://localhost:10010/players/bedroom/state" +``` +* currentTrack/next - skips to the next track +``` +curl -X PUT -H "Content-Type: application/json" -d ' {"currentTrack": {"skip":"next"}}' "http://localhost:10010/players/bedroom/state" +``` +* currentTrack/previous - skips to the previous track +``` +curl -X PUT -H "Content-Type: application/json" -d ' {"currentTrack": {"skip":"previous"}}' "http://localhost:10010/players/bedroom/state" +``` +* currentTrack/linein - sets input to be linein of a specified player +``` +curl -X PUT -H "Content-Type: application/json" -d ' {"currentTrack": {"lineinSource":"kitchen"}}' "http://localhost:10010/players/bedroom/state" +``` +* currentTrack/artistTopTracks - plays the top tracks of the first artist returned by a spotify search +``` +curl -X PUT -H "Content-Type: application/json" -d ' {"currentTrack": {"artistTopTracks":"blink 182"}}' "http://localhost:10010/players/bedroom/state" +``` +* currentTrack/artistRadio - plays the artist radio of the first artist returned by a spotify search +``` +curl -X PUT -H "Content-Type: application/json" -d ' {"currentTrack": {"artistRadio":"blink 182"}}' "http://localhost:10010/players/bedroom/state" +``` +* currentTrack/song - plays the first song returned by a spotify search +``` +curl -X PUT -H "Content-Type: application/json" -d ' {"currentTrack": {"song":"all the small things"}}' "http://localhost:10010/players/bedroom/state" +``` + +**Parameters** + +| Name | Located in | Description | Required | Type | +| ---- | ---------- | ----------- | -------- | ---- | +| playerName | path | The player name | Yes | string | +| async | query | | No | boolean | +| body | body | | No | | + +**Responses** + +| Code | Description | +| ---- | ----------- | +| 201 | successful result | +| 202 | default asynchronous result | +| default | error result | + +### /players/{playerName}/nowplaying +--- +##### ***GET*** +**Summary:** get player now playing + +**Description:** This gets details of the currently playing track + +Example call +``` +curl -X GET "http://localhost:10010/zones/bedroom/nowplaying" +``` + +**Parameters** + +| Name | Located in | Description | Required | Type | +| ---- | ---------- | ----------- | -------- | ---- | +| playerName | path | The zone name | Yes | string | + +**Responses** + +| Code | Description | +| ---- | ----------- | +| 200 | successful result | +| default | error result | + +##### ***POST*** +**Summary:** set player now playing + +**Description:** This is used to set the currently playing track or radio. +The input should be passed in the body of the request and be an item returned from a search result + +Example call +``` +curl -X POST -H "Content-Type: application/json"-d '{"title": "The Animal In Me","artist": "The Animal In Me","album": "The Animal In Me","imageUrl": "https://i.scdn.co/image/37eff75cf19923b7dc796ba374515dbe45098c14","type": "artist","uri": "x-sonosapi-radio:spotify%3aartistRadio%3a6hyAYqBdxyramm4W9TB7R0?sid=9&flags=8300&sn=5","metadata": "\n The Animal In Me radioobject.item.audioItem.audioBroadcast.#artistRadio\n SA_RINCON2311_X_#Svc2311-0-Token"}' "http://localhost:10010/zones/bedroom/nowplaying" +``` + +**Parameters** + +| Name | Located in | Description | Required | Type | +| ---- | ---------- | ----------- | -------- | ---- | +| playerName | path | The zone name | Yes | string | +| async | query | | No | boolean | +| body | body | | No | | + +**Responses** + +| Code | Description | +| ---- | ----------- | +| 201 | successful result | +| 202 | default asynchronous result | +| default | error result | + +### /players/{playerName}/queue +--- +##### ***GET*** +**Summary:** get player queue + +**Description:** This gets the details of the current queue + +Example call +``` +curl -X GET "http://localhost:10010/zones/bedroom/queue?detailed=true" +``` + +**Parameters** + +| Name | Located in | Description | Required | Type | +| ---- | ---------- | ----------- | -------- | ---- | +| playerName | path | The zone name | Yes | string | +| detailed | query | Flag to indicate if detailed information should be returned. Default is false | No | boolean | + +**Responses** + +| Code | Description | +| ---- | ----------- | +| 200 | successful result | +| default | error result | + +##### ***PATCH*** +**Summary:** add to player queue + +**Description:** This is used to add an individual item to the current queue. +The input should be passed in the body of the request and be an item returned from a search result + +Example call +``` +curl -X PATCH -H "Content-Type: application/json" -d '{"uri": "x-sonos-spotify:spotify%3atrack%3a1D3ODoXHBLpdxolZRHWV1j?sid=9&flags=8224&sn=5","metadata": "object.item.audioItem.musicTrackSA_RINCON2311_X_#Svc2311-0-Token"}' "http://localhost:10010/zones/bedroom/queue" +``` + +**Parameters** + +| Name | Located in | Description | Required | Type | +| ---- | ---------- | ----------- | -------- | ---- | +| playerName | path | The zone name | Yes | string | +| async | query | | No | boolean | +| body | body | | No | | + +**Responses** + +| Code | Description | +| ---- | ----------- | +| 200 | successful result | +| 202 | default asynchronous result | +| default | error result | + +##### ***DELETE*** +**Summary:** clear player queue + +**Description:** This is used to clear the current queue + +Example call +``` +curl -X DELETE -H "Content-Type: application/json" -d '' "http://localhost:10010/zones/bedroom/queue" +``` + +**Parameters** + +| Name | Located in | Description | Required | Type | +| ---- | ---------- | ----------- | -------- | ---- | +| playerName | path | The zone name | Yes | string | +| async | query | | No | boolean | + +**Responses** + +| Code | Description | +| ---- | ----------- | +| 200 | successful result | +| 202 | default asynchronous result | +| default | error result | + +##### ***POST*** +**Summary:** replace playerqueue + +**Description:** This replaces the current queue with specified tracks. +The input should be passed in the body of the request and be an item returned from a search result + +Example call +``` +curl -X POST -H "Content-Type: application/json" -d ' {"uri": "x-sonos-spotify:spotify%3atrack%3a1D3ODoXHBLpdxolZRHWV1j?sid=9&flags=8224&sn=5", "metadata": "object.item.audioItem.musicTrackSA_RINCON2311_X_#Svc2311-0-Token"}' "http://localhost:10010/zones/bedroom/queue" +``` + +**Parameters** + +| Name | Located in | Description | Required | Type | +| ---- | ---------- | ----------- | -------- | ---- | +| playerName | path | The zone name | Yes | string | +| async | query | | No | boolean | +| body | body | | No | | + +**Responses** + +| Code | Description | +| ---- | ----------- | +| 201 | successful result | +| 202 | default asynchronous result | +| default | error result | + +### /zones +--- +##### ***GET*** +**Summary:** get all zones + +**Description:** This gets information about all zones currently discovered + +Example call +``` +curl -X GET 'http://localhost:10010/zones' +``` + +**Responses** + +| Code | Description | +| ---- | ----------- | +| 200 | successful result | +| default | error result | + +### /zones/{zoneName} +--- +##### ***GET*** +**Summary:** get individual zone + +**Description:** This gets the details of an individual zone + +Example call +``` +curl -X GET 'http://localhost:10010/zones/bedroom' +``` + +**Parameters** + +| Name | Located in | Description | Required | Type | +| ---- | ---------- | ----------- | -------- | ---- | +| zoneName | path | The zone name | Yes | string | + +**Responses** + +| Code | Description | +| ---- | ----------- | +| 200 | successful result | +| default | error result | + +### /zones/{zoneName}/state +--- +##### ***GET*** +**Summary:** get zone state + +**Description:** This gets the status of an individual zone + +Example call +``` +curl -X GET 'http://localhost:10010/zones/bedroom/state' +``` + +**Parameters** + +| Name | Located in | Description | Required | Type | +| ---- | ---------- | ----------- | -------- | ---- | +| zoneName | path | The zone name | Yes | string | + +**Responses** + +| Code | Description | +| ---- | ----------- | +| 200 | successful result | +| default | error result | + +##### ***PUT*** +**Summary:** set zone state + +**Description:** This endpoint is used to to change the status of a zone. The available states you can change are + +* volume - can either set the volume to an absolute value by passing a number, or relative by passing in a string prefixed by + or - +``` +curl -X PUT -H "Content-Type: application/json" -d '{"volume": 10}' "http://localhost:10010/zones/bedroom/state" +curl -X PUT -H "Content-Type: application/json" -d '{"volume": "+5"}' "http://localhost:10010/zones/bedroom/state" +``` + +* mute - can be either mute on or mute off +``` +curl -X PUT -H "Content-Type: application/json -d '{"mute": "mute on"}' "http://localhost:10010/zones/bedroom/state" +``` +* trackNo - used to skip to a specific track +``` +curl -X PUT -H "Content-Type: application/json -d '{"trackNo": 5}' "http://localhost:10010/zones/bedroom/state" +``` +* elapsedTime - used to skip to a time in the current track +``` +curl -X PUT -H "Content-Type: application/json -d '{"elapsedTime": 5}' "http://localhost:10010/zones/bedroom/state" +``` +* playbackState - used to set playback state - can be either play, pause or toggle +``` +curl -X PUT -H "Content-Type: application/json -d '{"playbackState": "play"}' "http://localhost:10010/zones/bedroom/state" +``` +* repeat - used to set repeat mode - can be either all, one or none +``` +curl -X PUT -H "Content-Type: application/json" -d ' {"playMode": {"repeat": "none"}}' "http://localhost:10010/zones/bedroom/state" +``` +* shuffle - used to set shuffle mode - can be either shuffle on or shuffle off +``` +curl -X PUT -H "Content-Type: application/json" -d ' {"playMode": {"shuffle": "shuffle on"}}' "http://localhost:10010/zones/bedroom/state" +``` +* crossfade - used to set crossfade mode - can be either crossfade on or crossfade off +``` +curl -X PUT -H "Content-Type: application/json" -d ' {"playMode": {"crossfade": "crossfade on"}}' "http://localhost:10010/zones/bedroom/state" +``` +* currentTrack/favourite - used to play a sonos favourite. Returns a 404 error if favourite not found +``` +curl -X PUT -H "Content-Type: application/json" -d ' {"currentTrack": {"favourite": "BBC Radio 1"}}' "http://localhost:10010/zones/bedroom/state" +``` +* currentTrack/playlist - used to play a sonos playlist. Returns a 404 error if playlist not found +``` +curl -X PUT -H "Content-Type: application/json" -d ' {"currentTrack": {"playlist": "test playlist"}}' "http://localhost:10010/zones/bedroom/state" +``` +* currentTrack/clip - plays a clip and then resumes playback (apart from when playing from spotify connect). The clip must exist in static/clips directory +``` +curl -X PUT -H "Content-Type: application/json" -d ' {"currentTrack": {"clip":"http://192.168.1.17:10010/static/clips/sample_clip.mp3"}}' "http://localhost:10010/zones/bedroom/state" +``` +* currentTrack/text - says some text and then resumes playback (apart from when playing from spotify connect) +``` +curl -X PUT -H "Content-Type: application/json" -d ' {"currentTrack": {"text":"hello world"}}' "http://localhost:10010/zones/bedroom/state" +``` +* currentTrack/next - skips to the next track +``` +curl -X PUT -H "Content-Type: application/json" -d ' {"currentTrack": {"skip":"next"}}' "http://localhost:10010/players/zones/state" +``` +* currentTrack/previous - skips to the previous track +``` +curl -X PUT -H "Content-Type: application/json" -d ' {"currentTrack": {"skip":"previous"}}' "http://localhost:10010/zones/bedroom/state" +``` +* currentTrack/linein - sets input to be linein of a specified player +``` +curl -X PUT -H "Content-Type: application/json" -d ' {"currentTrack": {"lineinSource":"kitchen"}}' "http://localhost:10010/zones/bedroom/state" +``` +* currentTrack/artistTopTracks - plays the top tracks of the first artist returned by a spotify search +``` +curl -X PUT -H "Content-Type: application/json" -d ' {"currentTrack": {"artistTopTracks":"blink 182"}}' "http://localhost:10010/zones/bedroom/state" +``` +* currentTrack/artistRadio - plays the artist radio of the first artist returned by a spotify search +``` +curl -X PUT -H "Content-Type: application/json" -d ' {"currentTrack": {"artistRadio":"blink 182"}}' "http://localhost:10010/zones/bedroom/state" +``` +* currentTrack/song - plays the first song returned by a spotify search +``` +curl -X PUT -H "Content-Type: application/json" -d ' {"currentTrack": {"song":"all the small things"}}' "http://localhost:10010/zones/bedroom/state" +``` + +**Parameters** + +| Name | Located in | Description | Required | Type | +| ---- | ---------- | ----------- | -------- | ---- | +| zoneName | path | The zone name | Yes | string | +| async | query | | No | boolean | +| body | body | | No | | + +**Responses** + +| Code | Description | +| ---- | ----------- | +| 201 | successful result | +| 202 | default asynchronous result | +| default | error result | + +### /zones/{zoneName}/nowplaying +--- +##### ***GET*** +**Summary:** get zone now playing + +**Description:** This gets details of the currently playing track + +Example call +``` +curl -X GET "http://localhost:10010/zones/bedroom/nowplaying" +``` + +**Parameters** + +| Name | Located in | Description | Required | Type | +| ---- | ---------- | ----------- | -------- | ---- | +| zoneName | path | The zone name | Yes | string | + +**Responses** + +| Code | Description | +| ---- | ----------- | +| 200 | successful result | +| default | error result | + +##### ***POST*** +**Summary:** set zone now playing + +**Description:** This is used to set the currently playing track or radio. +The input should be passed in the body of the request and be an item returned from a search result + +Example call +``` +curl -X POST -H "Content-Type: application/json"-d '{"title": "The Animal In Me","artist": "The Animal In Me","album": "The Animal In Me","imageUrl": "https://i.scdn.co/image/37eff75cf19923b7dc796ba374515dbe45098c14","type": "artist","uri": "x-sonosapi-radio:spotify%3aartistRadio%3a6hyAYqBdxyramm4W9TB7R0?sid=9&flags=8300&sn=5","metadata": "\n The Animal In Me radioobject.item.audioItem.audioBroadcast.#artistRadio\n SA_RINCON2311_X_#Svc2311-0-Token"}' "http://localhost:10010/zones/bedroom/nowplaying" +``` + +**Parameters** + +| Name | Located in | Description | Required | Type | +| ---- | ---------- | ----------- | -------- | ---- | +| zoneName | path | The zone name | Yes | string | +| async | query | | No | boolean | +| body | body | | No | | + +**Responses** + +| Code | Description | +| ---- | ----------- | +| 201 | successful result | +| 202 | default asynchronous result | +| default | error result | + +### /zones/{zoneName}/queue +--- +##### ***GET*** +**Summary:** get zone queue + +**Description:** This gets the details of the current queue + +Example call +``` +curl -X GET "http://localhost:10010/zones/bedroom/queue?detailed=true" +``` + +**Parameters** + +| Name | Located in | Description | Required | Type | +| ---- | ---------- | ----------- | -------- | ---- | +| zoneName | path | The zone name | Yes | string | +| detailed | query | Flag to indicate if detailed information should be returned. Default is false | No | boolean | + +**Responses** + +| Code | Description | +| ---- | ----------- | +| 200 | successful result | +| default | error result | + +##### ***PATCH*** +**Summary:** add to zone queue + +**Description:** This is used to add an individual item to the current queue. +The input should be passed in the body of the request and be an item returned from a search result + +Example call +``` +curl -X PATCH -H "Content-Type: application/json" -d '{"uri": "x-sonos-spotify:spotify%3atrack%3a1D3ODoXHBLpdxolZRHWV1j?sid=9&flags=8224&sn=5","metadata": "object.item.audioItem.musicTrackSA_RINCON2311_X_#Svc2311-0-Token"}' "http://localhost:10010/zones/bedroom/queue" +``` + +**Parameters** + +| Name | Located in | Description | Required | Type | +| ---- | ---------- | ----------- | -------- | ---- | +| zoneName | path | The zone name | Yes | string | +| async | query | | No | boolean | +| body | body | | No | | + +**Responses** + +| Code | Description | +| ---- | ----------- | +| 200 | successful result | +| 202 | default asynchronous result | +| default | error result | + +##### ***DELETE*** +**Summary:** clear zone queue + +**Description:** This is used to clear the current queue + +Example call +``` +curl -X DELETE -H "Content-Type: application/json" -d '' "http://localhost:10010/zones/bedroom/queue" +``` + +**Parameters** + +| Name | Located in | Description | Required | Type | +| ---- | ---------- | ----------- | -------- | ---- | +| zoneName | path | The zone name | Yes | string | +| async | query | | No | boolean | + +**Responses** + +| Code | Description | +| ---- | ----------- | +| 200 | successful result | +| 202 | default asynchronous result | +| default | error result | + +##### ***POST*** +**Summary:** replace zone queue + +**Description:** This replaces the current queue with specified tracks. +The input should be passed in the body of the request and be an item returned from a search result + +Example call +``` +curl -X POST -H "Content-Type: application/json" -d ' {"uri": "x-sonos-spotify:spotify%3atrack%3a1D3ODoXHBLpdxolZRHWV1j?sid=9&flags=8224&sn=5", "metadata": "object.item.audioItem.musicTrackSA_RINCON2311_X_#Svc2311-0-Token"}' "http://localhost:10010/zones/bedroom/queue" +``` + +**Parameters** + +| Name | Located in | Description | Required | Type | +| ---- | ---------- | ----------- | -------- | ---- | +| zoneName | path | The zone name | Yes | string | +| async | query | | No | boolean | +| body | body | | No | | + +**Responses** + +| Code | Description | +| ---- | ----------- | +| 201 | successful result | +| 202 | default asynchronous result | +| default | error result | + +### /zones/{zoneName}/members +--- +##### ***GET*** +**Summary:** get zone members + +**Description:** This gets all members of the zone +Example call + ``` + curl -X GET -H "Content-Type: application/json" "http://localhost:10010/zones/bedroom/members" + ``` + +**Parameters** + +| Name | Located in | Description | Required | Type | +| ---- | ---------- | ----------- | -------- | ---- | +| zoneName | path | | Yes | string | + +**Responses** + +| Code | Description | +| ---- | ----------- | +| 200 | successful result | +| default | error result | + +##### ***POST*** +**Summary:** add zone member + +**Description:** This adds a player to a zone +Example call +``` +curl -X POST -H "Content-Type: application/json" -d '{ + "player": "kitchen" +}' "http://localhost:10010/zones/bedroom/members" +``` + +**Parameters** + +| Name | Located in | Description | Required | Type | +| ---- | ---------- | ----------- | -------- | ---- | +| zoneName | path | | Yes | string | +| async | query | | No | boolean | +| body | body | | No | | + +**Responses** + +| Code | Description | +| ---- | ----------- | +| 201 | successful result | +| 202 | default asynchronous result | +| default | error result | + +### /zones/{zoneName}/members/{roomName} +--- +##### ***DELETE*** +**Summary:** remove zone member + +**Description:** This removes a member from a zone +Example call +``` +curl -X DELETE -d '' "http://localhost:10010/zones/bedroom/members/kitchen" +``` + +**Parameters** + +| Name | Located in | Description | Required | Type | +| ---- | ---------- | ----------- | -------- | ---- | +| zoneName | path | | Yes | string | +| roomName | path | | Yes | string | +| async | query | | No | boolean | + +**Responses** + +| Code | Description | +| ---- | ----------- | +| 200 | successful result | +| 202 | default asynchronous result | +| default | error result | + +### /search +--- +##### ***GET*** +**Summary:** get search results from a music service + +**Description:** This is used to search different music services. + +The service paramater controls which service to search. Currently implemented services are +* library +* spotify +* iplayer + +The type paramater controls what type to search for. This may vary accross services, but currently implemented for library and spotify services are +* song +* album +* artist + +For spotify, the type can also be specifed as artisttoptracks and it returns results which play the top tracks by the artist. + +For iplayer, this returns on demand programmes and the search types implemented are +* title +* synopsis + +For iplayer, the available list of programmes is refreshed every 24 hours, so the first time it is called, or if the data has not been refreshed for more than 24 hours, it may take a long time to run. You can force a refresh by calling + +The limit paramater allows you to limit the number of results returned. This defaults to 20 if not specified. + +The offset paramater allows you to page through results. This defaults to 0 if not specified. + +The returned results include an array of items. Each item in the array can be used as input to add to queue or set now playing endpoints. If the results returned are a radio stream then it can only be added to now playing. +Note - spotify retruns a radio stream for artist so can only be added to now playing + +Also included are links to next and previous results when more than the limit are returned to allow easy paging through the results, and details of the number of results returned and available. + +Example call +``` +curl -X GET 'http://localhost:10010/search?service=spotify&type=song&q=blue' +``` + +**Parameters** + +| Name | Located in | Description | Required | Type | +| ---- | ---------- | ----------- | -------- | ---- | +| service | query | The service to search | Yes | string | +| type | query | The type of search to perform - can be song, album, artist | Yes | string | +| q | query | The term to search for | Yes | string | +| offset | query | Used when multiple pages of results are returned to show results starting at offset | No | integer | +| limit | query | How many search items to return | No | integer | + +**Responses** + +| Code | Description | +| ---- | ----------- | +| 200 | successful result | +| default | error result | + +### /favourites +--- +##### ***GET*** +**Summary:** get favourites from sonos + +**Description:** This returns a list of favourites from sonos + +Example call +``` +curl -X GET 'http://localhost:10010/favourites' +``` + +**Parameters** + +| Name | Located in | Description | Required | Type | +| ---- | ---------- | ----------- | -------- | ---- | +| detailed | query | Used to specify if return just the names of the favourites or full details. Defaults to false | No | boolean | + +**Responses** + +| Code | Description | +| ---- | ----------- | +| 200 | successful result | +| default | error result | + +### /favourites/{favourite} +--- +##### ***GET*** +**Summary:** get individual favourite + +**Description:** This gets details of an individual favourite. +Example call +``` +curl -X GET "http://localhost:10010/favourites/6%20Music/" +``` + +**Parameters** + +| Name | Located in | Description | Required | Type | +| ---- | ---------- | ----------- | -------- | ---- | +| favourite | path | | Yes | string | + +**Responses** + +| Code | Description | +| ---- | ----------- | +| 200 | successful result | +| default | error result | + +### /swagger +--- +##### ***GET*** +**Description:** Gets the swagger definiton + +**Responses** + +| Code | Description | +| ---- | ----------- | +| default | | diff --git a/api/config/default.yaml b/api/config/default.yaml new file mode 100644 index 0000000..964d338 --- /dev/null +++ b/api/config/default.yaml @@ -0,0 +1,31 @@ +# swagger configuration file +# values in the swagger hash are system configuration for swagger-node +swagger: + fittingsDirs: [ fittings, node_modules ] + defaultPipe: null + swaggerControllerPipe: swagger_controllers # defines the standard processing pipe for controllers + # values defined in the bagpipes key are the bagpipes pipes and fittings definitions + # (see https://github.com/apigee-127/bagpipes) + bagpipes: + _router: + name: swagger_router + mockMode: false + mockControllersDirs: [ mocks ] + controllersDirs: [ controllers ] + controllersInterface: auto-detect + _swagger_validate: + name: swagger_validator + # pipe for all swagger-node controllers + swagger_controllers: + - onError: error_logger + - swagger_cors + - swagger_security + - swagger_params_parser + - _swagger_validate + - express_compatibility + - _router + # pipe to serve swagger (endpoint is in swagger.yaml) + swagger_raw: + - swagger_cors + - swagger_raw +# any other values in this file are just loaded into the config for application access... \ No newline at end of file diff --git a/api/controllers/favourites.js b/api/controllers/favourites.js new file mode 100644 index 0000000..17da6b3 --- /dev/null +++ b/api/controllers/favourites.js @@ -0,0 +1,63 @@ +'use strict'; +const log4js = require('log4js'); +const logger = log4js.getLogger('favourite.js'); +const favourites = require('../helpers/favourites'); +const commonFunctions = require('../helpers/commonFunctions'); +const zones = require('../helpers/zones'); + +function getFavourites(ctx, next) { + const discovery = ctx.request.discovery; + const detailed = ctx.request.swagger.params.detailed.value; + + zones.areZonesDiscovered(discovery) + .then(() => { + const player = discovery.getAnyPlayer(); + + return favourites.getFavourites(player, detailed); + }) + .then((results) => { + return commonFunctions.sendResponse(ctx, 200, results, next); + }) + .catch((error) => { + logger.error(error); + + return commonFunctions.errorHandler(ctx, error, next); + }); +} + +function getFavourite(ctx, next) { + const discovery = ctx.request.discovery; + const favouriteToLookFor = ctx.request.swagger.params.favourite.value; + + zones.areZonesDiscovered(discovery) + .then(() => { + const player = discovery.getAnyPlayer(); + + return favourites.getFavourite(player, favouriteToLookFor); + }) + .then((results) => { + if (results) { + return commonFunctions.sendResponse(ctx, 200, results, next); + } + + throw new Error('favourite not found'); + }) + .catch((error) => { + logger.error(error); + if (error.message === 'favourite not found') { + const response = { + code: 'favourite.not.found', + message: `cant find favourite ${favouriteToLookFor}` + }; + + return commonFunctions.sendResponse(ctx, 404, response, next); + } + + return commonFunctions.errorHandler(ctx, error, next); + }); +} + +module.exports = { + getFavourite, + getFavourites +}; diff --git a/api/controllers/playerNowPlaying.js b/api/controllers/playerNowPlaying.js new file mode 100644 index 0000000..0cdf487 --- /dev/null +++ b/api/controllers/playerNowPlaying.js @@ -0,0 +1,100 @@ +'use strict'; +const log4js = require('log4js'); +const logger = log4js.getLogger('nowplaying.js'); +const nowPlaying = require('../helpers/nowPlaying'); +const playpause = require('../helpers/playpause'); +const commonFunctions = require('../helpers/commonFunctions'); +const zones = require('../helpers/zones'); +const players = require('../helpers/players'); + +function getPlayerNowPlaying(ctx, next) { + const discovery = ctx.request.discovery; + const playerName = ctx.request.swagger.params.playerName.value; + let player; + + zones.areZonesDiscovered(discovery) + .then(() => { + return players.isValidPlayer(discovery, playerName); + }) + .then((isValidPlayer) => { + if (!isValidPlayer) { + throw new Error('player not found'); + } + player = discovery.getPlayer(playerName); + + return nowPlaying.getNowPlaying(player); + }) + .then((results) => { + return commonFunctions.sendResponse(ctx, 200, results, next); + }) + .catch((error) => { + logger.error(error); + if (error.message === 'player not found') { + const response = { + code: 'player.not.found', + message: `cant find player ${playerName}` + }; + + return commonFunctions.sendResponse(ctx, 404, response, next); + } + + return commonFunctions.errorHandler(ctx, error, next); + }); +} + +function setPlayerNowPlaying(ctx, next) { + logger.debug(`params ${commonFunctions.returnFullObject(ctx.request.swagger.params.body.value)}`); + const discovery = ctx.request.discovery; + const playerName = ctx.request.swagger.params.playerName.value; + const async = ctx.request.swagger.params.async.value; + const uri = ctx.request.swagger.params.body.value.uri; + const metadata = ctx.request.swagger.params.body.value.metadata; + let player; + + if (async) { + const response = { + message: 'OK' + }; + + commonFunctions.sendResponse(ctx, 202, response, next); + } + zones.areZonesDiscovered(discovery) + .then(() => { + return players.isValidPlayer(discovery, playerName); + }) + .then((isValidPlayer) => { + if (!isValidPlayer) { + throw new Error('player not found'); + } + player = discovery.getPlayer(playerName); + + return nowPlaying.setNowPlaying(player, uri, metadata); + }) + .then(() => { + return playpause.play(player); + }) + .then(() => { + return nowPlaying.getNowPlaying(player); + }) + .then((results) => { + return commonFunctions.sendResponse(ctx, 201, results, next, async); + }) + .catch((error) => { + logger.error(error); + if (error.message === 'player not found') { + const response = { + code: 'player.not.found', + message: `cant find player ${playerName}` + }; + + return commonFunctions.sendResponse(ctx, 404, response, next, async); + } + + return commonFunctions.errorHandler(ctx, error, next, async); + }); +} + +module.exports = { + getPlayerNowPlaying, + setPlayerNowPlaying +}; diff --git a/api/controllers/playerQueue.js b/api/controllers/playerQueue.js new file mode 100644 index 0000000..fa22e7d --- /dev/null +++ b/api/controllers/playerQueue.js @@ -0,0 +1,195 @@ +'use strict'; +const log4js = require('log4js'); +const logger = log4js.getLogger('queue.js'); +const queue = require('../helpers/queue'); +const commonFunctions = require('../helpers/commonFunctions'); +const zones = require('../helpers/zones'); +const players = require('../helpers/players'); + + +function addToPlayerQueue(ctx, next) { + logger.debug(`params ${commonFunctions.returnFullObject(ctx.request.swagger.params.body.value)}`); + const discovery = ctx.request.discovery; + const playerName = ctx.request.swagger.params.playerName.value; + const async = ctx.request.swagger.params.async.value; + const uri = ctx.request.swagger.params.body.value.uri; + const metadata = ctx.request.swagger.params.body.value.metadata; + const enqueAsNext = ctx.request.swagger.params.body.value.enqueAsNext; + const desiredFirstTrackNumberEnqueued = ctx.request.swagger.params.body.value.desiredFirstTrackNumberEnqueued; + + let player; + + if (async) { + const response = { + message: 'OK' + }; + + commonFunctions.sendResponse(ctx, 202, response, next); + } + zones.areZonesDiscovered(discovery) + .then(() => { + return players.isValidPlayer(discovery, playerName); + }) + .then((isValidPlayer) => { + if (!isValidPlayer) { + throw new Error('player not found'); + } + player = discovery.getPlayer(playerName); + + return queue.addToQueue(player, uri, metadata, enqueAsNext, desiredFirstTrackNumberEnqueued); + }) + .then((results) => { + return commonFunctions.sendResponse(ctx, 201, results, next, async); + }) + .catch((error) => { + logger.error(error); + if (error.message === 'player not found') { + const response = { + code: 'player.not.found', + message: `cant find player ${playerName}` + }; + + return commonFunctions.sendResponse(ctx, 404, response, next, async); + } + + return commonFunctions.errorHandler(ctx, error, next, async); + }); +} + +function replacePlayerQueue(ctx, next) { + logger.debug(`params ${commonFunctions.returnFullObject(ctx.request.swagger.params.body.value)}`); + const discovery = ctx.request.discovery; + const playerName = ctx.request.swagger.params.playerName.value; + const async = ctx.request.swagger.params.async.value; + const uri = ctx.request.swagger.params.body.value.uri; + const metadata = ctx.request.swagger.params.body.value.metadata; + const enqueAsNext = ctx.request.swagger.params.body.value.enqueAsNext; + const desiredFirstTrackNumberEnqueued = ctx.request.swagger.params.body.value.desiredFirstTrackNumberEnqueued; + let player; + + if (async) { + const response = { + message: 'OK' + }; + + commonFunctions.sendResponse(ctx, 202, response, next); + } + zones.areZonesDiscovered(discovery) + .then(() => { + return players.isValidPlayer(discovery, playerName); + }) + .then((isValidPlayer) => { + if (!isValidPlayer) { + throw new Error('player not found'); + } + player = discovery.getPlayer(playerName); + + return queue.replaceQueue(player, uri, metadata, enqueAsNext, desiredFirstTrackNumberEnqueued); + }) + .then((results) => { + return commonFunctions.sendResponse(ctx, 200, results, next, async); + }) + .catch((error) => { + logger.error(error); + if (error.message === 'player not found') { + const response = { + code: 'player.not.found', + message: `cant find player ${playerName}` + }; + + return commonFunctions.sendResponse(ctx, 404, response, next, async); + } + + return commonFunctions.errorHandler(ctx, error, next, async); + }); +} + +function clearPlayerQueue(ctx, next) { + const discovery = ctx.request.discovery; + const playerName = ctx.request.swagger.params.playerName.value; + const async = ctx.request.swagger.params.async.value; + let player; + + if (async) { + const response = { + message: 'OK' + }; + + commonFunctions.sendResponse(ctx, 202, response, next); + } + zones.areZonesDiscovered(discovery) + .then(() => { + return players.isValidPlayer(discovery, playerName); + }) + .then((isValidPlayer) => { + if (!isValidPlayer) { + throw new Error('player not found'); + } + player = discovery.getPlayer(playerName); + + return queue.clearQueue(player); + }) + .then(() => { + const response = { + message: 'OK' + }; + + return commonFunctions.sendResponse(ctx, 200, response, next, async); + }) + .catch((error) => { + logger.error(error); + if (error.message === 'player not found') { + const response = { + code: 'player.not.found', + message: `cant find player ${playerName}` + }; + + return commonFunctions.sendResponse(ctx, 404, response, next, async); + } + + return commonFunctions.errorHandler(ctx, error, next, async); + }); +} + +function getPlayerQueue(ctx, next) { + const discovery = ctx.request.discovery; + const playerName = ctx.request.swagger.params.playerName.value; + const detailed = ctx.request.swagger.params.detailed.value; + let player; + + zones.areZonesDiscovered(discovery) + .then(() => { + return players.isValidPlayer(discovery, playerName); + }) + .then((isValidPlayer) => { + if (!isValidPlayer) { + throw new Error('player not found'); + } + player = discovery.getPlayer(playerName); + + return queue.getQueue(player, detailed); + }) + .then((results) => { + return commonFunctions.sendResponse(ctx, 200, results, next); + }) + .catch((error) => { + logger.error(error); + if (error.message === 'player not found') { + const response = { + code: 'player.not.found', + message: `cant find player ${playerName}` + }; + + return commonFunctions.sendResponse(ctx, 404, response, next); + } + + return commonFunctions.errorHandler(ctx, error, next); + }); +} + +module.exports = { + addToPlayerQueue, + clearPlayerQueue, + getPlayerQueue, + replacePlayerQueue +}; diff --git a/api/controllers/playerState.js b/api/controllers/playerState.js new file mode 100644 index 0000000..b0ba5ef --- /dev/null +++ b/api/controllers/playerState.js @@ -0,0 +1,208 @@ +'use strict'; +const log4js = require('log4js'); +const logger = log4js.getLogger('zoneDetail.js'); +const players = require('../helpers/players'); +const playpause = require('../helpers/playpause'); +const volume = require('../helpers/volume'); +const mute = require('../helpers/mute'); +const seek = require('../helpers/seek'); +const nextprevious = require('../helpers/nextprevious'); +const favourite = require('../helpers/favourite'); +const playlist = require('../helpers/playlist'); +const playmode = require('../helpers/playmode'); +const linein = require('../helpers/linein'); +const clip = require('../helpers/clip'); +const say = require('../helpers/say'); +const zones = require('../helpers/zones'); +const searchHelper = require('../helpers/search'); +const _ = require('lodash'); + +const commonFunctions = require('../helpers/commonFunctions'); + +function setPlayerState(ctx, next) { + logger.debug(`params ${commonFunctions.returnFullObject(ctx.request.swagger.params.body.value)}`); + const discovery = ctx.request.discovery; + const settings = ctx.request.settings; + const playerName = ctx.request.swagger.params.playerName.value; + const async = ctx.request.swagger.params.async.value; + + const requestedFavorite = _.get(ctx.request.swagger.params.body.value, 'currentTrack.favourite'); + const requestedPlaylist = _.get(ctx.request.swagger.params.body.value, 'currentTrack.playlist'); + const requestedClip = _.get(ctx.request.swagger.params.body.value, 'currentTrack.clip'); + const requestedText = _.get(ctx.request.swagger.params.body.value, 'currentTrack.text'); + const requestedSkip = _.get(ctx.request.swagger.params.body.value, 'currentTrack.skip'); + const requestedSource = _.get(ctx.request.swagger.params.body.value, 'currentTrack.source'); + const requestedLineinSource = _.get(ctx.request.swagger.params.body.value, 'currentTrack.lineinSource'); + + const requestedVolume = _.get(ctx.request.swagger.params.body.value, 'volume'); + const requestedMute = _.get(ctx.request.swagger.params.body.value, 'mute'); + const requestedTrackNumber = _.get(ctx.request.swagger.params.body.value, 'trackNo'); + const requestedTime = _.get(ctx.request.swagger.params.body.value, 'elapsedTime'); + const requestedPlaybackState = _.get(ctx.request.swagger.params.body.value, 'playbackState'); + + const requestedRepeatMode = _.get(ctx.request.swagger.params.body.value, 'playMode.repeat'); + const requestedShuffleMode = _.get(ctx.request.swagger.params.body.value, 'playMode.shuffle'); + const requestedCrossfadeMode = _.get(ctx.request.swagger.params.body.value, 'playMode.crossfade'); + const requestedArtistTopTracks = _.get(ctx.request.swagger.params.body.value, 'currentTrack.artistTopTracks'); + const requestedArtistRadio = _.get(ctx.request.swagger.params.body.value, 'currentTrack.artistRadio'); + const requestedSong = _.get(ctx.request.swagger.params.body.value, 'currentTrack.song'); + + const clipVolume = 20; + + let player; + + if (async) { + const response = { + message: 'OK' + }; + + commonFunctions.sendResponse(ctx, 202, response, next); + } + zones.areZonesDiscovered(discovery) + .then(() => { + return players.isValidPlayer(discovery, playerName); + }) + .then((isValidPlayer) => { + if (!isValidPlayer) { + throw new Error('player not found'); + } + player = discovery.getPlayer(playerName); + + if (requestedVolume) { + return volume.setVolume(player, requestedVolume); + } + if (requestedMute) { + return mute.setMuteStatus(player, requestedMute); + } + if (requestedTrackNumber) { + return seek.trackSeek(player, requestedTrackNumber); + } + if (requestedTime) { + return seek.timeSeek(player, requestedTime); + } + if (requestedPlaybackState) { + return playpause.setPlaybackState(player, requestedPlaybackState); + } + if (requestedRepeatMode) { + return playmode.setRepeatStatus(player, requestedRepeatMode); + } + if (requestedShuffleMode) { + return playmode.setShuffleStatus(player, requestedShuffleMode); + } + if (requestedCrossfadeMode) { + return playmode.setCrossfadeStatus(player, requestedCrossfadeMode); + } + if (requestedFavorite) { + return favourite.playFavourite(player, requestedFavorite); + } + if (requestedPlaylist) { + return playlist.playPlaylist(player, requestedPlaylist); + } + if (requestedClip) { + return clip.playClip(player, requestedClip); + } + if (requestedText) { + return say.playText(player, requestedText, 'en-GB', clipVolume, ctx.request.settings); + } + if (requestedSkip === 'next') { + return nextprevious.next(player); + } + if (requestedSkip === 'previous') { + return nextprevious.previous(player); + } + if (requestedSource) { + return linein.setLinein(player, requestedLineinSource); + } + if (requestedArtistRadio) { + const service = 'spotify'; + + return searchHelper.playArtistRadio(player, requestedArtistRadio, service, settings); + } + if (requestedArtistTopTracks) { + const service = 'spotify'; + + return searchHelper.playArtistTopTracks(player, requestedArtistTopTracks, service, settings); + } + if (requestedSong) { + const service = 'spotify'; + + return searchHelper.playSong(player, requestedSong, service, settings); + } + + return null; + }) + .then(() => { + return players.getPlayer(discovery, playerName); + }) + .then((results) => { + return commonFunctions.sendResponse(ctx, 201, results.state, next, async); + }) + .catch((error) => { + logger.error(error); + if (error.message === 'player not found') { + const response = { + code: 'player.not.found', + message: `cant find player ${playerName}` + }; + + return commonFunctions.sendResponse(ctx, 404, response, next, async); + } + if (error.message === 'favourite not found') { + const response = { + code: 'favourite.not.found', + message: `cant find favourite ${requestedFavorite}` + }; + + return commonFunctions.sendResponse(ctx, 404, response, next, async); + } + if (error.message === 'Playlist not found') { + const response = { + code: 'playlist.not.found', + message: `cant find playlist ${requestedPlaylist}` + }; + + return commonFunctions.sendResponse(ctx, 404, response, next, async); + } + + return commonFunctions.errorHandler(ctx, error, next); + }); +} + +function getPlayerState(ctx, next) { + const discovery = ctx.request.discovery; + const playerName = ctx.request.swagger.params.playerName.value; + + zones.areZonesDiscovered(discovery) + .then(() => { + return players.isValidPlayer(discovery, playerName); + }) + .then((isValidPlayer) => { + if (!isValidPlayer) { + throw new Error('player not found'); + } + + return players.getPlayer(discovery, playerName); + }) + .then((results) => { + return commonFunctions.sendResponse(ctx, 200, results.state, next); + }) + .catch((error) => { + logger.error(error); + if (error.message === 'player not found') { + const response = { + code: 'player.not.found', + message: `cant find player ${playerName}` + }; + + return commonFunctions.sendResponse(ctx, 404, response, next); + } + + return commonFunctions.errorHandler(ctx, error, next); + }); +} + +module.exports = { + getPlayerState, + setPlayerState +}; + diff --git a/api/controllers/players.js b/api/controllers/players.js new file mode 100644 index 0000000..238a5e4 --- /dev/null +++ b/api/controllers/players.js @@ -0,0 +1,60 @@ +'use strict'; +const log4js = require('log4js'); +const logger = log4js.getLogger('players.js'); +const players = require('../helpers/players'); +const commonFunctions = require('../helpers/commonFunctions'); +const zones = require('../helpers/zones'); + +function getPlayers(ctx, next) { + const discovery = ctx.request.discovery; + + zones.areZonesDiscovered(discovery) + .then(() => { + return players.getPlayers(discovery); + }) + .then((results) => { + return commonFunctions.sendResponse(ctx, 200, results, next); + }) + .catch((error) => { + logger.error(error); + commonFunctions.errorHandler(ctx, error, next); + }); +} + +function getPlayer(ctx, next) { + const discovery = ctx.request.discovery; + const playerName = ctx.request.swagger.params.playerName.value; + + zones.areZonesDiscovered(discovery) + .then(() => { + return players.isValidPlayer(discovery, playerName); + }) + .then((isValidPlayer) => { + if (!isValidPlayer) { + throw new Error('player not found'); + } + + return players.getPlayer(discovery, playerName); + }) + .then((results) => { + return commonFunctions.sendResponse(ctx, 200, results, next); + }) + .catch((error) => { + logger.error(error); + if (error.message === 'player not found') { + const response = { + code: 'player.not.found', + message: `cant find player ${playerName}` + }; + + return commonFunctions.sendResponse(ctx, 404, response, next); + } + + return commonFunctions.errorHandler(ctx, error, next); + }); +} + +module.exports = { + getPlayer, + getPlayers +}; diff --git a/api/controllers/search.js b/api/controllers/search.js new file mode 100644 index 0000000..40dfe4b --- /dev/null +++ b/api/controllers/search.js @@ -0,0 +1,35 @@ +'use strict'; +const log4js = require('log4js'); +const logger = log4js.getLogger('search.js'); +const searchHelper = require('../helpers/search'); +const commonFunctions = require('../helpers/commonFunctions'); +const zones = require('../helpers/zones'); + +function search(ctx, next) { + const discovery = ctx.request.discovery; + const settings = ctx.request.settings; + const service = ctx.request.swagger.params.service.value; + const type = ctx.request.swagger.params.type.value; + const query = ctx.request.swagger.params.q.value; + const offset = ctx.request.swagger.params.offset.value; + const limit = ctx.request.swagger.params.limit.value; + + zones.areZonesDiscovered(discovery) + .then(() => { + const player = discovery.getAnyPlayer(); + + return searchHelper.search(player, service, type, query, offset, limit, settings); + }) + .then((results) => { + return commonFunctions.sendResponse(ctx, 200, results, next); + }) + .catch((error) => { + logger.error(error); + + return commonFunctions.errorHandler(ctx, error, next); + }); +} + +module.exports = { + search +}; diff --git a/api/controllers/zoneMembers.js b/api/controllers/zoneMembers.js new file mode 100644 index 0000000..fa11f5c --- /dev/null +++ b/api/controllers/zoneMembers.js @@ -0,0 +1,159 @@ +const log4js = require('log4js'); +const logger = log4js.getLogger('zoneDetail.js'); +const zones = require('../helpers/zones'); +const players = require('../helpers/players'); +const commonFunctions = require('../helpers/commonFunctions'); + +function getZoneMembers(ctx, next) { + const discovery = ctx.request.discovery; + const zoneName = ctx.request.swagger.params.zoneName.value; + + zones.areZonesDiscovered(discovery) + .then(() => { + return zones.isValidZone(discovery, zoneName); + }) + .then((isValidZone) => { + if (!isValidZone) { + throw new Error('zone not found'); + } + + return zones.getZone(discovery, zoneName); + }) + .then((results) => { + return commonFunctions.sendResponse(ctx, 200, results.members, next); + }) + .catch((error) => { + logger.error(error); + if (error.message === 'zone not found') { + const response = { + code: 'zone.not.found', + message: `cant find zone ${zoneName}` + }; + + return commonFunctions.sendResponse(ctx, 404, response, next); + } + + return commonFunctions.errorHandler(ctx, error, next); + }); +} + +function addZoneMember(ctx, next) { + logger.debug(`params ${commonFunctions.returnFullObject(ctx.request.swagger.params.body.value)}`); + const discovery = ctx.request.discovery; + const zoneName = ctx.request.swagger.params.zoneName.value; + const playerName = ctx.request.swagger.params.body.value.player; + const async = ctx.request.swagger.params.async.value; + + if (async) { + const response = { + message: 'OK' + }; + + commonFunctions.sendResponse(ctx, 202, response, next); + } + zones.areZonesDiscovered(discovery) + .then(() => { + return zones.isValidZone(discovery, zoneName); + }) + .then((isValidZone) => { + if (!isValidZone) { + throw new Error('zone not found'); + } + + return players.isValidPlayer(discovery, playerName); + }) + .then((isValidPlayer) => { + if (!isValidPlayer) { + throw new Error('player not found'); + } + + const zone = discovery.getPlayer(zoneName); + const player = discovery.getPlayer(playerName); + + return zones.addMemberToZone(discovery, zone, player); + }) + .then(() => { + return zones.getZone(discovery, zoneName); + }) + .then((results) => { + return commonFunctions.sendResponse(ctx, 201, results.members, next, async); + }) + .catch((error) => { + logger.error(error); + if (error.message === 'zone not found') { + commonFunctions.createResponse(ctx, 404); + const response = { + code: 'zone.not.found', + message: `cant find zone ${zoneName}` + }; + + return commonFunctions.sendResponse(ctx, 404, response, next, async); + } + + return commonFunctions.errorHandler(ctx, error, next); + }); +} + +function removeZoneMember(ctx, next) { + const discovery = ctx.request.discovery; + const zoneName = ctx.request.swagger.params.zoneName.value; + const playerName = ctx.request.swagger.params.roomName.value; + const async = ctx.request.swagger.params.async.value; + + if (async) { + const response = { + message: 'OK' + }; + + commonFunctions.sendResponse(ctx, 202, response, next); + } + zones.areZonesDiscovered(discovery) + .then(() => { + return zones.isValidZone(discovery, zoneName); + }) + .then((isValidZone) => { + if (!isValidZone) { + throw new Error('zone not found'); + } + + return players.isValidPlayer(discovery, playerName); + }) + .then((isValidPlayer) => { + if (!isValidPlayer) { + throw new Error('player not found'); + } + if (zoneName === playerName) { + throw new Error('cant remove a player from its own zone'); + } + + const zone = discovery.getPlayer(zoneName); + const player = discovery.getPlayer(playerName); + + return zones.removeMemberFromZone(discovery, zone, player); + }) + .then(() => { + return zones.getZone(discovery, zoneName); + }) + .then((results) => { + return commonFunctions.sendResponse(ctx, 200, results.members, next, async); + }) + .catch((error) => { + logger.error(error); + if (error.message === 'zone not found') { + const response = { + code: 'zone.not.found', + message: `cant find zone ${zoneName}` + }; + + return commonFunctions.sendResponse(ctx, 404, response, next, async); + } + + return commonFunctions.errorHandler(ctx, error, next); + }); +} + +module.exports = { + addZoneMember, + getZoneMembers, + removeZoneMember +}; diff --git a/api/controllers/zoneNowPlaying.js b/api/controllers/zoneNowPlaying.js new file mode 100644 index 0000000..9c76a53 --- /dev/null +++ b/api/controllers/zoneNowPlaying.js @@ -0,0 +1,75 @@ +'use strict'; +const log4js = require('log4js'); +const logger = log4js.getLogger('nowplaying.js'); +const commonFunctions = require('../helpers/commonFunctions'); +const zones = require('../helpers/zones'); +const _ = require('lodash'); +const playerNowPlaying = require('./playerNowPlaying'); + +function getZoneNowPlaying(ctx, next) { + const discovery = ctx.request.discovery; + const zoneName = ctx.request.swagger.params.zoneName.value; + + zones.areZonesDiscovered(discovery) + .then(() => { + return zones.isValidZone(discovery, zoneName); + }) + .then((isValidZone) => { + if (!isValidZone) { + throw new Error('zone not found'); + } + _.set(ctx.request.swagger.params, 'playerName.value', zoneName); + + return playerNowPlaying.getPlayerNowPlaying(ctx, next); + }) + .catch((error) => { + logger.error(error); + if (error.message === 'zone not found') { + const response = { + code: 'zone.not.found', + message: `cant find zone ${zoneName}` + }; + + return commonFunctions.sendResponse(ctx, 404, response, next); + } + + return commonFunctions.errorHandler(ctx, error, next); + }); +} + +function setZoneNowPlaying(ctx, next) { + logger.debug(`params ${commonFunctions.returnFullObject(ctx.request.swagger.params.body.value)}`); + const discovery = ctx.request.discovery; + const zoneName = ctx.request.swagger.params.zoneName.value; + + zones.areZonesDiscovered(discovery) + .then(() => { + return zones.isValidZone(discovery, zoneName); + }) + .then((isValidZone) => { + if (!isValidZone) { + throw new Error('zone not found'); + } + _.set(ctx.request.swagger.params, 'playerName.value', zoneName); + + return playerNowPlaying.setPlayerNowPlaying(ctx, next); + }) + .catch((error) => { + logger.error(error); + if (error.message === 'zone not found') { + const response = { + code: 'zone.not.found', + message: `cant find zone ${zoneName}` + }; + + return commonFunctions.sendResponse(ctx, 404, response, next); + } + + return commonFunctions.errorHandler(ctx, error, next); + }); +} + +module.exports = { + getZoneNowPlaying, + setZoneNowPlaying +}; diff --git a/api/controllers/zoneQueue.js b/api/controllers/zoneQueue.js new file mode 100644 index 0000000..e3ac968 --- /dev/null +++ b/api/controllers/zoneQueue.js @@ -0,0 +1,140 @@ +'use strict'; +const log4js = require('log4js'); +const logger = log4js.getLogger('queue.js'); +const commonFunctions = require('../helpers/commonFunctions'); +const zones = require('../helpers/zones'); +const playerQueue = require('./playerQueue'); +const _ = require('lodash'); + +function addToZoneQueue(ctx, next) { + logger.debug(`params ${commonFunctions.returnFullObject(ctx.request.swagger.params.body.value)}`); + const discovery = ctx.request.discovery; + const zoneName = ctx.request.swagger.params.zoneName.value; + + zones.areZonesDiscovered(discovery) + .then(() => { + return zones.isValidZone(discovery, zoneName); + }) + .then((isValidZone) => { + if (!isValidZone) { + throw new Error('zone not found'); + } + _.set(ctx.request.swagger.params, 'playerName.value', zoneName); + + return playerQueue.addToPlayerQueue(ctx, next); + }) + .catch((error) => { + logger.error(error); + if (error.message === 'zone not found') { + const response = { + code: 'zone.not.found', + message: `cant find zone ${zoneName}` + }; + + return commonFunctions.sendResponse(ctx, 404, response, next); + } + + return commonFunctions.errorHandler(ctx, error, next); + }); +} + +function replaceZoneQueue(ctx, next) { + logger.debug(`params ${commonFunctions.returnFullObject(ctx.request.swagger.params.body.value)}`); + const discovery = ctx.request.discovery; + const zoneName = ctx.request.swagger.params.zoneName.value; + + zones.areZonesDiscovered(discovery) + .then(() => { + return zones.isValidZone(discovery, zoneName); + }) + .then((isValidZone) => { + if (!isValidZone) { + throw new Error('zone not found'); + } + _.set(ctx.request.swagger.params, 'playerName.value', zoneName); + + return playerQueue.replacePlayerQueue(ctx, next); + }) + .catch((error) => { + logger.error(error); + if (error.message === 'zone not found') { + const response = { + code: 'zone.not.found', + message: `cant find zone ${zoneName}` + }; + + return commonFunctions.sendResponse(ctx, 404, response, next); + } + + return commonFunctions.errorHandler(ctx, error, next); + }); +} + +function clearZoneQueue(ctx, next) { + const discovery = ctx.request.discovery; + const zoneName = ctx.request.swagger.params.zoneName.value; + + zones.areZonesDiscovered(discovery) + .then(() => { + return zones.isValidZone(discovery, zoneName); + }) + .then((isValidZone) => { + if (!isValidZone) { + throw new Error('zone not found'); + } + _.set(ctx.request.swagger.params, 'playerName.value', zoneName); + + return playerQueue.clearPlayerQueue(ctx, next); + }) + .catch((error) => { + logger.error(error); + if (error.message === 'zone not found') { + const response = { + code: 'zone.not.found', + message: `cant find zone ${zoneName}` + }; + + return commonFunctions.sendResponse(ctx, 404, response, next); + } + + return commonFunctions.errorHandler(ctx, error, next); + }); +} + +function getZoneQueue(ctx, next) { + const discovery = ctx.request.discovery; + const zoneName = ctx.request.swagger.params.zoneName.value; + + zones.areZonesDiscovered(discovery) + .then(() => { + return zones.isValidZone(discovery, zoneName); + }) + .then((isValidZone) => { + if (!isValidZone) { + throw new Error('zone not found'); + } + _.set(ctx.request.swagger.params, 'playerName.value', zoneName); + + return playerQueue.getPlayerQueue(ctx, next); + }) + .catch((error) => { + logger.error(error); + if (error.message === 'zone not found') { + const response = { + code: 'zone.not.found', + message: `cant find zone ${zoneName}` + }; + + return commonFunctions.sendResponse(ctx, 404, response, next); + } + + return commonFunctions.errorHandler(ctx, error, next); + }); +} + +module.exports = { + addToZoneQueue, + replaceZoneQueue, + clearZoneQueue, + getZoneQueue +}; diff --git a/api/controllers/zoneState.js b/api/controllers/zoneState.js new file mode 100644 index 0000000..14829ab --- /dev/null +++ b/api/controllers/zoneState.js @@ -0,0 +1,78 @@ +'use strict'; +const log4js = require('log4js'); +const logger = log4js.getLogger('zoneState.js'); +const zones = require('../helpers/zones'); +const _ = require('lodash'); +const playerState = require('./playerState'); + +const commonFunctions = require('../helpers/commonFunctions'); + +function setZoneState(ctx, next) { + logger.debug(`params ${commonFunctions.returnFullObject(ctx.request.swagger.params.body.value)}`); + const discovery = ctx.request.discovery; + const zoneName = ctx.request.swagger.params.zoneName.value; + + zones.areZonesDiscovered(discovery) + .then(() => { + return zones.isValidZone(discovery, zoneName); + }) + .then((isValidZone) => { + if (!isValidZone) { + throw new Error('zone not found'); + } + _.set(ctx.request.swagger.params, 'playerName.value', zoneName); + + return playerState.setPlayerState(ctx, next); + }) + .catch((error) => { + logger.error(error); + if (error.message === 'zone not found') { + const response = { + code: 'zone.not.found', + message: `cant find zone ${zoneName}` + }; + + return commonFunctions.sendResponse(ctx, 404, response, next); + } + + return commonFunctions.errorHandler(ctx, error, next); + }); +} + +function getZoneState(ctx, next) { + const discovery = ctx.request.discovery; + const zoneName = ctx.request.swagger.params.zoneName.value; + + zones.areZonesDiscovered(discovery) + .then(() => { + return zones.isValidZone(discovery, zoneName); + }) + .then((isValidZone) => { + if (!isValidZone) { + throw new Error('zone not found'); + } + + _.set(ctx.request.swagger.params, 'playerName.value', zoneName); + + return playerState.getPlayerState(ctx, next); + }) + .catch((error) => { + logger.error(error); + if (error.message === 'zone not found') { + const response = { + code: 'zone.not.found', + message: `cant find zone ${zoneName}` + }; + + return commonFunctions.sendResponse(ctx, 404, response, next); + } + + return commonFunctions.errorHandler(ctx, error, next); + }); +} + +module.exports = { + getZoneState, + setZoneState +}; + diff --git a/api/controllers/zones.js b/api/controllers/zones.js new file mode 100644 index 0000000..b3b4c31 --- /dev/null +++ b/api/controllers/zones.js @@ -0,0 +1,60 @@ +'use strict'; +const log4js = require('log4js'); +const logger = log4js.getLogger('zones.js'); +const zones = require('../helpers/zones'); +const commonFunctions = require('../helpers/commonFunctions'); + +function getZones(ctx, next) { + const discovery = ctx.request.discovery; + + zones.areZonesDiscovered(discovery) + .then(() => { + return zones.getZones(discovery); + }) + .then((results) => { + return commonFunctions.sendResponse(ctx, 200, results, next); + }) + .catch((error) => { + logger.error(error); + + return commonFunctions.errorHandler(ctx, error, next); + }); +} + +function getZone(ctx, next) { + const discovery = ctx.request.discovery; + const zoneName = ctx.request.swagger.params.zoneName.value; + + zones.areZonesDiscovered(discovery) + .then(() => { + return zones.isValidZone(discovery, zoneName); + }) + .then((isValidZone) => { + if (!isValidZone) { + throw new Error('zone not found'); + } + + return zones.getZone(discovery, zoneName); + }) + .then((results) => { + return commonFunctions.sendResponse(ctx, 200, results, next); + }) + .catch((error) => { + logger.error(error); + if (error.message === 'zone not found') { + const response = { + code: 'zone.not.found', + message: `cant find zone ${zoneName}` + }; + + return commonFunctions.sendResponse(ctx, 404, response, next); + } + + return commonFunctions.errorHandler(ctx, error, next); + }); +} + +module.exports = { + getZone, + getZones +}; diff --git a/api/fittings/error_logger.js b/api/fittings/error_logger.js new file mode 100644 index 0000000..072c871 --- /dev/null +++ b/api/fittings/error_logger.js @@ -0,0 +1,70 @@ +'use strict'; + +const debug = require('debug')('swagger:json_error_handler'); +const util = require('util'); +const log4js = require('log4js'); +const logger = log4js.getLogger('app.js'); + +/* eslint no-unused-vars: off, camelcase:off, no-mixed-operators: off*/ +module.exports = function create(fittingDef, bagpipes) { + return function error_handler(context, next) { + if (!util.isError(context.error)) { + return next(); + } + + const err = context.error; + let log; + let body; + + logger.error(util.inspect(context.error, false, null)); + + if (!context.statusCode || context.statusCode < 400) { + if (context.response && context.response.statusCode && context.response.statusCode >= 400) { + context.statusCode = context.response.statusCode; + } else if (err.statusCode && err.statusCode >= 400) { + context.statusCode = err.statusCode; + delete err.statusCode; + } else { + context.statusCode = 500; + } + } + + try { + if (context.statusCode === 500 && !fittingDef.handle500Errors) { + return next(err); + } + + context.headers['Content-Type'] = 'application/json'; + Object.defineProperty(err, 'message', { + enumerable: true + }); + if (fittingDef.includeErrStack) { + Object.defineProperty(err, 'stack', { + enumerable: true + }); + } + + delete context.error; + + return next(null, JSON.stringify(err)); + } catch (err2) { + log = context.request && ( + context.request.log || context.request.app && context.request.app.log + ) || context.response && context.response.log; + + body = { + message: 'unable to stringify error properly', + stringifyErr: err2.message, + originalErrInspect: util.inspect(err) + }; + context.statusCode = 500; + + debug('jsonErrorHandler unable to stringify error: ', err); + if (log) { + log.error(err2, 'onError: json_error_handler - unable to stringify error', err); + } + + return next(null, JSON.stringify(body)); + } + }; +}; diff --git a/api/helpers/clip.js b/api/helpers/clip.js new file mode 100644 index 0000000..d6f25b1 --- /dev/null +++ b/api/helpers/clip.js @@ -0,0 +1,96 @@ +'use strict'; +const log4js = require('log4js'); +const logger = log4js.getLogger('helpers/clip.js'); +const nowPlaying = require('./nowPlaying'); +const playPause = require('./playpause'); +const seek = require('./seek'); +const commonFunctions = require('./commonFunctions'); +const util = require('util'); + +function setTrackAndPosition(player, track, seconds) { + return Promise.resolve() + .then(() => { + return seek.trackSeek(player, track); + }) + .then(() => { + return seek.timeSeek(player, seconds); + }) + .catch((err) => { + logger.error(util.inspect(err, null, false)); + throw err; + }); +} + + +function playClip(player, clip) { + const initialState = player.state; + const initialMediaInfo = { + avTransportUri: player.avTransportUri, + avTransportUriMetadata: player.avTransportUriMetadata + }; + + let announceFinished; + let afterPlayingStateChange; + + function onTransportChange(state) { + logger.debug(`playback state switched to ${state.playbackState}`); + if (state.playbackState === 'PLAYING') { + logger.debug('announcement started'); + afterPlayingStateChange = announceFinished; + } + + if (state.playbackState !== 'STOPPED') { + return; + } + + if (afterPlayingStateChange instanceof Function) { + logger.debug('announcement finished because of STOPPED state identified'); + afterPlayingStateChange(); + } + } + + return Promise.resolve() + .then(() => { + return nowPlaying.setNowPlaying(player, clip, ''); + }) + .then(() => { + player.on('transport-state', onTransportChange); + + return playPause.playAsync(player); + }) + .then(() => { + return new Promise((resolve) => { + announceFinished = resolve; + }); + }) + .then(() => { + player.removeListener('transport-state', onTransportChange); + + return nowPlaying.setNowPlaying(player, initialMediaInfo.avTransportUri, initialMediaInfo.avTransportUriMetadata); + }) + .then(() => { + if (!commonFunctions.isRadioOrLineIn(initialMediaInfo.avTransportUri) && initialState.trackNo > 0) { + logger.debug('not a stream so setting position'); + + return setTrackAndPosition(player, initialState.trackNo, initialState.elapsedTime); + } + + return 1; + }) + .then(() => { + if (initialState.playbackState === 'PLAYING') { + return playPause.play(player); + } + + return 1; + }) + .catch((err) => { + player.removeListener('transport-state', onTransportChange); + logger.error(util.inspect(err, null, false)); + throw err; + }); +} + +module.exports = { + playClip +}; diff --git a/api/helpers/commonFunctions.js b/api/helpers/commonFunctions.js new file mode 100644 index 0000000..a52fe9b --- /dev/null +++ b/api/helpers/commonFunctions.js @@ -0,0 +1,69 @@ +'use strict'; +const util = require('util'); + + +function sendResponse(ctx, statusCode, response, next, async) { + if (async) { + return true; + } + ctx.headers = { + 'Content-Type': 'application/json' + }; + ctx.statusCode = statusCode; + + return next(null, response); +} +function errorHandler(ctx, error, next, async) { + const response = { + message: error.message, + stack: error.stack, + code: 'error' + }; + + sendResponse(ctx, 500, response, next, async); +} + +function checkReturnStatus(status) { + return Promise.resolve() + .then(() => { + if (status.statusCode === 200 && status.statusMessage === 'OK') { + return; + } + throw new Error(`bad response : ${status.statusMessage}`); + }); +} + +function returnFullObject(object) { + return util.inspect(object, false, null); +} + +function isRadioOrLineIn(uri) { + return uri.startsWith('x-sonosapi-stream:') || + uri.startsWith('x-sonosapi-radio:') || + uri.startsWith('pndrradio:') || + uri.startsWith('x-sonosapi-hls:') || + uri.startsWith('x-rincon-stream:') || + uri.startsWith('x-sonos-htastream:') || + uri.startsWith('x-sonosprog-http:') || + uri.startsWith('x-rincon-mp3radio:'); +} + +const types = { + array: '[object Array]', + boolean: '[object Boolean]', + get: (prop) => { + return Object.prototype.toString.call(prop); + }, + number: '[object Number]', + object: '[object Object]', + string: '[object String]' +}; + +module.exports = { + checkReturnStatus, + sendResponse, + errorHandler, + isRadioOrLineIn, + returnFullObject, + types +}; diff --git a/api/helpers/favourite.js b/api/helpers/favourite.js new file mode 100644 index 0000000..7a63948 --- /dev/null +++ b/api/helpers/favourite.js @@ -0,0 +1,87 @@ +'use strict'; + +const playPause = require('./playpause'); +const commonFunctions = require('./commonFunctions'); +const Promise = require('bluebird'); +const favourites = require('../helpers/favourites'); +const debug = require('debug')('helpers:favourite'); + +function playFavourite(player, requestedFavourite, timeout) { + let trackChanged; + let changeStateResult; + const promiseTimeout = timeout || 30000; + + function onTransportStateChange(status) { + debug(`status changed in onTransportStateChange ${commonFunctions.returnFullObject(status)}`); + if (trackChanged instanceof Function) { + trackChanged(); + } + } + + return Promise.resolve() + .then(() => { + return favourites.getFavourite(player, requestedFavourite); + }) + .then((results) => { + if (results) { + debug('calling playPause.pause()'); + + return playPause.pause(player); + } + throw new Error('favourite not found'); + }) + .then(() => { + debug('calling player.coordinator.replaceWithFavorite()'); + player.on('transport-state', onTransportStateChange); + + return player.coordinator.replaceWithFavorite(requestedFavourite); + }) + .then((result) => { + changeStateResult = result; + if (result) { + debug('waiting for state change'); + + return new Promise((resolve) => { + trackChanged = resolve; + }); + } + + return true; + }) + .timeout(promiseTimeout) + .then(() => { + if (changeStateResult) { + debug('calling checkReturnStatus()'); + + return commonFunctions.checkReturnStatus(changeStateResult); + } + + debug('currently playing'); + + return 'currently playing'; + }) + .then((result) => { + if (result === 'currently playing') { + debug('favourite already playing so not doing anything'); + + return true; + } + debug('calling playPause.play()'); + + return playPause.play(player); + }) + .catch(Promise.TimeoutError, (error) => { + throw new Error(`timeout waiting for state change : ${error}`); + }) + .catch((error) => { + debug(`error in playFavourite() : ${commonFunctions.returnFullObject(error)}`); + throw error; + }) + .finally(() => { + player.removeListener('transport-state', onTransportStateChange); + }); +} + +module.exports = { + playFavourite +}; diff --git a/api/helpers/favourites.js b/api/helpers/favourites.js new file mode 100644 index 0000000..4fcb8af --- /dev/null +++ b/api/helpers/favourites.js @@ -0,0 +1,52 @@ +'use strict'; + +const commonFunctions = require('./commonFunctions'); +const debug = require('debug')('test:helpers:checkValidPlayer'); +const Promise = require('bluebird'); +const _ = require('lodash'); + +function getFavourites(player, detailed) { + return Promise.resolve() + .then(() => { + debug('calling player.system.getFavorites()'); + + return player.system.getFavorites(); + }) + .then((result) => { + if (detailed) { + return result; + } + + return result.map((favourite) => { + return { + title: favourite.title + }; + }); + }) + .catch((error) => { + debug(`got error : ${commonFunctions.returnFullObject(error)}`); + throw error; + }); +} + +function getFavourite(player, favouriteToLookFor) { + return Promise.resolve() + .then(() => { + debug('calling player.system.getFavorites()'); + + return player.system.getFavorites(); + }) + .then((result) => { + return _.find(result, (favourite) => { + return favouriteToLookFor.toLowerCase() === favourite.title.toLowerCase(); + }); + }) + .catch((error) => { + debug(`got error : ${commonFunctions.returnFullObject(error)}`); + throw error; + }); +} +module.exports = { + getFavourite, + getFavourites +}; diff --git a/api/helpers/linein.js b/api/helpers/linein.js new file mode 100644 index 0000000..f8bb4b6 --- /dev/null +++ b/api/helpers/linein.js @@ -0,0 +1,36 @@ +'use strict'; +const nowPlaying = require('./nowPlaying'); +const commonFunctions = require('./commonFunctions'); +const debug = require('debug')('helpers:linein'); +const Promise = require('bluebird'); + + +function setLinein(player, sourcePlayerName, timeout) { + const promiseTimeout = timeout || 20000; + + return Promise.resolve() + .then(() => { + if (sourcePlayerName) { + debug('calling getPlayer()'); + + return player.system.getPlayer(decodeURIComponent(sourcePlayerName)); + } + + return player; + }) + .then((lineinSourcePlayer) => { + const uri = `x-rincon-stream:${lineinSourcePlayer.uuid}`; + + debug('calling setNowPlaying()'); + + return nowPlaying.setNowPlaying(player, uri, '', promiseTimeout); + }) + .catch((error) => { + debug(`got error ${commonFunctions.returnFullObject(error)}`); + throw error; + }); +} + +module.exports = { + setLinein +}; diff --git a/api/helpers/music_services/iplayerStations.js b/api/helpers/music_services/iplayerStations.js new file mode 100644 index 0000000..3f612aa --- /dev/null +++ b/api/helpers/music_services/iplayerStations.js @@ -0,0 +1,240 @@ +const stations = { + national: [ + { + name: 'BBC Radio 1', + channelId: 'radio1' + }, + { + name: 'BBC Radio 1Xtra', + channelId: '1xtra' + }, + { + name: 'BBC Radio 2', + channelId: 'radio2' + }, + { + name: 'BBC Radio 3', + channelId: 'radio3' + }, + { + name: 'BBC Radio 4', + channelId: 'radio4' + }, + { + name: 'BBC Radio 4 Extra', + channelId: 'radio4' + }, + { + name: 'BBC Radio 5 Live', + channelId: 'fivelive' + }, + { + name: 'BBC Radio 5 live sports extra', + channelId: 'sportsextra' + }, + { + name: 'BBC Radio 6 music', + channelId: '6music' + }, + { + name: 'BBC Radio Asian Network', + channelId: 'asiannetwork' + }, + { + name: 'BBC World Service', + channelId: 'worldservice' + } + ], + regional: [ + { + name: 'BBC Radio Foyle', + channelId: 'radiofoyle' + }, + { + name: 'BBC Radio Scotland', + channelId: 'radioscotland' + }, + { + name: 'BBC Radio Nan Gaidheal', + channelId: 'bbc_radio_nan_gaidheal' + }, + { + name: 'BBC Radio Ulster', + channelId: 'radioulster' + }, + { + name: 'BBC Radio Wales', + channelId: 'radiowales' + }, + { + name: 'BBC Radio Cymru', + channelId: 'radiocymru' + } + ], + local: [ + { + name: 'BBC Radio Cumbria', + channelId: 'bbc_radio_cumbria' + }, + { + name: 'BBC Radio Newcastle', + channelId: 'bbc_radio_newcastle' + }, + { + name: 'BBC Radio Tees', + channelId: 'bbc_tees' + }, + { + name: 'BBC Radio Lancashire', + channelId: 'bbc_radio_lancashire' + }, + { + name: 'BBC Radio Merseyside', + channelId: 'bbc_radio_merseyside' + }, + { + name: 'BBC Radio Manchester', + channelId: 'bbc_radio_manchester' + }, + { + name: 'BBC Radio Leeds', + channelId: 'bbc_radio_leeds' + }, + { + name: 'BBC Radio Sheffield', + channelId: 'bbc_radio_sheffield' + }, + { + name: 'BBC Radio York', + channelId: 'bbc_radio_york' + }, + { + name: 'BBC Radio Humberside', + channelId: 'bbc_radio_humberside' + }, + { + name: 'BBC Radio Lincolnshire', + channelId: 'bbc_radio_lincolnshire' + }, + { + name: 'BBC Radio Nottingham', + channelId: 'bbc_radio_nottingham' + }, + { + name: 'BBC Radio Leicester', + channelId: 'bbc_radio_leicester' + }, + { + name: 'BBC Radio Derby', + channelId: 'bbc_radio_derby' + }, + { + name: 'BBC Radio Stoke', + channelId: 'bbc_radio_stoke' + }, + { + name: 'BBC Radio Shropshire', + channelId: 'bbc_radio_shropshire' + }, + { + name: 'BBC WM 95.6', + channelId: 'bbc_wm' + }, + { + name: 'BBC Radio Coventry & Warwickshire', + channelId: 'bbc_radio_coventry_warwickshire' + }, + { + name: 'BBC Radio Hereford & Worcester', + channelId: 'bbc_radio_hereford_worcester' + }, + { + name: 'BBC Radio Northampton', + channelId: 'bbc_radio_northampton' + }, + { + name: 'BBC Radio Three Counties Radio', + channelId: 'bbc_three_counties_radio' + }, + { + name: 'BBC Radio Cambridgeshire', + channelId: 'bbc_radio_cambridge' + }, + { + name: 'BBC Radio Norfolk', + channelId: 'bbc_radio_norfolk' + }, + { + name: 'BBC Radio Suffolk', + channelId: 'bbc_radio_suffolk' + }, + { + name: 'BBC Radio Essex', + channelId: 'bbc_radio_essex' + }, + { + name: 'BBC Radio London', + channelId: 'bbc_london' + }, + { + name: 'BBC Radio Kent', + channelId: 'bbc_radio_kent' + }, + { + name: 'BBC Radio Surrey', + channelId: 'bbc_radio_surrey' + }, + { + name: 'BBC Radio Sussex', + channelId: 'bbc_radio_sussex' + }, + { + name: 'BBC Radio Oxford', + channelId: 'bbc_radio_oxford' + }, + { + name: 'BBC Radio Berkshire', + channelId: 'bbc_radio_berkshire' + }, + { + name: 'BBC Radio Solent', + channelId: 'bbc_radio_solent' + }, + { + name: 'BBC Radio Gloucestershire', + channelId: 'bbc_radio_gloucestershire' + }, + { + name: 'BBC Radio Wiltshire', + channelId: 'bbc_radio_wiltshire' + }, + { + name: 'BBC Radio Bristol', + channelId: 'bbc_radio_bristol' + }, + { + name: 'BBC Radio Somerset', + channelId: 'bbc_radio_somerset_sound' + }, + { + name: 'BBC Radio Devon', + channelId: 'bbc_radio_devon' + }, + { + name: 'BBC Radio Cornwall', + channelId: 'bbc_radio_cornwall' + }, + { + name: 'BBC Radio Guernsey', + channelId: 'bbc_radio_guernsey' + }, + { + name: 'BBC Radio Jersey', + channelId: 'bbc_radio_jersey' + } + ] +}; + +module.exports = { + stations +}; diff --git a/api/helpers/music_services/iplayer_on_demand.js b/api/helpers/music_services/iplayer_on_demand.js new file mode 100644 index 0000000..61762ee --- /dev/null +++ b/api/helpers/music_services/iplayer_on_demand.js @@ -0,0 +1,224 @@ +'use strict'; + +const rp = require('request-promise'); +const xml2js = require('xml-to-json-promise'); +const Promise = require('bluebird'); +const _ = require('lodash'); +const log4js = require('log4js'); +const logger = log4js.getLogger('helpers/iplayer_on_damand.js'); +const stations = require('./iplayerStations').stations; + +function getFeed(channelId) { + return Promise.resolve() + .then(() => { + return rp(`http://www.bbc.co.uk/radio/aod/availability/${channelId}.xml`); + }) + .then((data) => { + return xml2js.xmlDataToJSON(data); + }) + .catch((error) => { + if (error.statusCode === 404) { + logger.error(`cant get feed for ${channelId}`); + } else { + logger.error(error); + } + + return null; + }); +} + +function refreshChannel(station, iplayerProgramDB) { + const currentDate = new Date(); + + return Promise.resolve() + .then(() => { + return iplayerProgramDB.removeAsync({ + channelId: station.channelId + }, { + multi: true + }); + }) + .then(() => { + return getFeed(station.channelId); + }) + .then((data) => { + if (data) { + return Promise.map(data.schedule.entry, (entry) => { + const availabilityStart = new Date(entry.availability[0].$.start); + const availabilityEnd = new Date(entry.availability[0].$.end); + + if (availabilityStart < currentDate && availabilityEnd > currentDate) { + const dataToInsert = { + channelId: station.channelId, + pid: entry.$.pid, + service: station.name, + synopsis: entry.synopsis[0], + title: entry.title[0], + imageUrl: entry.images[0].image[0], + broadcast: entry.broadcast[0].$.start, + duration: entry.broadcast[0].$.duration, + streamingLink: _.get(_.find(entry.links[0].link, (link) => { + return link.$.transferformat === 'hls'; + }), '_') + }; + + return iplayerProgramDB.insertAsync(dataToInsert); + } + + return null; + }); + } + + return null; + }); +} + +function refreshAllChannels(iplayerProgramDB, refreshSettingsDB) { + return Promise.resolve() + .then(() => { + return Promise.map(stations.national, (station) => { + return refreshChannel(station, iplayerProgramDB); + }); + }) + .then(() => { + return Promise.map(stations.regional, (station) => { + return refreshChannel(station, iplayerProgramDB); + }); + }) + .then(() => { + return Promise.map(stations.local, (station) => { + return refreshChannel(station, iplayerProgramDB); + }); + }) + .then(() => { + return refreshSettingsDB.findAsync({ + refreshType: 'iplayerOnDemand' + }); + }) + .then((result) => { + if (result.length > 0) { + return refreshSettingsDB.updateAsync({ + refreshType: 'iplayerOnDemand' + }, { + refreshType: 'iplayerOnDemand', + lastRefresh: new Date() + }); + } + + return refreshSettingsDB.insertAsync({ + refreshType: 'iplayerOnDemand', + lastRefresh: new Date() + }); + }); +} + +function refreshIfNeeded(iplayerProgramDB, refreshSettingsDB) { + return Promise.resolve() + .then(() => { + return refreshSettingsDB.findAsync({ + refreshType: 'iplayerOnDemand' + }); + }) + .then((result) => { + // Only refresh if last refresh was more than 24 hours ago + + if (result.length > 0) { + const lastRefreshTime = new Date(result[0].lastRefresh); + const yesterday = new Date(); + + yesterday.setDate(yesterday.getDate() - 1); + if (lastRefreshTime < yesterday) { + return true; + } + + return false; + } + + // Forec a refresh if it has never happened + return true; + }) + .then((doRefresh) => { + if (doRefresh === true) { + return refreshAllChannels(iplayerProgramDB, refreshSettingsDB); + } + + return null; + }); +} + +function search(player, type, query, offset, limit, settings) { + return Promise.resolve() + .then(() => { + return refreshIfNeeded(settings.dbSettings.iplayerProgramDB, settings.dbSettings.refreshSettingsDB); + }) + .then(() => { + const queryRegExp = new RegExp(query, 'i'); + let iplayerQuery; + + switch (type) { + case 'title': + iplayerQuery = { + title: { + $regex: queryRegExp + } + }; + break; + case 'synopsis': + iplayerQuery = { + synopsis: { + $regex: queryRegExp + } + }; + break; + default: + throw new Error('this search has not been implemented yet'); + } + + return settings.dbSettings.iplayerProgramDB.findAsync(iplayerQuery); + }) + .then((data) => { + const uniqueResults = _.uniqWith(data, (entry, otherValue) => { + return _.isEqual({ + title: entry.title, + synopsis: entry.synopsis + }, { + title: otherValue.title, + synopsis: otherValue.synopsis + }); + }); + const sortedResults = _.sortBy(uniqueResults, ['title', 'station', 'broadcast']); + const offsetResults = _.slice(sortedResults, offset || 0, limit + offset || 20); + const searchResults = _.map(offsetResults, (result) => { + return { + uri: result.streamingLink.replace('http://', 'x-rincon-mp3radio://'), + title: result.title, + type: 'iplayer stream', + metadata: `${result.title}object.item.audioItem.audioBroadcastSA_RINCON65031_`, + synopsis: result.synopsis, + station: result.service, + broadcast: result.broadcast, + duration: parseInt(result.duration) + }; + }); + const results = { + returned: searchResults.length, + start: offset || 0, + total: sortedResults.length, + items: searchResults + }; + + if (offset > 0) { + results.previous = `${settings.webroot}/search?service=iplayer&type=${type}&q=${query}&limit=${limit}&offset=${(offset - limit < 0 ? 0 : offset - limit)}`; + } + if (offset + searchResults.length < sortedResults.length) { + results.next = `${settings.webroot}/search?service=iplayer&type=${type}&q=${query}&limit=${limit}&offset=${(offset + limit)}`; + } + + return results; + }); +} + +module.exports = { + search, + refreshAllChannels +}; diff --git a/api/helpers/music_services/library.js b/api/helpers/music_services/library.js new file mode 100644 index 0000000..b560e6d --- /dev/null +++ b/api/helpers/music_services/library.js @@ -0,0 +1,85 @@ +'use strict'; + +const _ = require('lodash'); +const util = require('util'); +const log4js = require('log4js'); +const logger = log4js.getLogger('helpers/music_services/library.js'); + + +function tidyArray(items) { + const newItems = []; + const myDefault = { + album: '', + artist: '', + imageUrl: '', + metadata: '', + title: '', + uri: '' + }; + + _.forEach(items, (item) => { + const newItem = _.defaults(_(item) + .omit(_.isNull) + .value(), myDefault); + + newItem.albumTrackNumber = parseInt(item.albumTrackNumber); + newItem.imageUrl = item.albumArtUri; + newItems.push(newItem); + }); + + return newItems; +} + + +function search(player, type, query, offset, limit, settings) { + return Promise.resolve() + .then(() => { + let sonosQuery; + + switch (type) { + case 'song': + sonosQuery = `A:TRACKS: ${query}`; + break; + case 'artist': + sonosQuery = `A:ARTIST: ${query}`; + break; + case 'album': + sonosQuery = `A:ALBUM: ${query}`; + break; + default: + throw new Error('this search has not been implemented yet'); + } + + return player.browse(sonosQuery, offset, limit); + }) + .then((data) => { + const items = _.map(data.items, (value) => { + return _.assign(value, { + type + }); + }); + const result = { + items: tidyArray(items), + returned: parseInt(data.numberReturned), + start: parseInt(offset) || 0, + total: parseInt(data.totalMatches) + }; + + if (offset > 0) { + result.previous = `${settings.webroot}/search?service=library&type=${type}&q=${query}&limit=${limit}&offset=${(offset - limit < 0 ? 0 : offset - limit)}`; + } + if (offset + parseInt(data.numberReturned) < parseInt(data.totalMatches)) { + result.next = `${settings.webroot}/search?service=library&type=${type}&q=${query}&limit=${limit}&offset=${(offset + limit)}`; + } + + return result; + }) + .catch((err) => { + logger.error(util.inspect(err, null, false)); + throw err; + }); +} + +module.exports = { + search +}; diff --git a/api/helpers/music_services/spotify.js b/api/helpers/music_services/spotify.js new file mode 100644 index 0000000..ccd9159 --- /dev/null +++ b/api/helpers/music_services/spotify.js @@ -0,0 +1,183 @@ +'use strict'; + +const SpotifyWebApi = require('spotify-web-api-node'); +const _ = require('lodash'); +const util = require('util'); +const log4js = require('log4js'); +const logger = log4js.getLogger('helpers/music_services/spotify.js'); + +const spotifyDef = { + country: '&market=', + metastart: { + album: '0004206cspotify%3aalbum%3a', + artist: '000c206cspotify%3aartistRadio%3a', + song: '00032020spotify%3atrack%3a', + artisttoptracks: '000e206cspotify%3aartistTopTracks%3a' + }, + object: { + album: 'container.album.musicAlbum', + artist: 'item.audioItem.audioBroadcast.#artistRadio', + song: 'item.audioItem.musicTrack', + artisttoptracks: 'container.playlistContainer' + }, + parent: { + album: '00020000album:', + artist: '00052064spotify%3aartist%3a', + song: '00020000track:', + artisttoptracks: '00052064spotify%3aartist%3a' + } +}; + + +let sid = ''; +let serviceType = ''; + +function getURI(type, id) { + const accountSN = '5'; + + switch (type) { + case 'album': + return `x-rincon-cpcontainer:0004206cspotify%3aalbum%3a${id}`; + case 'song': + return `x-sonos-spotify:spotify%3atrack%3a${id}?sid=${sid}&flags=8224&sn=${accountSN}`; + case 'artist': + return `x-sonosapi-radio:spotify%3aartistRadio%3a${id}?sid=${sid}&flags=8300&sn=${accountSN}`; + case 'artisttoptracks': + return `x-rincon-cpcontainer:000e206cspotify%3aartistTopTracks%3a${id}`; + default: + throw new Error('URI type not defined in getURI'); + + } +} + +function getServiceToken() { + return `SA_RINCON${serviceType}_X_#Svc${serviceType}-0-Token`; +} + + +function getMetadata(type, id, name, title) { + const token = getServiceToken(); + const parentUri = spotifyDef.parent[type] + name; + const objectType = spotifyDef.object[type]; + let metaTitle = title; + + if (type !== 'station') { + metaTitle = `${title} radio`; + } + + return ` + ${metaTitle}object.${objectType} + ${token}`; +} + +function getReturnResults(data, type) { + const items = []; + let itemsReturned = 0; + let MetadataID = ''; + + if (data.total > 0) { + data.items.forEach((item) => { + const itemResult = {}; + + itemResult.title = item.name; + if (type === 'song') { + itemResult.artist = item.artists[0].name; + itemResult.album = item.album.name; + itemResult.imageUrl = item.album.images[0].url; + itemResult.albumTrackNumber = item.track_number; + } + if (type === 'album') { + itemResult.artist = item.artists[0].name; + itemResult.album = item.name; + itemResult.imageUrl = item.images[0].url; + } + if (type === 'artist') { + itemResult.artist = item.name; + itemResult.album = item.name; + itemResult.imageUrl = _.get(item, 'images[0].url', ''); + itemResult.artistId = item.id; + } + if (type === 'artisttoptracks') { + itemResult.artist = item.name; + itemResult.album = item.name; + itemResult.imageUrl = _.get(item, 'images[0].url', ''); + itemResult.artistId = item.id; + } + itemResult.type = type; + itemResult.uri = getURI(type, encodeURIComponent(item.id)); + MetadataID = spotifyDef.metastart[type] + encodeURIComponent(item.id); + itemResult.metadata = getMetadata(type, MetadataID, item.name.toLowerCase(), item.name); + items.push(itemResult); + itemsReturned += 1; + }); + } + const result = { + items, + returned: itemsReturned, + total: data.total + }; + + return result; +} + +function search(player, type, query, offset, limit, settings) { + const spotifyApi = new SpotifyWebApi(); + const options = { + limit, + market: 'GB', + offset + }; + + sid = player.system.getServiceId('Spotify'); + serviceType = player.system.getServiceType('Spotify'); + + return Promise.resolve() + .then(() => { + switch (type) { + case 'song': + return spotifyApi.searchTracks(`track:${query}`, options); + case 'artist': + return spotifyApi.searchArtists(`artist:${query}`, options); + case 'artisttoptracks': + return spotifyApi.searchArtists(`artist:${query}`, options); + case 'album': + return spotifyApi.searchAlbums(`album:${query}`, options); + default: + throw new Error('this search has not been implemented yet'); + } + }) + .then((data) => { + switch (type) { + case 'song': + return getReturnResults(data.body.tracks, 'song'); + case 'artist': + return getReturnResults(data.body.artists, 'artist'); + case 'artisttoptracks': + return getReturnResults(data.body.artists, 'artisttoptracks'); + case 'album': + return getReturnResults(data.body.albums, 'album'); + default: + throw new Error('this search has not been implemented yet'); + } + }) + .then((result) => { + result.start = parseInt(offset); + if (offset > 0) { + result.previous = `${settings.webroot}/search?service=spotify&type=${type}&q=${query}&limit=${limit}&offset=${(offset - limit < 0 ? 0 : offset - limit)}`; + } + if (offset + result.returned < result.total) { + result.next = `${settings.webroot}/search?service=spotify&type=${type}&q=${query}&limit=${limit}&offset=${(offset + limit)}`; + } + + return result; + }) + .catch((err) => { + logger.error(util.inspect(err, null, false)); + throw err; + }); +} + +module.exports = { + search +}; diff --git a/api/helpers/mute.js b/api/helpers/mute.js new file mode 100644 index 0000000..c8ccb99 --- /dev/null +++ b/api/helpers/mute.js @@ -0,0 +1,143 @@ +'use strict'; + +const commonFunctions = require('./commonFunctions'); +const debug = require('debug')('helpers:mute'); +const Promise = require('bluebird'); +const state = require('./state'); + + +function setMuteStatus(player, muteStatus, timeout) { + let muteChanged; + let changeStateResult; + const promiseTimeout = timeout || 20000; + + function onMuteChange(status) { + debug(`mute status changed in onMuteChange ${commonFunctions.returnFullObject(status)}`); + if (muteChanged instanceof Function && status.previousMute !== status.newMute) { + muteChanged(); + } + } + + return Promise.resolve() + .then(() => { + return state.getPlayerState(player); + }) + .then((currentState) => { + if (currentState.mute === muteStatus) { + debug('already at requested state so not doing anything'); + throw new Error('already at state'); + } + player.on('mute-change', onMuteChange); + if (muteStatus === 'mute on') { + debug('calling player.mute'); + + return player.mute(); + } + if (muteStatus === 'mute off') { + debug('calling player.unMute'); + + return player.unMute(); + } + + throw new Error(`invalid mutestatus : ${muteStatus}`); + }) + .then((result) => { + debug('got return status - waiting for onMuteChange'); + changeStateResult = result; + + return new Promise((resolve) => { + muteChanged = resolve; + }); + }) + .timeout(promiseTimeout) + .then(() => { + debug('checking return status'); + + return commonFunctions.checkReturnStatus(changeStateResult); + }) + .catch(Promise.TimeoutError, (error) => { + debug(`got error ${commonFunctions.returnFullObject(error)}`); + throw new Error(`timeout waiting for state change : ${error}`); + }) + .catch((error) => { + if (error.message === 'already at state') { + return; + } + debug(`got error ${commonFunctions.returnFullObject(error)}`); + throw error; + }) + .finally(() => { + player.removeListener('mute-change', onMuteChange); + }); +} + +function setGroupMuteStatus(player, muteStatus, timeout) { + let muteChanged; + let changeStateResult; + const promiseTimeout = timeout || 20000; + + function onMuteChange(status) { + debug(`mute status changed in onMuteChange ${commonFunctions.returnFullObject(status)}`); + if (muteChanged instanceof Function && status.previousMute !== status.newMute) { + muteChanged(); + } + } + + return Promise.resolve() + .then(() => { + return state.simplifyPlayer(player); + }) + .then((currentState) => { + if (currentState.groupState.mute === muteStatus) { + debug('already at requested state so not doing anything'); + throw new Error('already at state'); + } + player.on('group-mute', onMuteChange); + if (muteStatus === 'mute on') { + debug('calling player.coordinator.mute'); + + return player.coordinator.muteGroup(); + } + if (muteStatus === 'mute off') { + debug('calling player.coordinator.unMute'); + + return player.coordinator.unMuteGroup(); + } + + throw new Error(`invalid mutestatus : ${muteStatus}`); + }) + .then((result) => { + debug('got return status - waiting for onMuteChange'); + changeStateResult = result; + + return new Promise((resolve) => { + muteChanged = resolve; + }); + }) + .timeout(promiseTimeout) + .then(() => { + debug('checking return status'); + + return commonFunctions.checkReturnStatus(changeStateResult); + }) + .catch(Promise.TimeoutError, (error) => { + debug(`got error ${commonFunctions.returnFullObject(error)}`); + throw new Error(`timeout waiting for state change : ${error}`); + }) + .catch((error) => { + if (error.message === 'already at state') { + return; + } + debug(`got error ${commonFunctions.returnFullObject(error)}`); + throw error; + }) + .finally(() => { + player.removeListener('group-mute', onMuteChange); + }); +} + + +module.exports = { + setGroupMuteStatus, + setMuteStatus +}; diff --git a/api/helpers/nextprevious.js b/api/helpers/nextprevious.js new file mode 100644 index 0000000..120e25f --- /dev/null +++ b/api/helpers/nextprevious.js @@ -0,0 +1,108 @@ +'use strict'; +const commonFunctions = require('./commonFunctions'); +const state = require('./state'); +const Promise = require('bluebird'); +const debug = require('debug')('helpers:nextPrevious'); + +function next(player, timeout) { + let trackChanged; + let changeStateResult; + const promiseTimeout = timeout || 20000; + + function onTransportStateChange(status) { + debug(`status changed in onTransportStateChange ${commonFunctions.returnFullObject(status)}`); + if (trackChanged instanceof Function) { + trackChanged(); + } + } + + return Promise.resolve() + .then(() => { + return state.getPlayerState(player); + }) + .then((currentState) => { + if (commonFunctions.isRadioOrLineIn(currentState.currentTrack.uri)) { + throw new Error('cant skip to next in current state'); + } + player.on('transport-state', onTransportStateChange); + debug('calling player.coordinator.nextTrack()'); + + return player.coordinator.nextTrack(); + }) + .then((result) => { + changeStateResult = result; + + return new Promise((resolve) => { + trackChanged = resolve; + }); + }) + .then(() => { + return commonFunctions.checkReturnStatus(changeStateResult); + }) + .timeout(promiseTimeout) + .catch(Promise.TimeoutError, (error) => { + debug(`got error ${commonFunctions.returnFullObject(error)}`); + throw new Error(`timeout waiting for state change : ${error}`); + }) + .catch((error) => { + debug(`error in next() : ${commonFunctions.returnFullObject(error)}`); + throw error; + }) + .finally(() => { + player.removeListener('transport-state', onTransportStateChange); + }); +} + +function previous(player, timeout) { + let trackChanged; + let changeStateResult; + const promiseTimeout = timeout || 20000; + + function onTransportStateChange(status) { + debug(`status changed in onTransportStateChange ${commonFunctions.returnFullObject(status)}`); + if (trackChanged instanceof Function) { + trackChanged(); + } + } + + return Promise.resolve() + .then(() => { + return state.getPlayerState(player); + }) + .then((currentState) => { + if (commonFunctions.isRadioOrLineIn(currentState.currentTrack.uri)) { + throw new Error('cant skip to previous in current state'); + } + player.on('transport-state', onTransportStateChange); + debug('calling player.coordinator.previousTrack()'); + + return player.coordinator.previousTrack(); + }) + .then((result) => { + changeStateResult = result; + + return new Promise((resolve) => { + trackChanged = resolve; + }); + }) + .timeout(promiseTimeout) + .then(() => { + return commonFunctions.checkReturnStatus(changeStateResult); + }) + .catch(Promise.TimeoutError, (error) => { + debug(`got error ${commonFunctions.returnFullObject(error)}`); + throw new Error(`timeout waiting for state change : ${error}`); + }) + .catch((error) => { + debug(`error in previous() : ${commonFunctions.returnFullObject(error)}`); + throw error; + }) + .finally(() => { + player.removeListener('transport-state', onTransportStateChange); + }); +} + +module.exports = { + next, + previous +}; diff --git a/api/helpers/nowPlaying.js b/api/helpers/nowPlaying.js new file mode 100644 index 0000000..1e2728b --- /dev/null +++ b/api/helpers/nowPlaying.js @@ -0,0 +1,73 @@ +'use strict'; +const commonFunctions = require('./commonFunctions'); +const debug = require('debug')('helpers:nowPlaying'); +const Promise = require('bluebird'); + +function getNowPlaying(player) { + return Promise.resolve() + .then(() => { + debug('getting player.state.currentTrack'); + const currentState = player.state.currentTrack; + + currentState.uriMetadata = player.avTransportUriMetadata; + currentState.avTransportUri = player.avTransportUri; + + return currentState; + }) + .catch((error) => { + debug(`got error ${commonFunctions.returnFullObject(error)}`); + throw error; + }); +} + +function setNowPlaying(player, uri, metadata, timeout) { + let trackChanged; + let changeStateResult; + const promiseTimeout = timeout || 20000; + + function onTransportStateChange(status) { + debug(`status changed in onTransportStateChange ${commonFunctions.returnFullObject(status)}`); + if (trackChanged instanceof Function) { + trackChanged(); + } + } + + return Promise.resolve() + .then(() => { + player.on('transport-state', onTransportStateChange); + debug('calling player.coordinator.setAVTransport()'); + + return player.coordinator.setAVTransport(uri, metadata); + }) + .then((result) => { + debug('waiting for state change'); + changeStateResult = result; + + return new Promise((resolve) => { + trackChanged = resolve; + }); + }) + .timeout(promiseTimeout) + .then(() => { + return commonFunctions.checkReturnStatus(changeStateResult); + }) + .then(() => { + return getNowPlaying(player); + }) + .catch(Promise.TimeoutError, (error) => { + debug(`got error ${commonFunctions.returnFullObject(error)}`); + throw new Error(`timeout waiting for state change : ${error}`); + }) + .catch((error) => { + debug(`got error ${commonFunctions.returnFullObject(error)}`); + throw error; + }) + .finally(() => { + player.removeListener('transport-state', onTransportStateChange); + }); +} + +module.exports = { + getNowPlaying, + setNowPlaying +}; diff --git a/api/helpers/players.js b/api/helpers/players.js new file mode 100644 index 0000000..0d97264 --- /dev/null +++ b/api/helpers/players.js @@ -0,0 +1,96 @@ +'use strict'; + +const commonFunctions = require('./commonFunctions'); +const debug = require('debug')('helpers:players'); +const Promise = require('bluebird'); +const state = require('./state'); +const _ = require('lodash'); + +function isValidPlayer(discovery, playerName) { + return Promise.resolve() + .then(() => { + if (discovery.zones.length === 0) { + throw new Error('No system has yet been discovered'); + } + + return; + }) + .then(() => { + const player = discovery.getPlayer(playerName); + + if (typeof player === 'undefined') { + return false; + } + + return true; + }); +} + + +function getPlayers(discovery) { + return Promise.resolve() + .then(() => { + if (discovery.zones.length === 0) { + throw new Error('No system has yet been discovered'); + } + + return; + }) + .then(() => { + const player = discovery.getAnyPlayer(); + + return player.system.zones; + }) + .then((returnedZones) => { + const rooms = _.map(returnedZones, (value) => { + return value.members; + }); + + return _.flatten(rooms); + }) + .then((rooms) => { + return Promise.all(_.map(rooms, (room) => { + return state.simplifyPlayer(room); + })); + }) + .catch((error) => { + debug(`error in getPlayers() : ${commonFunctions.returnFullObject(error)}`); + + throw error; + }); +} + +function getPlayer(discovery, playerName) { + let player; + + return Promise.resolve() + .then(() => { + if (discovery.zones.length === 0) { + throw new Error('No system has yet been discovered'); + } + + return; + }) + .then(() => { + return isValidPlayer(discovery, playerName); + }) + .then((isValidPlayerResult) => { + if (!isValidPlayerResult) { + throw new Error(`${playerName} is not a valid player`); + } + player = discovery.getPlayer(playerName); + + return state.simplifyPlayer(player); + }) + .catch((error) => { + debug(`error in getPlayers() : ${commonFunctions.returnFullObject(error)}`); + + throw error; + }); +} + +module.exports = { + getPlayer, + getPlayers, + isValidPlayer +}; diff --git a/api/helpers/playlist.js b/api/helpers/playlist.js new file mode 100644 index 0000000..006f505 --- /dev/null +++ b/api/helpers/playlist.js @@ -0,0 +1,64 @@ +'use strict'; +const playPause = require('./playpause'); +const commonFunctions = require('./commonFunctions'); +const Promise = require('bluebird'); +const debug = require('debug')('helpers:playlist'); + + +function playPlaylist(player, playlistName, timeout) { + let trackChanged; + let changeStateResult; + const promiseTimeout = timeout || 20000; + + function onTransportStateChange(status) { + debug(`status changed in onTransportStateChange ${commonFunctions.returnFullObject(status)}`); + if (trackChanged instanceof Function) { + trackChanged(); + } + } + + return Promise.resolve() + .then(() => { + debug('calling playPause.pause()'); + + return playPause.pause(player); + }) + .then(() => { + debug('calling player.coordinator.replaceWithPlaylist()'); + player.on('transport-state', onTransportStateChange); + + return player.coordinator.replaceWithPlaylist(playlistName); + }) + .then((result) => { + changeStateResult = result; + debug('waiting for state change'); + + return new Promise((resolve) => { + trackChanged = resolve; + }); + }) + .timeout(promiseTimeout) + .then(() => { + return commonFunctions.checkReturnStatus(changeStateResult); + }) + .then(() => { + debug('calling playPause.play()'); + + return playPause.play(player); + }) + .catch(Promise.TimeoutError, (error) => { + throw new Error(`timeout waiting for state change : ${error}`); + }) + .catch((error) => { + debug(`error in playFavourite() : ${commonFunctions.returnFullObject(error)}`); + + throw error; + }) + .finally(() => { + player.removeListener('transport-state', onTransportStateChange); + }); +} + +module.exports = { + playPlaylist +}; diff --git a/api/helpers/playmode.js b/api/helpers/playmode.js new file mode 100644 index 0000000..59e4402 --- /dev/null +++ b/api/helpers/playmode.js @@ -0,0 +1,200 @@ +'use strict'; +const commonFunctions = require('./commonFunctions'); +const Promise = require('bluebird'); +const debug = require('debug')('helpers:playmode'); +const state = require('./state'); + +function setRepeatStatus(player, requestedState, timeout) { + let trackChanged; + let changeStateResult; + const promiseTimeout = timeout || 20000; + + function onTransportStateChange(status) { + debug(`status changed in onTransportStateChange ${commonFunctions.returnFullObject(status)}`); + if (trackChanged instanceof Function) { + trackChanged(); + } + } + + return Promise.resolve() + .then(() => { + if (requestedState !== 'all' && requestedState !== 'none' && requestedState !== 'one') { + throw new Error(`invalid repeat status : ${requestedState}`); + } + debug('calling state.getPlayerState()'); + + return state.getPlayerState(player); + }) + .then((currentState) => { + if (currentState.playMode.repeat === requestedState) { + debug(`already at status ${requestedState} so exiting`); + + throw new Error('already at requested status'); + } + player.on('transport-state', onTransportStateChange); + debug(`calling player.coordinator.repeat(${requestedState})`); + + return player.coordinator.repeat(requestedState); + }) + .then((result) => { + debug('waiting for state change'); + changeStateResult = result; + + return new Promise((resolve) => { + trackChanged = resolve; + }); + }) + .timeout(promiseTimeout) + .then(() => { + return commonFunctions.checkReturnStatus(changeStateResult); + }) + .catch(Promise.TimeoutError, (error) => { + debug(`got error ${commonFunctions.returnFullObject(error)}`); + throw new Error(`timeout waiting for state change : ${error}`); + }) + .catch((error) => { + if (error.message === 'already at requested status') { + return true; + } + debug(`error in setRepeatStatus() : ${commonFunctions.returnFullObject(error)}`); + throw error; + }) + .finally(() => { + player.removeListener('transport-state', onTransportStateChange); + }); +} + +function setShuffleStatus(player, requestedState, timeout) { + let trackChanged; + let changeStateResult; + const promiseTimeout = timeout || 20000; + + function onTransportStateChange(status) { + debug(`status changed in onTransportStateChange ${commonFunctions.returnFullObject(status)}`); + if (trackChanged instanceof Function) { + trackChanged(); + } + } + + + return Promise.resolve() + .then(() => { + if (requestedState !== 'shuffle on' && requestedState !== 'shuffle off') { + throw new Error(`invalid shuffle status : ${requestedState}`); + } + + return state.getPlayerState(player); + }) + .then((currentState) => { + if (currentState.playMode.shuffle === requestedState) { + debug(`already at status ${requestedState} so exiting`); + + throw new Error('already at requested status'); + } + player.on('transport-state', onTransportStateChange); + if (requestedState === 'shuffle on') { + debug('calling player.coordinator.shuffle(true)'); + + return player.coordinator.shuffle(1); + } + debug('calling player.coordinator.shuffle(false)'); + + return player.coordinator.shuffle(0); + }) + .then((result) => { + debug('waiting for state change'); + changeStateResult = result; + + return new Promise((resolve) => { + trackChanged = resolve; + }); + }) + .timeout(promiseTimeout) + .then(() => { + return commonFunctions.checkReturnStatus(changeStateResult); + }) + .catch(Promise.TimeoutError, (error) => { + debug(`got error ${commonFunctions.returnFullObject(error)}`); + throw new Error(`timeout waiting for state change : ${error}`); + }) + .catch((error) => { + if (error.message === 'already at requested status') { + return true; + } + debug(`error in setShuffleStatus() : ${commonFunctions.returnFullObject(error)}`); + throw error; + }) + .finally(() => { + player.removeListener('transport-state', onTransportStateChange); + }); +} + +function setCrossfadeStatus(player, requestedState, timeout) { + let trackChanged; + let changeStateResult; + const promiseTimeout = timeout || 20000; + + function onTransportStateChange(status) { + debug(`status changed in onTransportStateChange ${commonFunctions.returnFullObject(status)}`); + if (trackChanged instanceof Function) { + trackChanged(); + } + } + + return Promise.resolve() + .then(() => { + if (requestedState !== 'crossfade on' && requestedState !== 'crossfade off') { + throw new Error(`invalid crossfade status : ${requestedState}`); + } + + return state.getPlayerState(player); + }) + .then((currentState) => { + if (currentState.playMode.crossfade === requestedState) { + debug(`already at status ${requestedState} so exiting`); + + throw new Error('already at requested status'); + } + player.on('transport-state', onTransportStateChange); + if (requestedState === 'crossfade on') { + debug('calling player.coordinator.crossfade(true)'); + + return player.coordinator.crossfade(1); + } + debug('calling player.coordinator.crossfade(false)'); + + return player.coordinator.crossfade(0); + }) + .then((result) => { + debug('waiting for state change'); + changeStateResult = result; + + return new Promise((resolve) => { + trackChanged = resolve; + }); + }) + .timeout(promiseTimeout) + .then(() => { + return commonFunctions.checkReturnStatus(changeStateResult); + }) + .catch(Promise.TimeoutError, (error) => { + debug(`got error ${commonFunctions.returnFullObject(error)}`); + throw new Error(`timeout waiting for state change : ${error}`); + }) + .catch((error) => { + if (error.message === 'already at requested status') { + return true; + } + debug(`error in setCrossfadeStatus() : ${commonFunctions.returnFullObject(error)}`); + throw error; + }) + .finally(() => { + player.removeListener('transport-state', onTransportStateChange); + }); +} + +module.exports = { + setCrossfadeStatus, + setRepeatStatus, + setShuffleStatus +}; diff --git a/api/helpers/playpause.js b/api/helpers/playpause.js new file mode 100644 index 0000000..c49fc24 --- /dev/null +++ b/api/helpers/playpause.js @@ -0,0 +1,206 @@ +'use strict'; +const commonFunctions = require('./commonFunctions'); +const Promise = require('bluebird'); +const debug = require('debug')('helpers:playpause'); +const state = require('./state'); + + +function play(player, timeout) { + let trackChanged; + let changeStateResult; + const promiseTimeout = timeout || 20000; + + function onTransportStateChange(status) { + debug(`status changed in onTransportStateChange ${commonFunctions.returnFullObject(status)}`); + if (trackChanged instanceof Function) { + trackChanged(); + } + } + + + return Promise.resolve() + .then(() => { + return state.getPlayerState(player); + }) + .then((currentState) => { + if (currentState.playbackState === 'play') { + debug('already playing so not doing anything'); + throw new Error('already playing'); + } + player.on('transport-state', onTransportStateChange); + debug('calling player.coordinator.play()'); + + return player.coordinator.play(); + }) + .then((result) => { + changeStateResult = result; + + if (player.coordinator.state.playbackState !== 'PLAYING') { + debug('currently not playing so waiting for state change'); + + return new Promise((resolve) => { + trackChanged = resolve; + }); + } + debug('currently playing so not waiting for state change'); + + return true; + }) + .timeout(promiseTimeout) + .then(() => { + return commonFunctions.checkReturnStatus(changeStateResult); + }) + .catch(Promise.TimeoutError, (error) => { + debug(`got error ${commonFunctions.returnFullObject(error)}`); + throw new Error(`timeout waiting for state change : ${error}`); + }) + .catch((error) => { + if (error.message === 'already playing') { + return; + } + debug(`error in play() : ${commonFunctions.returnFullObject(error)}`); + throw error; + }) + .finally(() => { + player.removeListener('transport-state', onTransportStateChange); + }); +} + +function playAsync(player) { + return Promise.resolve() + .then(() => { + debug('calling player.coordinator.play()'); + + return player.coordinator.play(); + }) + .then((changeStateResult) => { + return commonFunctions.checkReturnStatus(changeStateResult); + }) + .catch((error) => { + debug(`error in play() : ${commonFunctions.returnFullObject(error)}`); + throw error; + }); +} + +function pause(player, timeout) { + let trackChanged; + let changeStateResult; + const promiseTimeout = timeout || 20000; + + function onTransportStateChange(status) { + debug(`status changed in onTransportStateChange ${commonFunctions.returnFullObject(status)}`); + if (trackChanged instanceof Function) { + trackChanged(); + } + } + + return Promise.resolve() + .then(() => { + return state.getPlayerState(player); + }) + .then((currentState) => { + if (currentState.playbackState === 'pause') { + debug('already paused so not doing anything'); + throw new Error('already paused'); + } + player.on('transport-state', onTransportStateChange); + debug('calling player.coordinator.pause()'); + + return player.coordinator.pause(); + }) + .then((result) => { + changeStateResult = result; + + if (player.coordinator.state.playbackState === 'PLAYING') { + debug('currently playing so waiting for state change'); + + return new Promise((resolve) => { + trackChanged = resolve; + }); + } + debug('currently not playing so not waiting for state change'); + + return true; + }) + .timeout(promiseTimeout) + .then(() => { + return commonFunctions.checkReturnStatus(changeStateResult); + }) + .catch(Promise.TimeoutError, (error) => { + debug(`got error ${commonFunctions.returnFullObject(error)}`); + throw new Error(`timeout waiting for state change : ${error}`); + }) + .catch((error) => { + if (error.message === 'already paused') { + return; + } + debug(`error in pause() : ${commonFunctions.returnFullObject(error)}`); + throw error; + }) + .finally(() => { + player.removeListener('transport-state', onTransportStateChange); + }); +} + +function getPlaystate(player) { + return Promise.resolve() + .then(() => { + return player.coordinator.state.playbackState; + }) + .catch((err) => { + debug(`Error in getPlaystate() : ${commonFunctions.returnFullObject(err)}`); + throw err; + }); +} + +function togglePlayPause(player) { + return Promise.resolve() + .then(() => { + if (player.coordinator.state.playbackState === 'PLAYING') { + debug('currently playing so calling pause()'); + + return pause(player); + } + debug('currently not playing so calling play()'); + + return play(player); + }) + .catch((err) => { + debug(`Error in playpause() : ${commonFunctions.returnFullObject(err)}`); + throw err; + }); +} + +function setPlaybackState(player, playbackState) { + return Promise.resolve() + .then(() => { + if (playbackState === 'pause') { + debug('calling pause()'); + + return pause(player); + } else if (playbackState === 'play') { + debug('calling play()'); + + return play(player); + } else if (playbackState === 'toggle') { + debug('calling togglePlayPause()'); + + return togglePlayPause(player); + } + + throw new Error('invalid playback state'); + }) + .catch((err) => { + debug(`Error in playpause() : ${commonFunctions.returnFullObject(err)}`); + throw err; + }); +} + +module.exports = { + getPlaystate, + pause, + play, + playAsync, + setPlaybackState, + togglePlayPause +}; diff --git a/api/helpers/queue.js b/api/helpers/queue.js new file mode 100644 index 0000000..c7b0075 --- /dev/null +++ b/api/helpers/queue.js @@ -0,0 +1,210 @@ +'use strict'; +const commonFunctions = require('./commonFunctions'); +const playPause = require('./playpause'); +const nowPlaying = require('./nowPlaying'); +const seek = require('./seek'); +const state = require('./state'); +const Promise = require('bluebird'); +const debug = require('debug')('helpers:queue'); +const _ = require('lodash'); + +function simplify(items) { + return items + .map((item) => { + return { + album: item.album, + albumArtUri: item.albumArtUri, + artist: item.artist, + title: item.title + }; + }); +} + +function getQueue(player, detailed) { + return Promise.resolve() + .then(() => { + const queue = player.coordinator.getQueue(); + + return queue; + }) + .then((result) => { + if (detailed) { + return result; + } + + return simplify(result); + }) + .catch((err) => { + debug(`Error in getQueue() : ${commonFunctions.returnFullObject(err)}`); + throw err; + }); +} + +function addToQueue(player, uri, metadata, desiredFirstTrackNumberEnqueued, enqueAsNext) { + return Promise.resolve() + .then(() => { + return player.coordinator.addURIToQueue(uri, metadata, enqueAsNext, desiredFirstTrackNumberEnqueued); + }) + .catch((err) => { + debug(`Error in addToQueue() : ${commonFunctions.returnFullObject(err)}`); + throw err; + }); +} + +function addMultipleItemsToQueue(player, items) { + return Promise.resolve() + .then(() => { + const remapedItems = []; + + _.forEach(items, (item) => { + remapedItems.push([item.uri, item.metadata]); + }); + + return remapedItems; + }) + .then((remapedItems) => { + return player.coordinator.addMultipleURIsToQueue(remapedItems, ''); + }) + .catch((err) => { + debug(`Error in addMultipleItemsToQueue() : ${commonFunctions.returnFullObject(err)}`); + throw err; + }); +} + +function clearQueue(player) { + return Promise.resolve() + .then(() => { + return player.coordinator.clearQueue(); + }) + .then((result) => { + return commonFunctions.checkReturnStatus(result); + }) + .catch((err) => { + debug(`Error in clearQueue() : ${commonFunctions.returnFullObject(err)}`); + throw err; + }); +} + +function replaceQueue(player, uri, metadata) { + return Promise.resolve() + .then(() => { + return clearQueue(player); + }) + .then(() => { + return addToQueue(player, uri, metadata); + }) + .catch((err) => { + debug(`Error in replaceQueue() : ${commonFunctions.returnFullObject(err)}`); + throw err; + }); +} + +function replaceQueueAndPlay(player, uri, metadata) { + let returnData; + + return Promise.resolve() + .then(() => { + return replaceQueue(player, uri, metadata); + }) + .then((result) => { + returnData = result; + + return state.getPlayerState(player); + }) + .then((currentState) => { + if (currentState.type === 'track') { + return playPause.pause(player); + } + + return true; + }) + .then(() => { + const nowPlayinguri = 'x-rincon-queue:RINCON_000E58C4373C01400#0'; + + debug('calling setNowPlaying()'); + + return nowPlaying.setNowPlaying(player, nowPlayinguri, ''); + }) + .then(() => { + return playPause.play(player); + }) + .then(() => { + return returnData; + }) + .catch((err) => { + debug(`Error in replaceQueue() : ${commonFunctions.returnFullObject(err)}`); + throw err; + }); +} +function addToQueueAndPlay(player, uri, metadata) { + let returnData; + + return Promise.resolve() + .then(() => { + return addToQueue(player, uri, metadata, player.state.trackNo + 1, true); + }) + .then((result) => { + returnData = result; + + return playPause.pause(player); + }) + .then(() => { + const nowPlayinguri = 'x-rincon-queue:RINCON_000E58C4373C01400#0'; + + debug('calling setNowPlaying()'); + + return nowPlaying.setNowPlaying(player, nowPlayinguri, ''); + }) + .then(() => { + return seek.trackSeek(player, returnData.firsttracknumberenqueued); + }) + .then(() => { + return playPause.play(player); + }) + .then(() => { + return returnData; + }) + .catch((err) => { + debug(`Error in replaceQueue() : ${commonFunctions.returnFullObject(err)}`); + throw err; + }); +} + +function replaceQueueWithMultipleItems(player, items) { + const initialState = player.state; + let returnData; + + return Promise.resolve() + .then(() => { + return clearQueue(player); + }) + .then(() => { + return addMultipleItemsToQueue(player, items); + }) + .then((result) => { + returnData = result; + if (initialState.playbackState === 'PLAYING') { + return playPause.play(player); + } + + return 1; + }) + .then(() => { + return returnData; + }) + .catch((err) => { + debug(`Error in replaceQueue() : ${commonFunctions.returnFullObject(err)}`); + throw err; + }); +} + +module.exports = { + addToQueue, + clearQueue, + getQueue, + replaceQueue, + replaceQueueAndPlay, + addMultipleItemsToQueue, + replaceQueueWithMultipleItems, + addToQueueAndPlay +}; diff --git a/api/helpers/say.js b/api/helpers/say.js new file mode 100644 index 0000000..8989ce1 --- /dev/null +++ b/api/helpers/say.js @@ -0,0 +1,44 @@ +'use strict'; +const clip = require('./clip'); +const Promise = require('bluebird'); +const util = require('util'); +const log4js = require('log4js'); +const logger = log4js.getLogger('helpers/say.js'); +const voicerss = require('./tts_services/voicerss'); +const google = require('./tts_services/google'); + +function playText(player, text, language, volume, settings) { + const apiKey = settings.voicerssApiKey; + const ttsProvider = settings.ttsProvider; + const staticWebRootPath = settings.staticWebRootPath; + const webRoot = settings.webRoot; + + return Promise.resolve() + .then(() => { + switch (ttsProvider) { + case 'voicerss': + return voicerss; + case 'google': + return google; + default: + throw new Error('this service has not been implemented yet'); + } + }) + .then((serviceImplemntation) => { + return serviceImplemntation.downloadTTS(text, language, apiKey, staticWebRootPath); + }) + .then((filename) => { + const downloadUrl = `${webRoot}/static/tts/${filename}`; + + return clip.playClip(player, downloadUrl); + }) + .catch((err) => { + logger.error(util.inspect(err, null, false)); + throw err; + }); +} + + +module.exports = { + playText +}; diff --git a/api/helpers/search.js b/api/helpers/search.js new file mode 100644 index 0000000..ad4d4ca --- /dev/null +++ b/api/helpers/search.js @@ -0,0 +1,90 @@ +'use strict'; + +const spotify = require('./music_services/spotify'); +const library = require('./music_services/library'); +const iplayer = require('./music_services/iplayer_on_demand'); +const nowPlaying = require('./nowPlaying'); +const playpause = require('./playpause'); +const queue = require('./queue'); +const util = require('util'); +const log4js = require('log4js'); +const logger = log4js.getLogger('helpers/search.js'); + +function search(player, service, type, query, offset, limit, settings) { + let defaultedOffset = offset; + let defaultedLimit = limit; + + if (typeof offset === 'undefined') { + defaultedOffset = 0; + } + + if (typeof limit === 'undefined') { + defaultedLimit = 20; + } + + return Promise.resolve() + .then(() => { + switch (service) { + case 'spotify': + return spotify; + case 'library': + return library; + case 'iplayer': + return iplayer; + default: + throw new Error('this service has not been implemented yet'); + } + }) + .then((serviceImplemntation) => { + return serviceImplemntation.search(player, type, query, defaultedOffset, defaultedLimit, settings); + }) + .catch((err) => { + logger.error(util.inspect(err, null, false)); + throw err; + }); +} + +function playArtistRadio(player, artist, service, settings) { + const type = 'artist'; + + return Promise.resolve() + .then(() => { + return search(player, service, type, artist, 0, 1, settings); + }) + .then((results) => { + return nowPlaying.setNowPlaying(player, results.items[0].uri, results.items[0].metadata); + }) + .then(() => { + return playpause.play(player); + }); +} + +function playArtistTopTracks(player, artist, service, settings) { + const type = 'artisttoptracks'; + + return Promise.resolve() + .then(() => { + return search(player, service, type, artist, 0, 1, settings); + }) + .then((results) => { + return queue.replaceQueueAndPlay(player, results.items[0].uri, results.items[0].metadata); + }); +} + +function playSong(player, artist, service, settings) { + const type = 'song'; + + return Promise.resolve() + .then(() => { + return search(player, service, type, artist, 0, 1, settings); + }) + .then((results) => { + return queue.addToQueueAndPlay(player, results.items[0].uri, results.items[0].metadata); + }); +} +module.exports = { + search, + playArtistRadio, + playArtistTopTracks, + playSong +}; diff --git a/api/helpers/seek.js b/api/helpers/seek.js new file mode 100644 index 0000000..a5a85b6 --- /dev/null +++ b/api/helpers/seek.js @@ -0,0 +1,100 @@ +'use strict'; +const commonFunctions = require('./commonFunctions'); +const debug = require('debug')('helpers:seek'); +const Promise = require('bluebird'); + + +function timeSeek(player, seconds, timeout) { + let trackChanged; + let changeStateResult; + const promiseTimeout = timeout || 20000; + + function onTransportStateChange(status) { + debug(`status changed in onTransportStateChange ${commonFunctions.returnFullObject(status)}`); + if (trackChanged instanceof Function) { + trackChanged(); + } + } + + return Promise.resolve() + .then(() => { + player.on('transport-state', onTransportStateChange); + + return player.coordinator.timeSeek(seconds); + }) + .then((result) => { + debug('waiting for state change'); + changeStateResult = result; + + return new Promise((resolve) => { + trackChanged = resolve; + }); + }) + .timeout(promiseTimeout) + .then(() => { + return commonFunctions.checkReturnStatus(changeStateResult); + }) + .catch(Promise.TimeoutError, (error) => { + debug(`got error ${commonFunctions.returnFullObject(error)}`); + throw new Error(`timeout waiting for state change : ${error}`); + }) + .catch((error) => { + debug(`error in timeSeek() : ${commonFunctions.returnFullObject(error)}`); + throw error; + }) + .finally(() => { + player.removeListener('transport-state', onTransportStateChange); + }); +} + +function trackSeek(player, track, timeout) { + let trackChanged; + let changeStateResult; + const promiseTimeout = timeout || 20000; + + function onTransportStateChange(status) { + debug(`status changed in onTransportStateChange ${commonFunctions.returnFullObject(status)}`); + if (trackChanged instanceof Function) { + trackChanged(); + } + } + + return Promise.resolve() + .then(() => { + player.on('transport-state', onTransportStateChange); + + return player.coordinator.trackSeek(track); + }) + .tap((result) => { + debug('waiting for state change'); + changeStateResult = result; + + return new Promise((resolve) => { + trackChanged = resolve; + }); + }) + .timeout(promiseTimeout) + .then(() => { + return commonFunctions.checkReturnStatus(changeStateResult); + }) + .catch(Promise.TimeoutError, (error) => { + debug(`got error ${commonFunctions.returnFullObject(error)}`); + throw new Error(`timeout waiting for state change : ${error}`); + }) + .catch((error) => { + if (error.message === 'already at requested status') { + return true; + } + debug(`error in trackSeek() : ${commonFunctions.returnFullObject(error)}`); + throw error; + }) + .finally(() => { + player.removeListener('transport-state', onTransportStateChange); + }); +} + + +module.exports = { + timeSeek, + trackSeek +}; diff --git a/api/helpers/sleep.js b/api/helpers/sleep.js new file mode 100644 index 0000000..31406ed --- /dev/null +++ b/api/helpers/sleep.js @@ -0,0 +1,28 @@ +'use strict'; +const commonFunctions = require('./commonFunctions'); +const util = require('util'); +const log4js = require('log4js'); +const logger = log4js.getLogger('helpers/sleep.js'); + + +function sleep(player, timestamp) { + return Promise.resolve() + .then(() => { + if (/^\d+$/.test(timestamp) || timestamp.toLowerCase() === 'off') { + return player.coordinator.sleep(timestamp); + } + // Broken input + throw new Error(`bad timestamp : ${timestamp}`); + }) + .then((result) => { + return commonFunctions.checkReturnStatus(result); + }) + .catch((err) => { + logger.error(util.inspect(err, null, false)); + throw err; + }); +} + +module.exports = { + sleep +}; diff --git a/api/helpers/state.js b/api/helpers/state.js new file mode 100644 index 0000000..bd56e10 --- /dev/null +++ b/api/helpers/state.js @@ -0,0 +1,114 @@ +'use strict'; + +const Promise = require('bluebird'); +const debug = require('debug')('helpers:state'); +const commonFunctions = require('./commonFunctions'); +const _ = require('lodash'); + +function getPlayerState(player) { + return Promise.resolve() + .then(() => { + const results = _.cloneDeep(player.state); + + debug(`node-sonos-discovery state : ${commonFunctions.returnFullObject(results)}`); + + if (results.mute) { + results.mute = 'mute on'; + } else { + results.mute = 'mute off'; + } + + if (results.playMode.shuffle) { + results.playMode.shuffle = 'shuffle on'; + } else { + results.playMode.shuffle = 'shuffle off'; + } + + if (results.playMode.crossfade) { + results.playMode.crossfade = 'crossfade on'; + } else { + results.playMode.crossfade = 'crossfade off'; + } + + if (results.playbackState === 'PLAYING') { + results.playbackState = 'play'; + } else { + results.playbackState = 'pause'; + } + debug(`new state returned : ${commonFunctions.returnFullObject(results)}`); + + return results; + }) + .catch((error) => { + debug(`error in getPlayerState() : ${commonFunctions.returnFullObject(error)}`); + + throw error; + }); +} + +function simplifyPlayer(player) { + return Promise.resolve() + .then(() => { + return getPlayerState(player); + }) + .then((playerState) => { + const zone = { + uuid: player.coordinator.uuid, + zoneName: player.coordinator.roomName + }; + const groupState = { + volume: player.groupState.volume + }; + + if (player.groupState.mute) { + groupState.mute = 'mute on'; + } else { + groupState.mute = 'mute off'; + } + + return { + coordinator: zone, + groupState, + playerName: player.roomName, + state: playerState, + uuid: player.uuid + }; + }); +} + +function simplifyPlayers(players) { + return Promise.all(players.map((player) => { + return simplifyPlayer(player); + })); +} + +function simplifyZone(zone) { + return simplifyPlayer(zone.coordinator) + .then((result) => { + return simplifyPlayers(zone.members) + .then((players) => { + let returnResult = result; + + returnResult.members = players; + returnResult.state.mute = returnResult.groupState.mute; + returnResult.state.volume = returnResult.groupState.volume; + returnResult.zoneName = result.playerName; + returnResult.members = _.map(returnResult.members, (member) => { + return { + playerName: member.playerName, + state: member.state, + uuid: member.uuid + }; + }); + returnResult = _.pick(returnResult, 'zoneName', 'state', 'members', 'uuid'); + + return returnResult; + }); + }); +} + +module.exports = { + getPlayerState, + simplifyPlayer, + simplifyZone +}; diff --git a/api/helpers/tts_services/google.js b/api/helpers/tts_services/google.js new file mode 100644 index 0000000..b463f72 --- /dev/null +++ b/api/helpers/tts_services/google.js @@ -0,0 +1,68 @@ +'use strict'; +const Promise = require('bluebird'); +const log4js = require('log4js'); +const logger = log4js.getLogger('helpers/say.js'); +const rp = require('request-promise'); +const fs = Promise.promisifyAll(require('fs')); +const util = require('util'); +const crypto = require('crypto'); +const path = require('path'); + + +function downloadFile(uri, filename) { + const options = { + encoding: null, + uri + }; + + return Promise.resolve() + .then(() => { + return rp(options); + }) + .then((response) => { + return fs.writeFileAsync(filename, response); + }) + .catch((err) => { + logger.error(util.inspect(err, null, false)); + throw err; + }); +} + +function downloadTTS(phrase, language, apiKey, staticWebRootPath) { + const ttsRequestUrl = `http://translate.google.com/translate_tts?client=tw-ob&tl=${language}&q=${encodeURIComponent(phrase)}`; + + // Construct a filesystem neutral filename + const hashedFilename = crypto.createHash('sha1'). + update(phrase). + digest('hex'); + const filename = `${hashedFilename}-${language}.mp3`; + const filepath = path.resolve(staticWebRootPath, 'tts', filename); + + return Promise.resolve() + .then(() => { + // Check if file exists + try { + return fs.statSync(filepath).isFile(); + } catch (err) { + return false; + } + }) + .then((fileExists) => { + if (!fileExists) { + return downloadFile(ttsRequestUrl, filepath); + } + + return 1; + }) + .then(() => { + return filename; + }) + .catch((err) => { + logger.error(util.inspect(err, null, false)); + throw err; + }); +} + +module.exports = { + downloadTTS +}; diff --git a/api/helpers/tts_services/voicerss.js b/api/helpers/tts_services/voicerss.js new file mode 100644 index 0000000..b993337 --- /dev/null +++ b/api/helpers/tts_services/voicerss.js @@ -0,0 +1,68 @@ +'use strict'; +const Promise = require('bluebird'); +const log4js = require('log4js'); +const logger = log4js.getLogger('helpers/say.js'); +const rp = require('request-promise'); +const fs = Promise.promisifyAll(require('fs')); +const util = require('util'); +const crypto = require('crypto'); +const path = require('path'); + + +function downloadFile(uri, filename) { + const options = { + encoding: null, + uri + }; + + return Promise.resolve() + .then(() => { + return rp(options); + }) + .then((response) => { + return fs.writeFileAsync(filename, response); + }) + .catch((err) => { + logger.error(util.inspect(err, null, false)); + throw err; + }); +} + +function downloadTTS(phrase, language, apiKey, staticWebRootPath) { + // Use voicerss tts translation service to create a mp3 file + const ttsRequestUrl = `http://api.voicerss.org/?key=${apiKey}&f=22khz_16bit_mono&hl=${language}&src=${phrase}`; + // Construct a filesystem neutral filename + const hashedFilename = crypto.createHash('sha1'). + update(phrase). + digest('hex'); + const filename = `${hashedFilename}-${language}.mp3`; + const filepath = path.resolve(staticWebRootPath, 'tts', filename); + + return Promise.resolve() + .then(() => { + // Check if file exists + try { + return fs.statSync(filepath).isFile(); + } catch (err) { + return false; + } + }) + .then((fileExists) => { + if (!fileExists) { + return downloadFile(ttsRequestUrl, filepath); + } + + return 1; + }) + .then(() => { + return filename; + }) + .catch((err) => { + logger.error(util.inspect(err, null, false)); + throw err; + }); +} + +module.exports = { + downloadTTS +}; diff --git a/api/helpers/volume.js b/api/helpers/volume.js new file mode 100644 index 0000000..28d8223 --- /dev/null +++ b/api/helpers/volume.js @@ -0,0 +1,96 @@ +'use strict'; +const commonFunctions = require('./commonFunctions'); +const debug = require('debug')('helpers:volume'); +const Promise = require('bluebird'); + +function setVolume(player, requestedVolume, timeout) { + let muteChanged; + let changeStateResult; + const promiseTimeout = timeout || 20000; + + function onVolumeChange(status) { + debug(`volume status changed in onVolumeChange ${commonFunctions.returnFullObject(status)}`); + muteChanged(); + } + + return Promise.resolve() + .then(() => { + player.on('volume-change', onVolumeChange); + + return player.setVolume(requestedVolume); + }) + .then((result) => { + debug('got return status - waiting for onMuteChange'); + changeStateResult = result; + + return new Promise((resolve) => { + muteChanged = resolve; + }); + }) + .timeout(promiseTimeout) + .then(() => { + debug('checking return status'); + + return commonFunctions.checkReturnStatus(changeStateResult); + }) + .catch(Promise.TimeoutError, (error) => { + debug(`got error ${commonFunctions.returnFullObject(error)}`); + throw new Error(`timeout waiting for state change : ${error}`); + }) + .catch((error) => { + debug(`got error ${commonFunctions.returnFullObject(error)}`); + throw error; + }) + .finally(() => { + player.removeListener('volume-change', onVolumeChange); + }); +} + + +function setGroupVolume(player, requestedVolume, timeout) { + let muteChanged; + let changeStateResult; + const promiseTimeout = timeout || 20000; + + function onVolumeChange(status) { + debug(`volume status changed in onVolumeChange ${commonFunctions.returnFullObject(status)}`); + muteChanged(); + } + + return Promise.resolve() + .then(() => { + player.on('group-volume', onVolumeChange); + + return player.coordinator.setGroupVolume(requestedVolume); + }) + .then((result) => { + debug('got return status - waiting for onMuteChange'); + changeStateResult = result; + + return new Promise((resolve) => { + muteChanged = resolve; + }); + }) + .timeout(promiseTimeout) + .then(() => { + debug('checking return status'); + + return commonFunctions.checkReturnStatus(changeStateResult); + }) + .catch(Promise.TimeoutError, (error) => { + debug(`got error ${commonFunctions.returnFullObject(error)}`); + throw new Error(`timeout waiting for state change : ${error}`); + }) + .catch((error) => { + debug(`got error ${commonFunctions.returnFullObject(error)}`); + throw error; + }) + .finally(() => { + player.removeListener('group-volume', onVolumeChange); + }); +} + +module.exports = { + setGroupVolume, + setVolume +}; diff --git a/api/helpers/zones.js b/api/helpers/zones.js new file mode 100644 index 0000000..17ba6f9 --- /dev/null +++ b/api/helpers/zones.js @@ -0,0 +1,218 @@ +'use strict'; + +const commonFunctions = require('./commonFunctions'); +const state = require('./state'); +const debug = require('debug')('helpers:zones'); +const Promise = require('bluebird'); +const _ = require('lodash'); + + +function rinconUri(player) { + return `x-rincon:${player.uuid}`; +} + +function getZones(discovery) { + return Promise.resolve() + .then(() => { + if (discovery.zones.length === 0) { + throw new Error('No system has yet been discovered'); + } + + return; + }) + .then(() => { + const player = discovery.getAnyPlayer(); + + return Promise.all(_.map(player.system.zones, (zone) => { + return state.simplifyZone(zone); + })); + }) + .catch((error) => { + debug(`error in getZones() : ${commonFunctions.returnFullObject(error)}`); + + throw error; + }); +} + +function isMemberOfZone(zone, player) { + const thisZone = _.find(zone.system.zones, (searchZone) => { + return searchZone.coordinator.roomName === zone.roomName; + }); + + return _.some(thisZone.members, (room) => { + return room.roomName === player.roomName; + }); +} + +function addMemberToZone(discovery, zone, player, timeout) { + let trackChanged; + let changeStateResult; + const promiseTimeout = timeout || 30000; + + function onTransportStateChange(newState) { + _.forEach(newState, (individualState) => { + if (individualState.coordinator.roomName === zone.roomName && + trackChanged instanceof Function && + isMemberOfZone(zone, player)) { + trackChanged(); + } + }); + } + + if (isMemberOfZone(zone, player)) { + debug(`${player.roomName} is already a member of ${zone.roomName}`); + + return Promise.resolve(); + } + + discovery.on('topology-change', onTransportStateChange); + + return Promise.resolve() + .then(() => { + debug('about to add player to zone'); + + return player.setAVTransport(rinconUri(zone)); + }) + .then((result) => { + debug('waiting for state change'); + changeStateResult = result; + + return new Promise((resolve) => { + trackChanged = resolve; + }); + }) + .timeout(promiseTimeout) + .then(() => { + return commonFunctions.checkReturnStatus(changeStateResult); + }) + .catch(Promise.TimeoutError, (error) => { + debug(`got error ${commonFunctions.returnFullObject(error)}`); + throw new Error(`timeout waiting for state change : ${error}`); + }) + .catch((error) => { + debug(`error in addMemberToZone() : ${commonFunctions.returnFullObject(error)}`); + + throw error; + }) + .finally(() => { + discovery.removeListener('topology-change', onTransportStateChange); + }); +} + +function removeMemberFromZone(discovery, zone, player, timeout) { + let trackChanged; + let changeStateResult; + const promiseTimeout = timeout || 20000; + + function onTransportStateChange(newState) { + _.forEach(newState, (individualState) => { + if (individualState.coordinator.roomName === zone.roomName && + trackChanged instanceof Function && + !isMemberOfZone(zone, player)) { + trackChanged(); + } + }); + } + + if (!isMemberOfZone(zone, player)) { + debug(`${player.roomName} is not a member of ${zone.roomName}`); + + return Promise.resolve(); + } + + return Promise.resolve() + .then(() => { + discovery.on('topology-change', onTransportStateChange); + debug('about to remove player from group'); + + return player.becomeCoordinatorOfStandaloneGroup(); + }) + .then((result) => { + debug('waiting for state change'); + changeStateResult = result; + + return new Promise((resolve) => { + trackChanged = resolve; + }); + }) + .timeout(promiseTimeout) + .then(() => { + return commonFunctions.checkReturnStatus(changeStateResult); + }) + .catch(Promise.TimeoutError, (error) => { + debug(`got error ${commonFunctions.returnFullObject(error)}`); + throw new Error(`timeout waiting for state change : ${error}`); + }) + .catch((error) => { + debug(`error in removeMemberFromZone() : ${commonFunctions.returnFullObject(error)}`); + + throw error; + }) + .finally(() => { + discovery.removeListener('topology-change', onTransportStateChange); + }); +} + +function isValidZone(discovery, zoneName) { + let player; + + return Promise.resolve() + .then(() => { + if (discovery.zones.length === 0) { + throw new Error('No system has yet been discovered'); + } + + return; + }) + .then(() => { + player = discovery.getAnyPlayer(); + const zoneNames = _.map(player.system.zones, (zone) => { + return zone.coordinator.roomName.toLowerCase(); + }); + + return zoneNames.indexOf(zoneName.toLowerCase()) > -1; + }) + .catch((error) => { + debug(`error in getZones() : ${commonFunctions.returnFullObject(error)}`); + + throw error; + }); +} + +function getZone(discovery, zoneName) { + const player = discovery.getAnyPlayer(); + + return Promise.resolve() + .then(() => { + return _.find(player.system.zones, (zone) => { + return zone.coordinator.roomName.toLowerCase() === zoneName.toLowerCase(); + }); + }) + .then((zone) => { + return state.simplifyZone(zone); + }) + .catch((error) => { + debug(`error in getZones() : ${commonFunctions.returnFullObject(error)}`); + + throw error; + }); +} + +function areZonesDiscovered(discovery) { + return Promise.resolve() + .then(() => { + if (discovery.zones.length === 0) { + throw new Error('No system has yet been discovered'); + } + + return; + }); +} +module.exports = { + addMemberToZone, + areZonesDiscovered, + getZone, + getZones, + isValidZone, + removeMemberFromZone +}; diff --git a/api/swagger/swagger.yaml b/api/swagger/swagger.yaml new file mode 100644 index 0000000..9d5735c --- /dev/null +++ b/api/swagger/swagger.yaml @@ -0,0 +1,3380 @@ +swagger: '2.0' +info: + version: '0.9' + title: SONOS SWAGGER API + description: '' +host: localhost +schemes: + - http +consumes: + - application/json +produces: + - application/json +paths: + /players: + get: + operationId: getPlayers + summary: get all players + tags: + - Players + description: |- + This gets information about all players currently discovered + + Example call + ``` + curl -X GET 'http://localhost:10010/players' + ``` + responses: + '200': + description: successful result + schema: + $ref: '#/definitions/playersResult' + examples: + application/json: + - coordinator: RINCON_000E58C4373C01400 + groupState: + volume: 22 + mute: false + roomName: Bedroom + state: + currentTrack: + artist: '' + title: '' + album: '' + albumArtUri: '' + duration: 0 + uri: '' + type: track + stationName: '' + nextTrack: + artist: '' + title: '' + album: '' + albumArtUri: '' + duration: 0 + uri: '' + volume: 22 + mute: false + trackNo: 0 + elapsedTime: 0 + elapsedTimeFormatted: '00:00:00' + playbackState: STOPPED + playMode: + repeat: none + shuffle: false + crossfade: false + uuid: RINCON_000E58C4373C01400 + - coordinator: RINCON_000E585394A801400 + groupState: + volume: 22 + mute: false + roomName: Kitchen + state: + currentTrack: + artist: '' + title: '' + album: '' + albumArtUri: '' + duration: 0 + uri: '' + type: track + stationName: '' + nextTrack: + artist: '' + title: '' + album: '' + albumArtUri: '' + duration: 0 + uri: '' + volume: 22 + mute: false + trackNo: 0 + elapsedTime: 0 + elapsedTimeFormatted: '00:00:00' + playbackState: STOPPED + playMode: + repeat: none + shuffle: false + crossfade: false + uuid: RINCON_000E585394A801400 + - coordinator: RINCON_5CAAFD23191C01400 + groupState: + volume: 17 + mute: false + roomName: James bedroom + state: + currentTrack: + artist: Lil Wayne + title: >- + Sucker For Pain (with Wiz Khalifa, Imagine Dragons, Logic + & Ty Dolla $ign feat. X Ambassadors) + album: >- + Sucker For Pain (with Logic & Ty Dolla $ign feat. X + Ambassadors) + albumArtUri: >- + /getaa?s=1&u=x-sonos-spotify%3aspotify%253atrack%253a4dASQiO1Eoo3RJvt74FtXB%3fsid%3d9%26flags%3d8224%26sn%3d9 + duration: 243 + uri: >- + x-sonos-spotify:spotify%3atrack%3a4dASQiO1Eoo3RJvt74FtXB?sid=9&flags=8224&sn=9 + type: track + stationName: '' + absoluteAlbumArtUri: >- + https://i.scdn.co/image/78d07d2361fce8f76216a2b3eddcf1f0e05e82b6 + nextTrack: + artist: Fall Out Boy + title: Fourth Of July + album: American Beauty/American Psycho + albumArtUri: >- + /getaa?s=1&u=x-sonos-spotify%3aspotify%253atrack%253a3CBWtVFHhxeaHVm4VverBG%3fsid%3d9%26flags%3d8224%26sn%3d9 + duration: 224 + uri: >- + x-sonos-spotify:spotify%3atrack%3a3CBWtVFHhxeaHVm4VverBG?sid=9&flags=8224&sn=9 + absoluteAlbumArtUri: >- + https://i.scdn.co/image/a7e0ad744b844d7743f2a99f82012af0b41e2d5a + volume: 17 + mute: false + trackNo: 1 + elapsedTime: 14 + elapsedTimeFormatted: '00:00:14' + playbackState: PAUSED_PLAYBACK + playMode: + repeat: none + shuffle: true + crossfade: false + uuid: RINCON_5CAAFD23191C01400 + default: + description: error result + schema: + $ref: '#/definitions/errorResponse' + x-swagger-router-controller: players + '/players/{playerName}': + parameters: + - name: playerName + in: path + description: The player name + required: true + type: string + get: + operationId: getPlayer + summary: get individual player + tags: + - Players + description: |- + This gets the details of an individual player + + Example call + ``` + curl -X GET 'http://localhost:10010/players/bedroom' + ``` + responses: + '200': + description: successful result + schema: + $ref: '#/definitions/player' + examples: + application/json: + currentTrack: + artist: Virgin Radio UK + title: 'Now Playing: Talk Tonight - Oasis' + albumArtUri: >- + /getaa?s=1&u=x-sonosapi-stream%3as266113%3fsid%3d254%26flags%3d8224%26sn%3d0 + duration: 0 + uri: 'x-sonosapi-stream:s266113?sid=254&flags=8224&sn=0' + type: radio + stationName: Virgin Radio UK + absoluteAlbumArtUri: >- + http://192.168.1.21:1400/getaa?s=1&u=x-sonosapi-stream%3as266113%3fsid%3d254%26flags%3d8224%26sn%3d0 + nextTrack: + artist: '' + title: '' + album: '' + albumArtUri: '' + duration: 0 + uri: '' + volume: 28 + mute: false + trackNo: 1 + elapsedTime: 120 + elapsedTimeFormatted: '00:02:00' + playbackState: PLAYING + playMode: + repeat: none + shuffle: false + crossfade: false + default: + description: error result + schema: + $ref: '#/definitions/errorResponse' + x-swagger-router-controller: players + '/players/{playerName}/state': + parameters: + - name: playerName + in: path + description: The player name + required: true + type: string + get: + operationId: getPlayerState + summary: get player state + tags: + - Players + description: |- + This gets the status of an individual player + + Example call + ``` + curl -X GET 'http://localhost:10010/players/bedroom/state' + ``` + responses: + '200': + description: successful result + schema: + $ref: '#/definitions/state' + examples: + application/json: + currentTrack: + artist: Virgin Radio UK + title: 'Now Playing: Talk Tonight - Oasis' + albumArtUri: >- + /getaa?s=1&u=x-sonosapi-stream%3as266113%3fsid%3d254%26flags%3d8224%26sn%3d0 + duration: 0 + uri: 'x-sonosapi-stream:s266113?sid=254&flags=8224&sn=0' + type: radio + stationName: Virgin Radio UK + absoluteAlbumArtUri: >- + http://192.168.1.21:1400/getaa?s=1&u=x-sonosapi-stream%3as266113%3fsid%3d254%26flags%3d8224%26sn%3d0 + nextTrack: + artist: '' + title: '' + album: '' + albumArtUri: '' + duration: 0 + uri: '' + volume: 28 + mute: false + trackNo: 1 + elapsedTime: 120 + elapsedTimeFormatted: '00:02:00' + playbackState: PLAYING + playMode: + repeat: none + shuffle: false + crossfade: false + default: + description: error result + schema: + $ref: '#/definitions/errorResponse' + put: + operationId: setPlayerState + summary: set player state + tags: + - Players + description: >- + This endpoint is used to to change the status of a player. The available + states you can change are + + + * volume - can either set the volume to an absolute value by passing a + number, or relative by passing in a string prefixed by + or - + + ``` + + curl -X PUT -H "Content-Type: application/json" -d '{"volume": 10}' + "http://localhost:10010/players/bedroom/state" + + curl -X PUT -H "Content-Type: application/json" -d '{"volume": "+5"}' + "http://localhost:10010/players/bedroom/state" + + ``` + + + * mute - can be either mute on or mute off + + ``` + + curl -X PUT -H "Content-Type: application/json -d '{"mute": "mute on"}' + "http://localhost:10010/players/bedroom/state" + + ``` + + * trackNo - used to skip to a specific track + + ``` + + curl -X PUT -H "Content-Type: application/json -d '{"trackNo": 5}' + "http://localhost:10010/players/bedroom/state" + + ``` + + * elapsedTime - used to skip to a time in the current track + + ``` + + curl -X PUT -H "Content-Type: application/json -d '{"elapsedTime": 5}' + "http://localhost:10010/players/bedroom/state" + + ``` + + * playbackState - used to set playback state - can be either play, pause + or toggle + + ``` + + curl -X PUT -H "Content-Type: application/json -d '{"playbackState": + "play"}' "http://localhost:10010/players/bedroom/state" + + ``` + + * repeat - used to set repeat mode - can be either all, one or none + + ``` + + curl -X PUT -H "Content-Type: application/json" -d ' {"playMode": + {"repeat": "none"}}' "http://localhost:10010/players/bedroom/state" + + ``` + + * shuffle - used to set shuffle mode - can be either shuffle on or + shuffle off + + ``` + + curl -X PUT -H "Content-Type: application/json" -d ' {"playMode": + {"shuffle": "shuffle on"}}' + "http://localhost:10010/players/bedroom/state" + + ``` + + * crossfade - used to set crossfade mode - can be either crossfade on or + crossfade off + + ``` + + curl -X PUT -H "Content-Type: application/json" -d ' {"playMode": + {"crossfade": "crossfade on"}}' + "http://localhost:10010/players/bedroom/state" + + ``` + + * currentTrack/favourite - used to play a sonos favourite. Returns a 404 + error if favourite not found + + ``` + + curl -X PUT -H "Content-Type: application/json" -d ' {"currentTrack": + {"favourite": "BBC Radio 1"}}' + "http://localhost:10010/players/bedroom/state" + + ``` + + * currentTrack/playlist - used to play a sonos playlist. Returns a 404 + error if playlist not found + + ``` + + curl -X PUT -H "Content-Type: application/json" -d ' {"currentTrack": + {"playlist": "test playlist"}}' + "http://localhost:10010/players/bedroom/state" + + ``` + + * currentTrack/clip - plays a clip and then resumes playback (apart from + when playing from spotify connect). The clip must be in the directory + static/clips/ + + ``` + + curl -X PUT -H "Content-Type: application/json" -d ' {"currentTrack": + {"clip":"http://192.168.1.17:10010/static/clips/sample_clip.mp3"}}' + "http://localhost:10010/players/bedroom/state" + + ``` + + * currentTrack/text - says some text and then resumes playback (apart + from when playing from spotify connect) + + ``` + + curl -X PUT -H "Content-Type: application/json" -d ' {"currentTrack": + {"text":"hello world"}}' "http://localhost:10010/players/bedroom/state" + + ``` + + * currentTrack/next - skips to the next track + + ``` + + curl -X PUT -H "Content-Type: application/json" -d ' {"currentTrack": + {"skip":"next"}}' "http://localhost:10010/players/bedroom/state" + + ``` + + * currentTrack/previous - skips to the previous track + + ``` + + curl -X PUT -H "Content-Type: application/json" -d ' {"currentTrack": + {"skip":"previous"}}' "http://localhost:10010/players/bedroom/state" + + ``` + + * currentTrack/linein - sets input to be linein of a specified player + + ``` + + curl -X PUT -H "Content-Type: application/json" -d ' {"currentTrack": + {"lineinSource":"kitchen"}}' + "http://localhost:10010/players/bedroom/state" + + ``` + + * currentTrack/artistTopTracks - plays the top tracks of the first + artist returned by a spotify search + + ``` + + curl -X PUT -H "Content-Type: application/json" -d ' {"currentTrack": + {"artistTopTracks":"blink 182"}}' + "http://localhost:10010/players/bedroom/state" + + ``` + + * currentTrack/artistRadio - plays the artist radio of the first artist + returned by a spotify search + + ``` + + curl -X PUT -H "Content-Type: application/json" -d ' {"currentTrack": + {"artistRadio":"blink 182"}}' + "http://localhost:10010/players/bedroom/state" + + ``` + + * currentTrack/song - plays the first song returned by a spotify search + + ``` + + curl -X PUT -H "Content-Type: application/json" -d ' {"currentTrack": + {"song":"all the small things"}}' + "http://localhost:10010/players/bedroom/state" + + ``` + parameters: + - name: async + in: query + type: boolean + - name: body + in: body + schema: + $ref: '#/definitions/stateInput' + responses: + '201': + description: successful result + schema: + $ref: '#/definitions/state' + '202': + description: default asynchronous result + schema: + $ref: '#/definitions/ok' + default: + description: error result + schema: + $ref: '#/definitions/errorResponse' + x-swagger-router-controller: playerState + '/players/{playerName}/nowplaying': + parameters: + - name: playerName + in: path + description: The zone name + required: true + type: string + get: + operationId: getPlayerNowPlaying + summary: get player now playing + tags: + - Players + description: |- + This gets details of the currently playing track + + Example call + ``` + curl -X GET "http://localhost:10010/zones/bedroom/nowplaying" + ``` + responses: + '200': + description: successful result + schema: + $ref: '#/definitions/nowPlayingResult' + default: + description: error result + schema: + $ref: '#/definitions/errorResponse' + post: + operationId: setPlayerNowPlaying + summary: set player now playing + tags: + - Players + description: >- + This is used to set the currently playing track or radio. + + The input should be passed in the body of the request and be an item + returned from a search result + + + Example call + + ``` + + curl -X POST -H "Content-Type: application/json"-d '{"title": "The + Animal In Me","artist": "The Animal In Me","album": "The Animal In + Me","imageUrl": + "https://i.scdn.co/image/37eff75cf19923b7dc796ba374515dbe45098c14","type": + "artist","uri": + "x-sonosapi-radio:spotify%3aartistRadio%3a6hyAYqBdxyramm4W9TB7R0?sid=9&flags=8300&sn=5","metadata": + "\n The Animal In Me + radioobject.item.audioItem.audioBroadcast.#artistRadio\n + SA_RINCON2311_X_#Svc2311-0-Token"}' + "http://localhost:10010/zones/bedroom/nowplaying" + + ``` + parameters: + - name: async + in: query + type: boolean + - name: body + in: body + schema: + type: object + properties: + uri: + type: string + metadata: + type: string + responses: + '201': + description: successful result + schema: + $ref: '#/definitions/nowPlayingResult' + '202': + description: default asynchronous result + schema: + $ref: '#/definitions/ok' + default: + description: error result + schema: + $ref: '#/definitions/errorResponse' + x-swagger-router-controller: playerNowPlaying + '/players/{playerName}/queue': + parameters: + - name: playerName + in: path + description: The zone name + required: true + type: string + get: + operationId: getPlayerQueue + summary: get player queue + tags: + - Players + description: |- + This gets the details of the current queue + + Example call + ``` + curl -X GET "http://localhost:10010/zones/bedroom/queue?detailed=true" + ``` + parameters: + - name: detailed + in: query + description: >- + Flag to indicate if detailed information should be returned. Default + is false + required: false + type: boolean + responses: + '200': + description: successful result + schema: + type: array + description: successful result + items: + type: object + properties: + uri: + type: string + albumArtURI: + type: string + title: + type: string + artist: + type: string + album: + type: string + examples: + application/json: + - 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 + album: In Blue + default: + description: error result + schema: + $ref: '#/definitions/errorResponse' + patch: + operationId: addToPlayerQueue + summary: add to player queue + tags: + - Players + description: >- + This is used to add an individual item to the current queue. + + The input should be passed in the body of the request and be an item + returned from a search result + + + Example call + + ``` + + curl -X PATCH -H "Content-Type: application/json" -d '{"uri": + "x-sonos-spotify:spotify%3atrack%3a1D3ODoXHBLpdxolZRHWV1j?sid=9&flags=8224&sn=5","metadata": + "object.item.audioItem.musicTrackSA_RINCON2311_X_#Svc2311-0-Token"}' + "http://localhost:10010/zones/bedroom/queue" + + ``` + parameters: + - name: async + in: query + type: boolean + - name: body + in: body + schema: + $ref: '#/definitions/queueRequest' + responses: + '200': + description: successful result + schema: + type: object + description: successful result + '202': + description: default asynchronous result + schema: + $ref: '#/definitions/ok' + default: + description: error result + schema: + $ref: '#/definitions/errorResponse' + delete: + operationId: clearPlayerQueue + summary: clear player queue + tags: + - Players + description: >- + This is used to clear the current queue + + + Example call + + ``` + + curl -X DELETE -H "Content-Type: application/json" -d '' + "http://localhost:10010/zones/bedroom/queue" + + ``` + parameters: + - name: async + in: query + type: boolean + responses: + '200': + description: successful result + schema: + $ref: '#/definitions/ok' + '202': + description: default asynchronous result + schema: + $ref: '#/definitions/ok' + default: + description: error result + schema: + $ref: '#/definitions/errorResponse' + post: + operationId: replacePlayerQueue + summary: replace playerqueue + tags: + - Players + description: >- + This replaces the current queue with specified tracks. + + The input should be passed in the body of the request and be an item + returned from a search result + + + Example call + + ``` + + curl -X POST -H "Content-Type: application/json" -d ' {"uri": + "x-sonos-spotify:spotify%3atrack%3a1D3ODoXHBLpdxolZRHWV1j?sid=9&flags=8224&sn=5", + "metadata": "object.item.audioItem.musicTrackSA_RINCON2311_X_#Svc2311-0-Token"}' + "http://localhost:10010/zones/bedroom/queue" + + ``` + parameters: + - name: async + in: query + type: boolean + - name: body + in: body + schema: + $ref: '#/definitions/queueRequest' + responses: + '201': + description: successful result + schema: + type: object + description: successful result + '202': + description: default asynchronous result + schema: + $ref: '#/definitions/ok' + default: + description: error result + schema: + $ref: '#/definitions/errorResponse' + x-swagger-router-controller: playerQueue + /zones: + get: + operationId: getZones + summary: get all zones + tags: + - Zones + description: |- + This gets information about all zones currently discovered + + Example call + ``` + curl -X GET 'http://localhost:10010/zones' + ``` + responses: + '200': + description: successful result + schema: + $ref: '#/definitions/zonesResult' + examples: + application/json: + - uuid: RINCON_000E58C4373C01400 + coordinator: + uuid: RINCON_000E58C4373C01400 + state: + currentTrack: + artist: Virgin Radio UK + title: >- + You are listening to Edith Bowman at Breakfast on Virgin + Radio UK + albumArtUri: >- + /getaa?s=1&u=x-sonosapi-stream%3as266113%3fsid%3d254%26flags%3d8224%26sn%3d0 + duration: 0 + uri: 'x-sonosapi-stream:s266113?sid=254&flags=8224&sn=0' + type: radio + stationName: Virgin Radio UK + absoluteAlbumArtUri: >- + http://192.168.1.21:1400/getaa?s=1&u=x-sonosapi-stream%3as266113%3fsid%3d254%26flags%3d8224%26sn%3d0 + nextTrack: + artist: '' + title: '' + album: '' + albumArtUri: '' + duration: 0 + uri: '' + volume: 28 + mute: false + trackNo: 1 + elapsedTime: 627 + elapsedTimeFormatted: '00:10:27' + playbackState: PLAYING + playMode: + repeat: none + shuffle: false + crossfade: false + roomName: Bedroom + coordinator: RINCON_000E58C4373C01400 + groupState: + volume: 28 + mute: false + members: + - uuid: RINCON_000E58C4373C01400 + state: + currentTrack: + artist: Virgin Radio UK + title: >- + You are listening to Edith Bowman at Breakfast on + Virgin Radio UK + albumArtUri: >- + /getaa?s=1&u=x-sonosapi-stream%3as266113%3fsid%3d254%26flags%3d8224%26sn%3d0 + duration: 0 + uri: 'x-sonosapi-stream:s266113?sid=254&flags=8224&sn=0' + type: radio + stationName: Virgin Radio UK + absoluteAlbumArtUri: >- + http://192.168.1.21:1400/getaa?s=1&u=x-sonosapi-stream%3as266113%3fsid%3d254%26flags%3d8224%26sn%3d0 + nextTrack: + artist: '' + title: '' + album: '' + albumArtUri: '' + duration: 0 + uri: '' + volume: 28 + mute: false + trackNo: 1 + elapsedTime: 627 + elapsedTimeFormatted: '00:10:27' + playbackState: PLAYING + playMode: + repeat: none + shuffle: false + crossfade: false + roomName: Bedroom + coordinator: RINCON_000E58C4373C01400 + groupState: + volume: 28 + mute: false + - uuid: RINCON_5CAAFD23191C01400 + coordinator: + uuid: RINCON_5CAAFD23191C01400 + state: + currentTrack: + artist: Panic! At The Disco + title: Victorious + album: Death Of A Bachelor + albumArtUri: >- + /getaa?s=1&u=x-sonos-spotify%3aspotify%253atrack%253a6od5hFv9IT5JHc7NEF9HRv%3fsid%3d9%26flags%3d8224%26sn%3d9 + duration: 178 + uri: >- + x-sonos-spotify:spotify%3atrack%3a6od5hFv9IT5JHc7NEF9HRv?sid=9&flags=8224&sn=9 + type: track + stationName: '' + absoluteAlbumArtUri: >- + https://i.scdn.co/image/a1e4ed2942e4a9b4fce5d654cb28fc30ceb968ef + nextTrack: + artist: Panic! At The Disco + title: Don't Threaten Me With A Good Time + album: Death Of A Bachelor + albumArtUri: >- + /getaa?s=1&u=x-sonos-spotify%3aspotify%253atrack%253a2fh3bZ8jZhMxOcfESE9nQY%3fsid%3d9%26flags%3d8224%26sn%3d9 + duration: 213 + uri: >- + x-sonos-spotify:spotify%3atrack%3a2fh3bZ8jZhMxOcfESE9nQY?sid=9&flags=8224&sn=9 + absoluteAlbumArtUri: >- + https://i.scdn.co/image/a1e4ed2942e4a9b4fce5d654cb28fc30ceb968ef + volume: 17 + mute: false + trackNo: 1 + elapsedTime: 0 + elapsedTimeFormatted: '00:00:00' + playbackState: STOPPED + playMode: + repeat: none + shuffle: false + crossfade: false + roomName: James bedroom + coordinator: RINCON_5CAAFD23191C01400 + groupState: + volume: 17 + mute: false + members: + - uuid: RINCON_5CAAFD23191C01400 + state: + currentTrack: + artist: Panic! At The Disco + title: Victorious + album: Death Of A Bachelor + albumArtUri: >- + /getaa?s=1&u=x-sonos-spotify%3aspotify%253atrack%253a6od5hFv9IT5JHc7NEF9HRv%3fsid%3d9%26flags%3d8224%26sn%3d9 + duration: 178 + uri: >- + x-sonos-spotify:spotify%3atrack%3a6od5hFv9IT5JHc7NEF9HRv?sid=9&flags=8224&sn=9 + type: track + stationName: '' + absoluteAlbumArtUri: >- + https://i.scdn.co/image/a1e4ed2942e4a9b4fce5d654cb28fc30ceb968ef + nextTrack: + artist: Panic! At The Disco + title: Don't Threaten Me With A Good Time + album: Death Of A Bachelor + albumArtUri: >- + /getaa?s=1&u=x-sonos-spotify%3aspotify%253atrack%253a2fh3bZ8jZhMxOcfESE9nQY%3fsid%3d9%26flags%3d8224%26sn%3d9 + duration: 213 + uri: >- + x-sonos-spotify:spotify%3atrack%3a2fh3bZ8jZhMxOcfESE9nQY?sid=9&flags=8224&sn=9 + absoluteAlbumArtUri: >- + https://i.scdn.co/image/a1e4ed2942e4a9b4fce5d654cb28fc30ceb968ef + volume: 17 + mute: false + trackNo: 1 + elapsedTime: 0 + elapsedTimeFormatted: '00:00:00' + playbackState: STOPPED + playMode: + repeat: none + shuffle: false + crossfade: false + roomName: James bedroom + coordinator: RINCON_5CAAFD23191C01400 + groupState: + volume: 17 + mute: false + - uuid: RINCON_000E585394A801400 + coordinator: + uuid: RINCON_000E585394A801400 + state: + currentTrack: + artist: Virgin Radio UK + title: >- + You are listening to Edith Bowman at Breakfast on Virgin + Radio UK + albumArtUri: >- + /getaa?s=1&u=x-sonosapi-stream%3as266113%3fsid%3d254%26flags%3d8224%26sn%3d0 + duration: 0 + uri: 'x-sonosapi-stream:s266113?sid=254&flags=8224&sn=0' + type: radio + stationName: Virgin Radio UK + absoluteAlbumArtUri: >- + http://192.168.1.19:1400/getaa?s=1&u=x-sonosapi-stream%3as266113%3fsid%3d254%26flags%3d8224%26sn%3d0 + nextTrack: + artist: '' + title: '' + album: '' + albumArtUri: '' + duration: 0 + uri: '' + volume: 21 + mute: false + trackNo: 1 + elapsedTime: 626 + elapsedTimeFormatted: '00:10:26' + playbackState: PLAYING + playMode: + repeat: none + shuffle: false + crossfade: false + roomName: Kitchen + coordinator: RINCON_000E585394A801400 + groupState: + volume: 21 + mute: false + members: + - uuid: RINCON_000E585394A801400 + state: + currentTrack: + artist: Virgin Radio UK + title: >- + You are listening to Edith Bowman at Breakfast on + Virgin Radio UK + albumArtUri: >- + /getaa?s=1&u=x-sonosapi-stream%3as266113%3fsid%3d254%26flags%3d8224%26sn%3d0 + duration: 0 + uri: 'x-sonosapi-stream:s266113?sid=254&flags=8224&sn=0' + type: radio + stationName: Virgin Radio UK + absoluteAlbumArtUri: >- + http://192.168.1.19:1400/getaa?s=1&u=x-sonosapi-stream%3as266113%3fsid%3d254%26flags%3d8224%26sn%3d0 + nextTrack: + artist: '' + title: '' + album: '' + albumArtUri: '' + duration: 0 + uri: '' + volume: 21 + mute: false + trackNo: 1 + elapsedTime: 626 + elapsedTimeFormatted: '00:10:26' + playbackState: PLAYING + playMode: + repeat: none + shuffle: false + crossfade: false + roomName: Kitchen + coordinator: RINCON_000E585394A801400 + groupState: + volume: 21 + mute: false + default: + description: error result + schema: + $ref: '#/definitions/errorResponse' + x-swagger-router-controller: zones + '/zones/{zoneName}': + parameters: + - name: zoneName + in: path + description: The zone name + required: true + type: string + get: + operationId: getZone + summary: get individual zone + tags: + - Zones + description: |- + This gets the details of an individual zone + + Example call + ``` + curl -X GET 'http://localhost:10010/zones/bedroom' + ``` + responses: + '200': + description: successful result + schema: + $ref: '#/definitions/zone' + examples: + application/json: + currentTrack: + artist: Virgin Radio UK + title: 'Now Playing: Talk Tonight - Oasis' + albumArtUri: >- + /getaa?s=1&u=x-sonosapi-stream%3as266113%3fsid%3d254%26flags%3d8224%26sn%3d0 + duration: 0 + uri: 'x-sonosapi-stream:s266113?sid=254&flags=8224&sn=0' + type: radio + stationName: Virgin Radio UK + absoluteAlbumArtUri: >- + http://192.168.1.21:1400/getaa?s=1&u=x-sonosapi-stream%3as266113%3fsid%3d254%26flags%3d8224%26sn%3d0 + nextTrack: + artist: '' + title: '' + album: '' + albumArtUri: '' + duration: 0 + uri: '' + volume: 28 + mute: false + trackNo: 1 + elapsedTime: 120 + elapsedTimeFormatted: '00:02:00' + playbackState: PLAYING + playMode: + repeat: none + shuffle: false + crossfade: false + default: + description: error result + schema: + $ref: '#/definitions/errorResponse' + x-swagger-router-controller: zones + '/zones/{zoneName}/state': + parameters: + - name: zoneName + in: path + description: The zone name + required: true + type: string + get: + operationId: getZoneState + summary: get zone state + tags: + - Zones + description: |- + This gets the status of an individual zone + + Example call + ``` + curl -X GET 'http://localhost:10010/zones/bedroom/state' + ``` + responses: + '200': + description: successful result + schema: + $ref: '#/definitions/state' + examples: + application/json: + currentTrack: + artist: Virgin Radio UK + title: 'Now Playing: Talk Tonight - Oasis' + albumArtUri: >- + /getaa?s=1&u=x-sonosapi-stream%3as266113%3fsid%3d254%26flags%3d8224%26sn%3d0 + duration: 0 + uri: 'x-sonosapi-stream:s266113?sid=254&flags=8224&sn=0' + type: radio + stationName: Virgin Radio UK + absoluteAlbumArtUri: >- + http://192.168.1.21:1400/getaa?s=1&u=x-sonosapi-stream%3as266113%3fsid%3d254%26flags%3d8224%26sn%3d0 + nextTrack: + artist: '' + title: '' + album: '' + albumArtUri: '' + duration: 0 + uri: '' + volume: 28 + mute: false + trackNo: 1 + elapsedTime: 120 + elapsedTimeFormatted: '00:02:00' + playbackState: PLAYING + playMode: + repeat: none + shuffle: false + crossfade: false + default: + description: error result + schema: + $ref: '#/definitions/errorResponse' + put: + operationId: setZoneState + summary: set zone state + tags: + - Zones + description: >- + This endpoint is used to to change the status of a zone. The available + states you can change are + + + * volume - can either set the volume to an absolute value by passing a + number, or relative by passing in a string prefixed by + or - + + ``` + + curl -X PUT -H "Content-Type: application/json" -d '{"volume": 10}' + "http://localhost:10010/zones/bedroom/state" + + curl -X PUT -H "Content-Type: application/json" -d '{"volume": "+5"}' + "http://localhost:10010/zones/bedroom/state" + + ``` + + + * mute - can be either mute on or mute off + + ``` + + curl -X PUT -H "Content-Type: application/json -d '{"mute": "mute on"}' + "http://localhost:10010/zones/bedroom/state" + + ``` + + * trackNo - used to skip to a specific track + + ``` + + curl -X PUT -H "Content-Type: application/json -d '{"trackNo": 5}' + "http://localhost:10010/zones/bedroom/state" + + ``` + + * elapsedTime - used to skip to a time in the current track + + ``` + + curl -X PUT -H "Content-Type: application/json -d '{"elapsedTime": 5}' + "http://localhost:10010/zones/bedroom/state" + + ``` + + * playbackState - used to set playback state - can be either play, pause + or toggle + + ``` + + curl -X PUT -H "Content-Type: application/json -d '{"playbackState": + "play"}' "http://localhost:10010/zones/bedroom/state" + + ``` + + * repeat - used to set repeat mode - can be either all, one or none + + ``` + + curl -X PUT -H "Content-Type: application/json" -d ' {"playMode": + {"repeat": "none"}}' "http://localhost:10010/zones/bedroom/state" + + ``` + + * shuffle - used to set shuffle mode - can be either shuffle on or + shuffle off + + ``` + + curl -X PUT -H "Content-Type: application/json" -d ' {"playMode": + {"shuffle": "shuffle on"}}' "http://localhost:10010/zones/bedroom/state" + + ``` + + * crossfade - used to set crossfade mode - can be either crossfade on or + crossfade off + + ``` + + curl -X PUT -H "Content-Type: application/json" -d ' {"playMode": + {"crossfade": "crossfade on"}}' + "http://localhost:10010/zones/bedroom/state" + + ``` + + * currentTrack/favourite - used to play a sonos favourite. Returns a 404 + error if favourite not found + + ``` + + curl -X PUT -H "Content-Type: application/json" -d ' {"currentTrack": + {"favourite": "BBC Radio 1"}}' + "http://localhost:10010/zones/bedroom/state" + + ``` + + * currentTrack/playlist - used to play a sonos playlist. Returns a 404 + error if playlist not found + + ``` + + curl -X PUT -H "Content-Type: application/json" -d ' {"currentTrack": + {"playlist": "test playlist"}}' + "http://localhost:10010/zones/bedroom/state" + + ``` + + * currentTrack/clip - plays a clip and then resumes playback (apart from + when playing from spotify connect). The clip must exist in static/clips + directory + + ``` + + curl -X PUT -H "Content-Type: application/json" -d ' {"currentTrack": + {"clip":"http://192.168.1.17:10010/static/clips/sample_clip.mp3"}}' + "http://localhost:10010/zones/bedroom/state" + + ``` + + * currentTrack/text - says some text and then resumes playback (apart + from when playing from spotify connect) + + ``` + + curl -X PUT -H "Content-Type: application/json" -d ' {"currentTrack": + {"text":"hello world"}}' "http://localhost:10010/zones/bedroom/state" + + ``` + + * currentTrack/next - skips to the next track + + ``` + + curl -X PUT -H "Content-Type: application/json" -d ' {"currentTrack": + {"skip":"next"}}' "http://localhost:10010/players/zones/state" + + ``` + + * currentTrack/previous - skips to the previous track + + ``` + + curl -X PUT -H "Content-Type: application/json" -d ' {"currentTrack": + {"skip":"previous"}}' "http://localhost:10010/zones/bedroom/state" + + ``` + + * currentTrack/linein - sets input to be linein of a specified player + + ``` + + curl -X PUT -H "Content-Type: application/json" -d ' {"currentTrack": + {"lineinSource":"kitchen"}}' + "http://localhost:10010/zones/bedroom/state" + + ``` + + * currentTrack/artistTopTracks - plays the top tracks of the first + artist returned by a spotify search + + ``` + + curl -X PUT -H "Content-Type: application/json" -d ' {"currentTrack": + {"artistTopTracks":"blink 182"}}' + "http://localhost:10010/zones/bedroom/state" + + ``` + + * currentTrack/artistRadio - plays the artist radio of the first artist + returned by a spotify search + + ``` + + curl -X PUT -H "Content-Type: application/json" -d ' {"currentTrack": + {"artistRadio":"blink 182"}}' + "http://localhost:10010/zones/bedroom/state" + + ``` + + * currentTrack/song - plays the first song returned by a spotify search + + ``` + + curl -X PUT -H "Content-Type: application/json" -d ' {"currentTrack": + {"song":"all the small things"}}' + "http://localhost:10010/zones/bedroom/state" + + ``` + parameters: + - name: async + in: query + type: boolean + - name: body + in: body + schema: + $ref: '#/definitions/stateInput' + responses: + '201': + description: successful result + schema: + $ref: '#/definitions/state' + '202': + description: default asynchronous result + schema: + $ref: '#/definitions/ok' + default: + description: error result + schema: + $ref: '#/definitions/errorResponse' + x-swagger-router-controller: zoneState + '/zones/{zoneName}/nowplaying': + parameters: + - name: zoneName + in: path + description: The zone name + required: true + type: string + get: + operationId: getZoneNowPlaying + summary: get zone now playing + tags: + - Zones + description: |- + This gets details of the currently playing track + + Example call + ``` + curl -X GET "http://localhost:10010/zones/bedroom/nowplaying" + ``` + responses: + '200': + description: successful result + schema: + $ref: '#/definitions/nowPlayingResult' + default: + description: error result + schema: + $ref: '#/definitions/errorResponse' + post: + operationId: setZoneNowPlaying + summary: set zone now playing + tags: + - Zones + description: >- + This is used to set the currently playing track or radio. + + The input should be passed in the body of the request and be an item + returned from a search result + + + Example call + + ``` + + curl -X POST -H "Content-Type: application/json"-d '{"title": "The + Animal In Me","artist": "The Animal In Me","album": "The Animal In + Me","imageUrl": + "https://i.scdn.co/image/37eff75cf19923b7dc796ba374515dbe45098c14","type": + "artist","uri": + "x-sonosapi-radio:spotify%3aartistRadio%3a6hyAYqBdxyramm4W9TB7R0?sid=9&flags=8300&sn=5","metadata": + "\n The Animal In Me + radioobject.item.audioItem.audioBroadcast.#artistRadio\n + SA_RINCON2311_X_#Svc2311-0-Token"}' + "http://localhost:10010/zones/bedroom/nowplaying" + + ``` + parameters: + - name: async + in: query + type: boolean + - name: body + in: body + schema: + type: object + properties: + uri: + type: string + metadata: + type: string + responses: + '201': + description: successful result + schema: + type: object + description: successful result + '202': + description: default asynchronous result + schema: + $ref: '#/definitions/ok' + default: + description: error result + schema: + $ref: '#/definitions/errorResponse' + x-swagger-router-controller: zoneNowPlaying + '/zones/{zoneName}/queue': + parameters: + - name: zoneName + in: path + description: The zone name + required: true + type: string + get: + operationId: getZoneQueue + summary: get zone queue + tags: + - Zones + description: |- + This gets the details of the current queue + + Example call + ``` + curl -X GET "http://localhost:10010/zones/bedroom/queue?detailed=true" + ``` + parameters: + - name: detailed + in: query + description: >- + Flag to indicate if detailed information should be returned. Default + is false + required: false + type: boolean + responses: + '200': + description: successful result + schema: + type: array + description: successful result + items: + type: object + properties: + uri: + type: string + albumArtURI: + type: string + title: + type: string + artist: + type: string + album: + type: string + examples: + application/json: + - 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 + album: In Blue + default: + description: error result + schema: + $ref: '#/definitions/errorResponse' + patch: + operationId: addToZoneQueue + summary: add to zone queue + tags: + - Zones + description: >- + This is used to add an individual item to the current queue. + + The input should be passed in the body of the request and be an item + returned from a search result + + + Example call + + ``` + + curl -X PATCH -H "Content-Type: application/json" -d '{"uri": + "x-sonos-spotify:spotify%3atrack%3a1D3ODoXHBLpdxolZRHWV1j?sid=9&flags=8224&sn=5","metadata": + "object.item.audioItem.musicTrackSA_RINCON2311_X_#Svc2311-0-Token"}' + "http://localhost:10010/zones/bedroom/queue" + + ``` + parameters: + - name: async + in: query + type: boolean + - name: body + in: body + schema: + $ref: '#/definitions/queueRequest' + responses: + '200': + description: successful result + schema: + type: object + description: successful result + '202': + description: default asynchronous result + schema: + $ref: '#/definitions/ok' + default: + description: error result + schema: + $ref: '#/definitions/errorResponse' + delete: + operationId: clearZoneQueue + summary: clear zone queue + tags: + - Zones + description: >- + This is used to clear the current queue + + + Example call + + ``` + + curl -X DELETE -H "Content-Type: application/json" -d '' + "http://localhost:10010/zones/bedroom/queue" + + ``` + parameters: + - name: async + in: query + type: boolean + responses: + '200': + description: successful result + schema: + type: object + description: successful result + '202': + description: default asynchronous result + schema: + $ref: '#/definitions/ok' + default: + description: error result + schema: + $ref: '#/definitions/errorResponse' + post: + operationId: replaceZoneQueue + summary: replace zone queue + tags: + - Zones + description: >- + This replaces the current queue with specified tracks. + + The input should be passed in the body of the request and be an item + returned from a search result + + + Example call + + ``` + + curl -X POST -H "Content-Type: application/json" -d ' {"uri": + "x-sonos-spotify:spotify%3atrack%3a1D3ODoXHBLpdxolZRHWV1j?sid=9&flags=8224&sn=5", + "metadata": "object.item.audioItem.musicTrackSA_RINCON2311_X_#Svc2311-0-Token"}' + "http://localhost:10010/zones/bedroom/queue" + + ``` + parameters: + - name: async + in: query + type: boolean + - name: body + in: body + schema: + $ref: '#/definitions/queueRequest' + responses: + '201': + description: successful result + schema: + type: object + description: successful result + '202': + description: default asynchronous result + schema: + $ref: '#/definitions/ok' + default: + description: error result + schema: + $ref: '#/definitions/errorResponse' + x-swagger-router-controller: zoneQueue + '/zones/{zoneName}/members': + parameters: + - name: zoneName + in: path + required: true + type: string + get: + operationId: getZoneMembers + summary: get zone members + tags: + - Zones + description: |- + This gets all members of the zone + Example call + ``` + curl -X GET -H "Content-Type: application/json" "http://localhost:10010/zones/bedroom/members" + ``` + responses: + '200': + description: successful result + schema: + $ref: '#/definitions/membersResult' + default: + description: error result + schema: + $ref: '#/definitions/errorResponse' + post: + operationId: addZoneMember + summary: add zone member + tags: + - Zones + description: |- + This adds a player to a zone + Example call + ``` + curl -X POST -H "Content-Type: application/json" -d '{ + "player": "kitchen" + }' "http://localhost:10010/zones/bedroom/members" + ``` + parameters: + - name: async + in: query + type: boolean + - name: body + in: body + schema: + type: object + properties: + player: + type: string + required: + - player + example: + player: james bedroom + responses: + '201': + description: successful result + schema: + $ref: '#/definitions/membersResult' + '202': + description: default asynchronous result + schema: + $ref: '#/definitions/ok' + default: + description: error result + schema: + $ref: '#/definitions/errorResponse' + x-swagger-router-controller: zoneMembers + '/zones/{zoneName}/members/{roomName}': + parameters: + - name: zoneName + in: path + required: true + type: string + - name: roomName + in: path + required: true + type: string + delete: + operationId: removeZoneMember + summary: remove zone member + tags: + - Zones + description: >- + This removes a member from a zone + + Example call + + ``` + + curl -X DELETE -d '' + "http://localhost:10010/zones/bedroom/members/kitchen" + + ``` + parameters: + - name: async + in: query + type: boolean + responses: + '200': + description: successful result + schema: + $ref: '#/definitions/membersResult' + '202': + description: default asynchronous result + schema: + $ref: '#/definitions/ok' + default: + description: error result + schema: + $ref: '#/definitions/errorResponse' + x-swagger-router-controller: zoneMembers + /search: + get: + operationId: search + summary: get search results from a music service + tags: + - Search + description: >- + This is used to search different music services. + + + The service paramater controls which service to search. Currently + implemented services are + + * library + + * spotify + + * iplayer + + + The type paramater controls what type to search for. This may vary + accross services, but currently implemented for library and spotify + services are + + * song + + * album + + * artist + + + For spotify, the type can also be specifed as artisttoptracks and it + returns results which play the top tracks by the artist. + + + For iplayer, this returns on demand programmes and the search types + implemented are + + * title + + * synopsis + + + For iplayer, the available list of programmes is refreshed every 24 + hours, so the first time it is called, or if the data has not been + refreshed for more than 24 hours, it may take a long time to run. You + can force a refresh by calling + + + The limit paramater allows you to limit the number of results returned. + This defaults to 20 if not specified. + + + The offset paramater allows you to page through results. This defaults + to 0 if not specified. + + The returned results include an array of items. Each item in the array + can be used as input to add to queue or set now playing endpoints. If + the results returned are a radio stream then it can only be added to now + playing. + + Note - spotify retruns a radio stream for artist so can only be added to + now playing + + + Also included are links to next and previous results when more than the + limit are returned to allow easy paging through the results, and details + of the number of results returned and available. + + + Example call + + ``` + + curl -X GET + 'http://localhost:10010/search?service=spotify&type=song&q=blue' + + ``` + parameters: + - name: service + in: query + description: The service to search + required: true + type: string + - name: type + in: query + description: 'The type of search to perform - can be song, album, artist' + required: true + type: string + - name: q + in: query + description: The term to search for + required: true + type: string + - name: offset + in: query + description: >- + Used when multiple pages of results are returned to show results + starting at offset + required: false + type: integer + - name: limit + in: query + description: How many search items to return + required: false + type: integer + responses: + '200': + description: successful result + schema: + $ref: '#/definitions/searchResult' + default: + description: error result + schema: + $ref: '#/definitions/errorResponse' + x-swagger-router-controller: search + /favourites: + get: + operationId: getFavourites + summary: get favourites from sonos + tags: + - Favourites + description: |- + This returns a list of favourites from sonos + + Example call + ``` + curl -X GET 'http://localhost:10010/favourites' + ``` + parameters: + - name: detailed + in: query + description: >- + Used to specify if return just the names of the favourites or full + details. Defaults to false + required: false + type: boolean + responses: + '200': + description: successful result + schema: + $ref: '#/definitions/favouritesResult' + default: + description: error result + schema: + $ref: '#/definitions/errorResponse' + x-swagger-router-controller: favourites + '/favourites/{favourite}': + parameters: + - name: favourite + in: path + required: true + type: string + get: + operationId: getFavourite + summary: get individual favourite + tags: + - Favourites + description: |- + This gets details of an individual favourite. + Example call + ``` + curl -X GET "http://localhost:10010/favourites/6%20Music/" + ``` + responses: + '200': + description: successful result + schema: + $ref: '#/definitions/favourite' + default: + description: error result + x-swagger-router-controller: favourites + /swagger: + get: + operationId: GET_swagger + tags: + - Swagger + description: Gets the swagger definiton + responses: + default: + description: '' + x-swagger-pipe: swagger_raw +definitions: + player: + title: player + type: object + properties: + coordinator: + type: object + properties: + uuid: + type: string + zoneName: + type: string + groupState: + type: object + properties: + volume: + type: integer + mute: + type: string + '': + type: string + playerName: + type: string + state: + $ref: '#/definitions/state' + uuid: + type: string + example: + coordinator: + uuid: RINCON_000E58C4373C01400 + zoneName: Bedroom + groupState: + volume: 0 + mute: mute on + playerName: James bedroom + state: + currentTrack: + artist: The Rolling Stones + title: Get Off My Cloud + album: 'Forty Licks [Disc 1]' + duration: 176 + uri: >- + x-file-cifs://ripley/music/V/Various%20Artists/stomp/14%20Get%20Off%20My%20Cloud.mp3 + type: track + stationName: '' + nextTrack: + artist: Butthole Surfers + title: Sweat Loaf + album: Locust Abortion Technician + duration: 358 + uri: >- + x-file-cifs://ripley/music/V/Various%20Artists/stomp/01%20-%20Sweat%20Loaf.mp3 + volume: 17 + mute: mute off + trackNo: 1 + elapsedTime: 0 + elapsedTimeFormatted: '00:00:00' + playbackState: pause + playMode: + repeat: none + shuffle: shuffle on + crossfade: crossfade on + uuid: RINCON_5CAAFD23191C01400 + state: + title: state + type: object + properties: + currentTrack: + type: object + properties: + artist: + type: string + title: + type: string + album: + type: string + duration: + type: integer + uri: + type: string + type: + type: string + stationName: + type: string + nextTrack: + type: object + properties: + artist: + type: string + title: + type: string + album: + type: string + duration: + type: integer + uri: + type: string + volume: + type: integer + mute: + type: string + trackNo: + type: integer + elapsedTime: + type: integer + elapsedTimeFormatted: + type: string + playbackState: + type: string + playMode: + type: object + properties: + repeat: + type: string + shuffle: + type: string + crossfade: + type: string + example: + currentTrack: + artist: The Rolling Stones + title: Get Off My Cloud + album: 'Forty Licks [Disc 1]' + duration: 176 + uri: >- + x-file-cifs://ripley/music/V/Various%20Artists/stomp/14%20Get%20Off%20My%20Cloud.mp3 + type: track + stationName: '' + nextTrack: + artist: Butthole Surfers + title: Sweat Loaf + album: Locust Abortion Technician + duration: 358 + uri: >- + x-file-cifs://ripley/music/V/Various%20Artists/stomp/01%20-%20Sweat%20Loaf.mp3 + volume: 9 + mute: mute off + trackNo: 1 + elapsedTime: 0 + elapsedTimeFormatted: '00:00:00' + playbackState: pause + playMode: + repeat: none + shuffle: shuffle on + crossfade: crossfade on + searchResult: + title: searchResult + type: object + description: successful result + properties: + returned: + type: integer + start: + type: integer + total: + type: integer + items: + type: array + items: + type: object + properties: + uri: + type: string + title: + type: string + artist: + type: string + album: + type: string + albumTrackNumber: + type: + - integer + - 'null' + imageUrl: + type: string + type: + type: string + metadata: + type: string + synopsis: + type: string + station: + type: string + broadcast: + type: string + duration: + type: integer + required: + - uri + - title + - type + - metadata + previous: + type: string + next: + type: string + required: + - returned + - start + - total + example: + items: + - title: Mr. Blue Sky + artist: Electric Light Orchestra + album: Out of the Blue + imageUrl: 'https://i.scdn.co/image/6e4bfaca5c09f0f480a51ac3b851e411b7338eb2' + albumTrackNumber: 13 + type: song + uri: >- + x-sonos-spotify:spotify%3atrack%3a2RlgNHKcydI9sayD2Df2xp?sid=9&flags=8224&sn=5 + metadata: >- + + Mr. Blue Sky radioobject.item.audioItem.musicTrack + SA_RINCON2311_X_#Svc2311-0-Token + - title: 'Blue [Da Ba Dee]' + artist: Eiffel 65 + album: 'Blue [da ba dee] [2009 Remixes]' + imageUrl: 'https://i.scdn.co/image/8c074f2a8eebc27bc034c07ddd563053b918d843' + albumTrackNumber: 7 + type: song + uri: >- + x-sonos-spotify:spotify%3atrack%3a3UCDrOA37BBdcExyIpN3Xj?sid=9&flags=8224&sn=5 + metadata: >- + + Blue [Da Ba Dee] radioobject.item.audioItem.musicTrack + SA_RINCON2311_X_#Svc2311-0-Token + - title: Still Falling For You - Jonas Blue Remix + artist: Ellie Goulding + album: Still Falling For You (Jonas Blue Remix) + imageUrl: 'https://i.scdn.co/image/fa66f40185626231236c0f8beea576f21ae9e866' + albumTrackNumber: 1 + type: song + uri: >- + x-sonos-spotify:spotify%3atrack%3a0SwjtkL3kzjZeJo9YrWUAH?sid=9&flags=8224&sn=5 + metadata: >- + + Still Falling For You - Jonas Blue Remix radioobject.item.audioItem.musicTrack + SA_RINCON2311_X_#Svc2311-0-Token + - title: Blue Christmas + artist: Michael Bublé + album: Christmas (Deluxe Special Edition) + imageUrl: 'https://i.scdn.co/image/91c2445c170390988d8010c45abb5c9d4c2db174' + albumTrackNumber: 11 + type: song + uri: >- + x-sonos-spotify:spotify%3atrack%3a0QUxMKYur7kAtauLnmyBCc?sid=9&flags=8224&sn=5 + metadata: >- + + Blue Christmas radioobject.item.audioItem.musicTrack + SA_RINCON2311_X_#Svc2311-0-Token + - title: Blue Orchid + artist: The White Stripes + album: Get Behind Me Satan + imageUrl: 'https://i.scdn.co/image/ac718619172b6b864a19a909f05b9c4f94c455d6' + albumTrackNumber: 1 + type: song + uri: >- + x-sonos-spotify:spotify%3atrack%3a1PCA097woCMSDvZPUVeRI7?sid=9&flags=8224&sn=5 + metadata: >- + + Blue Orchid radioobject.item.audioItem.musicTrack + SA_RINCON2311_X_#Svc2311-0-Token + - title: Blue Monday - 2011 Total Version + artist: New Order + album: TOTAL + imageUrl: 'https://i.scdn.co/image/66592439e1b15a0ad718aaf99be7c539f66af1c1' + albumTrackNumber: 8 + type: song + uri: >- + x-sonos-spotify:spotify%3atrack%3a5rvY5aFTSvQAo2VNUR1Fxy?sid=9&flags=8224&sn=5 + metadata: >- + + Blue Monday - 2011 Total Version radioobject.item.audioItem.musicTrack + SA_RINCON2311_X_#Svc2311-0-Token + - title: Blue Jeans + artist: Lana Del Rey + album: Born To Die - The Paradise Edition + imageUrl: 'https://i.scdn.co/image/d0b1088e6172acbe186bd7cdb47b099d252261ff' + albumTrackNumber: 3 + type: song + uri: >- + x-sonos-spotify:spotify%3atrack%3a4RyK6N4IQ85xxLgguQAFH5?sid=9&flags=8224&sn=5 + metadata: >- + + Blue Jeans radioobject.item.audioItem.musicTrack + SA_RINCON2311_X_#Svc2311-0-Token + - title: Blue Christmas + artist: Elvis Presley + album: Christmas Hits + imageUrl: 'https://i.scdn.co/image/2cffda0efefb495da2db0d7043166621526ffd35' + albumTrackNumber: 15 + type: song + uri: >- + x-sonos-spotify:spotify%3atrack%3a2trWYvBwj0DV2JF3e2Y0d1?sid=9&flags=8224&sn=5 + metadata: >- + + Blue Christmas radioobject.item.audioItem.musicTrack + SA_RINCON2311_X_#Svc2311-0-Token + - title: BLUE + artist: Troye Sivan + album: Blue Neighbourhood (Deluxe) + imageUrl: 'https://i.scdn.co/image/398e274e8b7cf908636f14af46e224e696196f9d' + albumTrackNumber: 15 + type: song + uri: >- + x-sonos-spotify:spotify%3atrack%3a0W2YQYzdW7EDKt17DfR8TV?sid=9&flags=8224&sn=5 + metadata: >- + + BLUE radioobject.item.audioItem.musicTrack + SA_RINCON2311_X_#Svc2311-0-Token + - title: Blue Notes + artist: Meek Mill + album: DC4 + imageUrl: 'https://i.scdn.co/image/04d983667863b05517687f2bf0e4d3004dd7266c' + albumTrackNumber: 8 + type: song + uri: >- + x-sonos-spotify:spotify%3atrack%3a7Gs3otHnZDq514kFHf0nx7?sid=9&flags=8224&sn=5 + metadata: >- + + Blue Notes radioobject.item.audioItem.musicTrack + SA_RINCON2311_X_#Svc2311-0-Token + - title: Blue (Da Ba Dee) + artist: Eiffel 65 + album: Europop + imageUrl: 'https://i.scdn.co/image/53a1ca4270ed9df62ea3fc0924caa49772409e6d' + albumTrackNumber: 4 + type: song + uri: >- + x-sonos-spotify:spotify%3atrack%3a5FgtdSf7I5lClThz2ptWvl?sid=9&flags=8224&sn=5 + metadata: >- + + Blue (Da Ba Dee) radioobject.item.audioItem.musicTrack + SA_RINCON2311_X_#Svc2311-0-Token + - title: Blue Lights + artist: Jorja Smith + album: Blue Lights + imageUrl: 'https://i.scdn.co/image/61f085ef5bd1c8d00bf47c252ddbfd9f4b14b1ce' + albumTrackNumber: 1 + type: song + uri: >- + x-sonos-spotify:spotify%3atrack%3a2h1htUS8h2oYq63385ZApR?sid=9&flags=8224&sn=5 + metadata: >- + + Blue Lights radioobject.item.audioItem.musicTrack + SA_RINCON2311_X_#Svc2311-0-Token + - title: Blue + artist: Beyoncé + album: 'BEYONCÉ [Platinum Edition]' + imageUrl: 'https://i.scdn.co/image/98be8968e1c29e6ef80831c5867733d2e687b508' + albumTrackNumber: 14 + type: song + uri: >- + x-sonos-spotify:spotify%3atrack%3a2LjkT4gu5wO4JdeEYl0fMY?sid=9&flags=8224&sn=5 + metadata: >- + + Blue radioobject.item.audioItem.musicTrack + SA_RINCON2311_X_#Svc2311-0-Token + - title: Behind Blue Eyes + artist: Limp Bizkit + album: Greatest Hitz (UK/Japan Version) + imageUrl: 'https://i.scdn.co/image/95e07602fe458bb439b5a3f9ee78f6a245ce7712' + albumTrackNumber: 13 + type: song + uri: >- + x-sonos-spotify:spotify%3atrack%3a54rZnh93otfFvkG37BkY39?sid=9&flags=8224&sn=5 + metadata: >- + + Behind Blue Eyes radioobject.item.audioItem.musicTrack + SA_RINCON2311_X_#Svc2311-0-Token + - title: Black and Blue + artist: Rory Butler + album: Black and Blue + imageUrl: 'https://i.scdn.co/image/0d62cdbde82532f40bbfa1221b7c3bb5d6446129' + albumTrackNumber: 1 + type: song + uri: >- + x-sonos-spotify:spotify%3atrack%3a02FOUinYxp3SWmFApdYGTu?sid=9&flags=8224&sn=5 + metadata: >- + + Black and Blue radioobject.item.audioItem.musicTrack + SA_RINCON2311_X_#Svc2311-0-Token + - title: Blue in Green + artist: Miles Davis + album: Kind Of Blue (Legacy Edition) + imageUrl: 'https://i.scdn.co/image/dd695d8a18d33640f2abca4588ef36a8ac1811a6' + albumTrackNumber: 3 + type: song + uri: >- + x-sonos-spotify:spotify%3atrack%3a0aWMVrwxPNYkKmFthzmpRi?sid=9&flags=8224&sn=5 + metadata: >- + + Blue in Green radioobject.item.audioItem.musicTrack + SA_RINCON2311_X_#Svc2311-0-Token + - title: Forever In Blue Jeans + artist: Neil Diamond + album: You Don't Bring Me Flowers + imageUrl: 'https://i.scdn.co/image/2243a147c2bd56362d245d026ef146d3aaf34beb' + albumTrackNumber: 2 + type: song + uri: >- + x-sonos-spotify:spotify%3atrack%3a1HNVGw472sKr3TeiECwmIH?sid=9&flags=8224&sn=5 + metadata: >- + + Forever In Blue Jeans radioobject.item.audioItem.musicTrack + SA_RINCON2311_X_#Svc2311-0-Token + - title: Pale Blue Eyes + artist: The Velvet Underground + album: The Velvet Underground + imageUrl: 'https://i.scdn.co/image/cc224b49c62de883afd35d12f73701b4589bcbbf' + albumTrackNumber: 4 + type: song + uri: >- + x-sonos-spotify:spotify%3atrack%3a1cG560wZP0s6fs1nsEVtQw?sid=9&flags=8224&sn=5 + metadata: >- + + Pale Blue Eyes radioobject.item.audioItem.musicTrack + SA_RINCON2311_X_#Svc2311-0-Token + - title: Blue Sky + artist: CAZZETTE + album: Blue Sky (ft. Laleh) + imageUrl: 'https://i.scdn.co/image/200042113b6dafcc45bb030529d70ffd2c8e714d' + albumTrackNumber: 1 + type: song + uri: >- + x-sonos-spotify:spotify%3atrack%3a6v9sCNj1KY3wblGJAohxX2?sid=9&flags=8224&sn=5 + metadata: >- + + Blue Sky radioobject.item.audioItem.musicTrack + SA_RINCON2311_X_#Svc2311-0-Token + - title: Blue Christmas + artist: Elvis Presley + album: Elvis' Christmas Album + imageUrl: 'https://i.scdn.co/image/d8ec333974abb5987719025a55a7395a22becb79' + albumTrackNumber: 5 + type: song + uri: >- + x-sonos-spotify:spotify%3atrack%3a3QiAAp20rPC3dcAtKtMaqQ?sid=9&flags=8224&sn=5 + metadata: >- + + Blue Christmas radioobject.item.audioItem.musicTrack + SA_RINCON2311_X_#Svc2311-0-Token + returned: 20 + total: 169596 + start: 0 + next: >- + http://192.168.1.17:10010/search?service=spotify&type=song&q=blue&limit=20&offset=20 + queueRequest: + title: queueRequest + type: object + description: successful result + properties: + uri: + type: string + metadata: + type: string + enqueAsNext: + type: boolean + desiredFirstTrackNumberEnqueued: + type: integer + errorResponse: + title: errorResponse + type: object + description: error result + properties: + code: + type: string + message: + type: string + description: + type: string + errors: + type: array + items: + type: object + properties: + error: + type: string + message: + type: string + '': + type: string + example: + code: not.implemnted.yet + message: not implemented yet + description: not implemented yet + errors: + - error: not implemnted yet + message: not implemented yet + playersResult: + title: playersResult + type: array + items: + $ref: '#/definitions/player' + example: + - coordinator: RINCON_000E58C4373C01400 + groupState: + volume: 0 + mute: false + roomName: Bedroom + state: + currentTrack: + artist: '' + title: '' + album: '' + albumArtUri: '' + duration: 0 + uri: '' + type: track + stationName: '' + nextTrack: + artist: '' + title: '' + album: '' + albumArtUri: '' + duration: 0 + uri: '' + volume: 0 + mute: mute off + trackNo: 0 + elapsedTime: 0 + elapsedTimeFormatted: '00:00:00' + playbackState: pause + playMode: + repeat: none + shuffle: shuffle off + crossfade: crossfade off + uuid: RINCON_000E58C4373C01400 + - coordinator: RINCON_000E585394A801400 + groupState: + volume: 0 + mute: false + roomName: Kitchen + state: + currentTrack: + artist: '' + title: '' + album: '' + albumArtUri: '' + duration: 0 + uri: '' + type: track + stationName: '' + nextTrack: + artist: '' + title: '' + album: '' + albumArtUri: '' + duration: 0 + uri: '' + volume: 0 + mute: mute off + trackNo: 0 + elapsedTime: 0 + elapsedTimeFormatted: '00:00:00' + playbackState: pause + playMode: + repeat: none + shuffle: shuffle off + crossfade: crossfade off + uuid: RINCON_000E585394A801400 + - coordinator: RINCON_5CAAFD23191C01400 + groupState: + volume: 0 + mute: false + roomName: James bedroom + state: + currentTrack: + artist: '' + title: '' + album: '' + albumArtUri: '' + duration: 0 + uri: '' + type: track + stationName: '' + nextTrack: + artist: '' + title: '' + album: '' + albumArtUri: '' + duration: 0 + uri: '' + volume: 0 + mute: mute off + trackNo: 0 + elapsedTime: 0 + elapsedTimeFormatted: '00:00:00' + playbackState: pause + playMode: + repeat: none + shuffle: shuffle off + crossfade: crossfade off + uuid: RINCON_5CAAFD23191C01400 + membersResult: + title: membersResult + type: array + items: + $ref: '#/definitions/member' + stateInput: + title: stateInput + type: object + properties: + currentTrack: + type: object + properties: + favourite: + type: string + playlist: + type: string + clip: + type: string + text: + type: string + skip: + type: string + source: + type: string + lineinSource: + type: string + artistTopTracks: + type: string + artistRadio: + type: string + song: + type: string + '': + type: string + volume: + type: + - integer + - string + mute: + type: string + enum: + - mute on + - mute off + trackNo: + type: integer + elapsedTime: + type: integer + playbackState: + type: string + enum: + - play + - pause + - toggle + playMode: + type: object + properties: + repeat: + type: string + enum: + - all + - one + - none + shuffle: + type: string + enum: + - shuffle on + - shuffle off + crossfade: + type: string + enum: + - crossfade on + - crossfade off + '': + type: string + example: + currentTrack: + favourite: '' + playlist: '' + clip: '' + text: '' + skip: previous + volume: 22 + mute: mute off + trackNo: 0 + elapsedTime: 0 + playbackState: pause + playMode: + repeat: none + shuffle: shuffle off + crossfade: crossfade off + zone: + title: zone + type: object + properties: + zoneName: + type: string + state: + $ref: '#/definitions/state' + members: + type: array + items: + $ref: '#/definitions/member' + uuid: + type: string + example: + zoneName: Bedroom + state: + currentTrack: + artist: The Rolling Stones + title: Get Off My Cloud + album: 'Forty Licks [Disc 1]' + duration: 176 + uri: >- + x-file-cifs://ripley/music/V/Various%20Artists/stomp/14%20Get%20Off%20My%20Cloud.mp3 + type: track + stationName: '' + nextTrack: + artist: Butthole Surfers + title: Sweat Loaf + album: Locust Abortion Technician + duration: 358 + uri: >- + x-file-cifs://ripley/music/V/Various%20Artists/stomp/01%20-%20Sweat%20Loaf.mp3 + volume: 9 + mute: mute off + trackNo: 1 + elapsedTime: 0 + elapsedTimeFormatted: '00:00:00' + playbackState: pause + playMode: + repeat: none + shuffle: shuffle on + crossfade: crossfade on + members: + - playerName: James bedroom + state: + currentTrack: + artist: The Rolling Stones + title: Get Off My Cloud + album: 'Forty Licks [Disc 1]' + duration: 176 + uri: >- + x-file-cifs://ripley/music/V/Various%20Artists/stomp/14%20Get%20Off%20My%20Cloud.mp3 + type: track + stationName: '' + nextTrack: + artist: Butthole Surfers + title: Sweat Loaf + album: Locust Abortion Technician + duration: 358 + uri: >- + x-file-cifs://ripley/music/V/Various%20Artists/stomp/01%20-%20Sweat%20Loaf.mp3 + volume: 17 + mute: mute off + trackNo: 1 + elapsedTime: 0 + elapsedTimeFormatted: '00:00:00' + playbackState: pause + playMode: + repeat: none + shuffle: shuffle on + crossfade: crossfade on + uuid: RINCON_5CAAFD23191C01400 + - playerName: Bedroom + state: + currentTrack: + artist: The Rolling Stones + title: Get Off My Cloud + album: 'Forty Licks [Disc 1]' + duration: 176 + uri: >- + x-file-cifs://ripley/music/V/Various%20Artists/stomp/14%20Get%20Off%20My%20Cloud.mp3 + type: track + stationName: '' + nextTrack: + artist: Butthole Surfers + title: Sweat Loaf + album: Locust Abortion Technician + duration: 358 + uri: >- + x-file-cifs://ripley/music/V/Various%20Artists/stomp/01%20-%20Sweat%20Loaf.mp3 + volume: 1 + mute: mute off + trackNo: 1 + elapsedTime: 0 + elapsedTimeFormatted: '00:00:00' + playbackState: pause + playMode: + repeat: none + shuffle: shuffle on + crossfade: crossfade on + uuid: RINCON_000E58C4373C01400 + uuid: RINCON_000E58C4373C01400 + nowPlayingResult: + title: nowPlayingResult + type: object + description: successful result + properties: + artist: + type: string + title: + type: string + album: + type: string + albumArtUri: + type: string + duration: + type: integer + uri: + type: string + type: + type: string + stationName: + type: string + absoluteAlbumArtUri: + type: string + uriMetadata: + type: string + avTransportUri: + type: string + example: + artist: The Animal In Me + title: We Don't Talk Anymore + album: We Don't Talk Anymore + albumArtUri: >- + /getaa?s=1&u=x-sonos-spotify%3aspotify%253atrack%253a2wTSNikJDa988ppmb1L1bH%3fsid%3d9%26flags%3d8224%26sn%3d7 + duration: 220 + uri: >- + x-sonos-spotify:spotify%3atrack%3a2wTSNikJDa988ppmb1L1bH?sid=9&flags=8224&sn=7 + type: track + stationName: '' + absoluteAlbumArtUri: 'https://i.scdn.co/image/e4ccfd7ed272b9e668f84fc01c2fc3c621d3d370' + uriMetadata: '' + avTransportUri: 'x-rincon-queue:RINCON_000E585394A801400#0' + favouritesResult: + title: favouritesResult + type: array + description: successful result + items: + type: object + properties: + uri: + type: string + title: + type: string + metadata: + type: string + albumArtUri: + type: string + example: + - uri: 'x-sonosapi-stream:s24939?sid=254&flags=8224&sn=0' + title: BBC Radio 1 + metadata: >- + BBC + Radio + 1object.item.audioItem.audioBroadcastSA_RINCON65031_ + - uri: 'x-sonosapi-stream:s20277?sid=254&flags=8224&sn=0' + title: BBC Radio 1Xtra + metadata: >- + BBC + Radio + 1Xtraobject.item.audioItem.audioBroadcastSA_RINCON65031_ + - uri: 'x-sonosapi-stream:s24940?sid=254&flags=8224&sn=0' + title: BBC Radio 2 + metadata: >- + BBC + Radio + 2object.item.audioItem.audioBroadcastSA_RINCON65031_ + - uri: 'x-sonosapi-stream:s24941?sid=254&flags=8224&sn=0' + title: BBC Radio 3 + metadata: >- + BBC + Radio + 3object.item.audioItem.audioBroadcastSA_RINCON65031_ + - uri: 'x-sonosapi-stream:s25419?sid=254&flags=8224&sn=0' + title: BBC Radio 4 + metadata: >- + BBC + Radio + 4object.item.audioItem.audioBroadcastSA_RINCON65031_ + - uri: 'x-sonosapi-stream:s6839?sid=254&flags=8224&sn=0' + title: BBC Radio 4 Extra + metadata: >- + BBC + Radio 4 + Extraobject.item.audioItem.audioBroadcastSA_RINCON65031_ + - uri: 'x-sonosapi-stream:s24943?sid=254&flags=8224&sn=0' + title: BBC Radio 5 live + metadata: >- + BBC + Radio 5 + liveobject.item.audioItem.audioBroadcastSA_RINCON65031_ + - uri: 'x-sonosapi-stream:s44491?sid=254&flags=8224&sn=0' + title: BBC Radio 6 Music + metadata: >- + BBC + Radio 6 + Musicobject.item.audioItem.audioBroadcastSA_RINCON65031_ + - uri: 'x-sonosapi-stream:s45579?sid=254&flags=32&sn=0' + title: Kerrang! Radio + albumArtUri: 'http://d1i6vahw24eb07.cloudfront.net/s45579q.gif' + metadata: >- + Kerrang! + Radioobject.item.audioItem.audioBroadcastSA_RINCON65031_ + - uri: 'x-rincon-mp3radio://192.168.1.2:9790/minimstreamer/*/R4' + title: proxy radio 4 + metadata: >- + proxy + radio + 4object.item.audioItem.audioBroadcastSA_RINCON65031_ + - uri: 'x-rincon-mp3radio://192.168.1.2:9790/minimstreamer/*/R6' + title: proxy radio 6 + metadata: >- + proxy + r6object.item.audioItem.audioBroadcastSA_RINCON65031_ + - uri: 'x-sonosapi-stream:s190122?sid=254&flags=8224&sn=0' + title: 'SomaFM: Folk Forward' + albumArtUri: 'http://cdn-radiotime-logos.tunein.com/s190122q.png' + metadata: >- + SomaFM: Folk + Forwardobject.item.audioItem.audioBroadcastSA_RINCON65031_ + - uri: 'x-sonosapi-stream:s200662?sid=254&flags=8224&sn=0' + title: TeamRock Radio + albumArtUri: 'http://cdn-radiotime-logos.tunein.com/s200662q.png' + metadata: >- + TeamRock + Radioobject.item.audioItem.audioBroadcastSA_RINCON65031_ + - uri: 'x-sonosapi-stream:s266113?sid=254&flags=8224&sn=0' + title: Virgin Radio UK + albumArtUri: 'http://cdn-radiotime-logos.tunein.com/s266113q.png' + metadata: >- + Virgin Radio + UKobject.item.audioItem.audioBroadcastSA_RINCON65031_ + zonesResult: + title: zonesResult + type: array + description: successful result + items: + $ref: '#/definitions/zone' + example: + - uuid: RINCON_000E585394A801400 + coordinator: + uuid: RINCON_000E585394A801400 + state: + currentTrack: + artist: Virgin Radio UK + title: 'x-sonosapi-stream:s266113?sid=254&flags=8224&sn=0' + albumArtUri: >- + /getaa?s=1&u=x-sonosapi-stream%3as266113%3fsid%3d254%26flags%3d8224%26sn%3d0 + duration: 0 + uri: 'x-sonosapi-stream:s266113?sid=254&flags=8224&sn=0' + type: radio + stationName: Virgin Radio UK + absoluteAlbumArtUri: >- + http://192.168.1.19:1400/getaa?s=1&u=x-sonosapi-stream%3as266113%3fsid%3d254%26flags%3d8224%26sn%3d0 + nextTrack: + artist: '' + title: '' + album: '' + albumArtUri: '' + duration: 0 + uri: '' + volume: 19 + mute: false + trackNo: 1 + elapsedTime: 0 + elapsedTimeFormatted: '00:00:00' + playbackState: STOPPED + playMode: + repeat: none + shuffle: false + crossfade: false + roomName: Kitchen + coordinator: RINCON_000E585394A801400 + groupState: + volume: 19 + mute: false + members: + - uuid: RINCON_000E585394A801400 + state: + currentTrack: + artist: Virgin Radio UK + title: 'x-sonosapi-stream:s266113?sid=254&flags=8224&sn=0' + albumArtUri: >- + /getaa?s=1&u=x-sonosapi-stream%3as266113%3fsid%3d254%26flags%3d8224%26sn%3d0 + duration: 0 + uri: 'x-sonosapi-stream:s266113?sid=254&flags=8224&sn=0' + type: radio + stationName: Virgin Radio UK + absoluteAlbumArtUri: >- + http://192.168.1.19:1400/getaa?s=1&u=x-sonosapi-stream%3as266113%3fsid%3d254%26flags%3d8224%26sn%3d0 + nextTrack: + artist: '' + title: '' + album: '' + albumArtUri: '' + duration: 0 + uri: '' + volume: 19 + mute: false + trackNo: 1 + elapsedTime: 0 + elapsedTimeFormatted: '00:00:00' + playbackState: STOPPED + playMode: + repeat: none + shuffle: false + crossfade: false + roomName: Kitchen + coordinator: RINCON_000E585394A801400 + groupState: + volume: 19 + mute: false + - uuid: RINCON_5CAAFD23191C01400 + coordinator: + uuid: RINCON_5CAAFD23191C01400 + state: + currentTrack: + artist: Fall Out Boy + title: Thriller - Live From Hammersmith Palais + album: Infinity On High (Deluxe Edition) + albumArtUri: >- + /getaa?s=1&u=x-sonos-spotify%3aspotify%253atrack%253a69H6lMNa5c10vN43tBDjt5%3fsid%3d9%26flags%3d8224%26sn%3d9 + duration: 212 + uri: >- + x-sonos-spotify:spotify%3atrack%3a69H6lMNa5c10vN43tBDjt5?sid=9&flags=8224&sn=9 + type: track + stationName: '' + absoluteAlbumArtUri: 'https://i.scdn.co/image/182f82994a6b8e3f866649ded4dbecdb7f9f5d1d' + nextTrack: + artist: Fall Out Boy + title: 'This Ain''t A Scene, It''s An Arms Race' + album: Infinity On High (Deluxe Edition) + albumArtUri: >- + /getaa?s=1&u=x-sonos-spotify%3aspotify%253atrack%253a2GD8Ia7rtJzfgTIy20TaRI%3fsid%3d9%26flags%3d8224%26sn%3d9 + duration: 212 + uri: >- + x-sonos-spotify:spotify%3atrack%3a2GD8Ia7rtJzfgTIy20TaRI?sid=9&flags=8224&sn=9 + absoluteAlbumArtUri: 'https://i.scdn.co/image/182f82994a6b8e3f866649ded4dbecdb7f9f5d1d' + volume: 19 + mute: false + trackNo: 3 + elapsedTime: 65 + elapsedTimeFormatted: '00:01:05' + playbackState: PAUSED_PLAYBACK + playMode: + repeat: none + shuffle: true + crossfade: false + roomName: James bedroom + coordinator: RINCON_5CAAFD23191C01400 + groupState: + volume: 19 + mute: false + members: + - uuid: RINCON_5CAAFD23191C01400 + state: + currentTrack: + artist: Fall Out Boy + title: Thriller - Live From Hammersmith Palais + album: Infinity On High (Deluxe Edition) + albumArtUri: >- + /getaa?s=1&u=x-sonos-spotify%3aspotify%253atrack%253a69H6lMNa5c10vN43tBDjt5%3fsid%3d9%26flags%3d8224%26sn%3d9 + duration: 212 + uri: >- + x-sonos-spotify:spotify%3atrack%3a69H6lMNa5c10vN43tBDjt5?sid=9&flags=8224&sn=9 + type: track + stationName: '' + absoluteAlbumArtUri: >- + https://i.scdn.co/image/182f82994a6b8e3f866649ded4dbecdb7f9f5d1d + nextTrack: + artist: Fall Out Boy + title: 'This Ain''t A Scene, It''s An Arms Race' + album: Infinity On High (Deluxe Edition) + albumArtUri: >- + /getaa?s=1&u=x-sonos-spotify%3aspotify%253atrack%253a2GD8Ia7rtJzfgTIy20TaRI%3fsid%3d9%26flags%3d8224%26sn%3d9 + duration: 212 + uri: >- + x-sonos-spotify:spotify%3atrack%3a2GD8Ia7rtJzfgTIy20TaRI?sid=9&flags=8224&sn=9 + absoluteAlbumArtUri: >- + https://i.scdn.co/image/182f82994a6b8e3f866649ded4dbecdb7f9f5d1d + volume: 19 + mute: false + trackNo: 3 + elapsedTime: 65 + elapsedTimeFormatted: '00:01:05' + playbackState: PAUSED_PLAYBACK + playMode: + repeat: none + shuffle: true + crossfade: false + roomName: James bedroom + coordinator: RINCON_5CAAFD23191C01400 + groupState: + volume: 19 + mute: false + - uuid: RINCON_000E58C4373C01400 + coordinator: + uuid: RINCON_000E58C4373C01400 + state: + currentTrack: + artist: Virgin Radio UK + title: 'x-sonosapi-stream:s266113?sid=254&flags=8224&sn=0' + albumArtUri: >- + /getaa?s=1&u=x-sonosapi-stream%3as266113%3fsid%3d254%26flags%3d8224%26sn%3d0 + duration: 0 + uri: 'x-sonosapi-stream:s266113?sid=254&flags=8224&sn=0' + type: radio + stationName: Virgin Radio UK + absoluteAlbumArtUri: >- + http://192.168.1.21:1400/getaa?s=1&u=x-sonosapi-stream%3as266113%3fsid%3d254%26flags%3d8224%26sn%3d0 + nextTrack: + artist: '' + title: '' + album: '' + albumArtUri: '' + duration: 0 + uri: '' + volume: 25 + mute: false + trackNo: 1 + elapsedTime: 0 + elapsedTimeFormatted: '00:00:00' + playbackState: STOPPED + playMode: + repeat: none + shuffle: false + crossfade: false + roomName: Bedroom + coordinator: RINCON_000E58C4373C01400 + groupState: + volume: 25 + mute: false + members: + - uuid: RINCON_000E58C4373C01400 + state: + currentTrack: + artist: Virgin Radio UK + title: 'x-sonosapi-stream:s266113?sid=254&flags=8224&sn=0' + albumArtUri: >- + /getaa?s=1&u=x-sonosapi-stream%3as266113%3fsid%3d254%26flags%3d8224%26sn%3d0 + duration: 0 + uri: 'x-sonosapi-stream:s266113?sid=254&flags=8224&sn=0' + type: radio + stationName: Virgin Radio UK + absoluteAlbumArtUri: >- + http://192.168.1.21:1400/getaa?s=1&u=x-sonosapi-stream%3as266113%3fsid%3d254%26flags%3d8224%26sn%3d0 + nextTrack: + artist: '' + title: '' + album: '' + albumArtUri: '' + duration: 0 + uri: '' + volume: 25 + mute: false + trackNo: 1 + elapsedTime: 0 + elapsedTimeFormatted: '00:00:00' + playbackState: STOPPED + playMode: + repeat: none + shuffle: false + crossfade: false + roomName: Bedroom + coordinator: RINCON_000E58C4373C01400 + groupState: + volume: 25 + mute: false + member: + title: member + type: object + properties: + playerName: + type: string + state: + type: object + properties: + currentTrack: + type: object + properties: + artist: + type: string + title: + type: string + album: + type: string + duration: + type: integer + uri: + type: string + type: + type: string + stationName: + type: string + nextTrack: + type: object + properties: + artist: + type: string + title: + type: string + album: + type: string + duration: + type: integer + uri: + type: string + volume: + type: integer + mute: + type: string + trackNo: + type: integer + elapsedTime: + type: integer + elapsedTimeFormatted: + type: string + playbackState: + type: string + playMode: + type: object + properties: + repeat: + type: string + shuffle: + type: string + crossfade: + type: string + uuid: + type: string + example: + playerName: Bedroom + state: + currentTrack: + artist: Senseless Things + title: Hold It Down + album: Empire Of The Senseless + duration: 263 + uri: >- + x-file-cifs://ripley/music/V/Various%20Artists/stomp/04%20-%20Hold%20It%20Down.mp3 + type: track + stationName: '' + nextTrack: + artist: The Rolling Stones + title: Get Off My Cloud + album: 'Forty Licks [Disc 1]' + duration: 176 + uri: >- + x-file-cifs://ripley/music/V/Various%20Artists/stomp/14%20Get%20Off%20My%20Cloud.mp3 + volume: 1 + mute: mute off + trackNo: 1 + elapsedTime: 0 + elapsedTimeFormatted: '00:00:00' + playbackState: pause + playMode: + repeat: none + shuffle: shuffle on + crossfade: crossfade on + uuid: RINCON_000E58C4373C01400 + favourite: + title: favourite + type: object + properties: + uri: + type: string + title: + type: string + albumArtUri: + type: string + metadata: + type: string + example: + uri: 'x-sonosapi-stream:s266113?sid=254&flags=8224&sn=0' + title: Virgin Radio UK + albumArtUri: 'http://cdn-radiotime-logos.tunein.com/s266113q.png' + metadata: >- + Virgin Radio + UKobject.item.audioItem.audioBroadcastSA_RINCON65031_ + ok: + title: ok + type: object + example: + message: OK diff --git a/app.js b/app.js new file mode 100644 index 0000000..babcbd4 --- /dev/null +++ b/app.js @@ -0,0 +1,88 @@ +'use strict'; +const path = require('path'); +const SwaggerExpress = require('swagger-express-mw'); +const express = require('express'); +const log4js = require('log4js'); +const logger = log4js.getLogger('app.js'); +const SonosDiscovery = require('sonos-discovery'); +const util = require('util'); +const Promise = require('bluebird'); +const fs = Promise.promisifyAll(require('fs')); +const discovery = new SonosDiscovery(); +const startupHelpers = require('./startupHelpers'); + + +// Store settings so they can be easily accessed +const settings = startupHelpers.loadConfig(); + +logger.debug(settings); + +// Create tts directory if it does not exist +const ttsDir = path.resolve(settings.staticWebRootPath, 'tts'); + +startupHelpers.createTtsDirectory(ttsDir) + .then(() => { + return startupHelpers.createDatabaseDirectory(settings.databasePath); + }) + .then(() => { + settings.dbSettings = startupHelpers.loadDatabases(settings); + + return null; + }) + .then(() => { + // Write a file with webroot in it so that swagger ui loads the swagger document + const webRootSettingPath = path.resolve(settings.staticWebRootPath, 'docs', 'webRootSetting.js'); + const webRootSettingContents = `var webRoot = "${settings.webRoot}";`; + + return fs.writeFileAsync(webRootSettingPath, webRootSettingContents); + }) + .then(() => { + // Replace the host in swagger file with webroot so that it can be used in swagger-ui + // Note - this creates a new file production.swagger.yaml which is used to control the program + return startupHelpers.createRunningSwaggerFile(settings.webRoot); + }) + .then(() => { + const app = express(); + const config = { + appRoot: path.resolve(__dirname, './api'), + swaggerFile: path.resolve(__dirname, './api/swagger/production.swagger.yaml'), + validateResponse: true + }; + + SwaggerExpress.create(config, (err, swaggerExpress) => { + if (err) { + throw err; + } + + // Serve /docs from ./static/docs folder - path in request is added to directory below + app.use('/docs', express.static(path.resolve(__dirname, 'static/docs'))); + // Serve /static from ./static folder + app.use('/static', express.static(path.resolve(__dirname, 'static'))); + + // Inject discovery and settings into request + app.use((req, res, next) => { + req.discovery = discovery; + req.settings = settings; + next(); + }); + + // Inject discovery and settings into request + app.use((req, res, next) => { + logger.debug(`request came into ${req.method} ${req.url}`); + next(); + }); + + // Install middleware + swaggerExpress.register(app); + // Install response validation listener (this will only be called if there actually are any errors or warnings) + swaggerExpress.runner.on('responseValidationError', (validationResponse) => { + // Log response validation errors... + logger.error(util.inspect(validationResponse.errors, false, null)); + }); + app.listen(settings.port); + logger.info('The server has started'); + }); + }) + .catch((error) => { + return logger.error('Error occurred : ', error); + }); diff --git a/package.json b/package.json new file mode 100644 index 0000000..fc47742 --- /dev/null +++ b/package.json @@ -0,0 +1,42 @@ +{ + "name": "swagger-sonos-api", + "version": "0.9.0", + "description": "A swagger based node app for controlling a Sonos system with HTTP requests", + "scripts": { + "start": "node app.js", + "eslint": "eslint ." + }, + "author": "Anthony Brown ", + "repository": { + "type": "git", + "url": "https://github.com/antxxxx/sonos-swagger-api.git" + }, + "dependencies": { + "bluebird": "^3.4.6", + "debug": "^2.3.3", + "express": "^4.14.0", + "feedparser-promised": "^1.1.1", + "fs-extra": "^1.0.0", + "js-yaml": "^3.7.0", + "lodash": "^4.17.4", + "log4js": "^1.0.1", + "nconf": "^0.8.4", + "nedb": "^1.8.0", + "request": "^2.79.0", + "request-promise": "^1.0.2", + "sonos-discovery": "https://github.com/jishi/node-sonos-discovery/archive/v1.4.1.tar.gz", + "spotify-web-api-node": "^2.3.6", + "swagger-express-mw": "^0.7.0", + "sway": "^1.0.0", + "xml-to-json-promise": "0.0.3" + }, + "engines": { + "node": ">=4.0.0", + "npm": "^2.0.0" + }, + "main": "app.js", + "license": "MIT", + "devDependencies": { + "eslint": "^3.11.1" + } +} diff --git a/settings.json.example b/settings.json.example new file mode 100644 index 0000000..c64ee86 --- /dev/null +++ b/settings.json.example @@ -0,0 +1,7 @@ +{ + "settings": { + "port": 10010, + "webRoot": "http://localhost:10010", + "announceVolume": 40 + } +} \ No newline at end of file diff --git a/startupHelpers.js b/startupHelpers.js new file mode 100644 index 0000000..ec37e59 --- /dev/null +++ b/startupHelpers.js @@ -0,0 +1,167 @@ +'use strict'; + +const Promise = require('bluebird'); +const fs = Promise.promisifyAll(require('fs-extra')); +const log4js = require('log4js'); +const logger = log4js.getLogger('startupHelpers.js'); +const path = require('path'); +const sway = require('sway'); +const yaml = require('js-yaml'); +const _ = require('lodash'); +const nconf = require('nconf'); +const Datastore = require('nedb'); + +function filePathExists(filePath) { + return new Promise((resolve, reject) => { + fs.stat(filePath, (err, stats) => { + if (err && err.code === 'ENOENT') { + return resolve(false); + } else if (err) { + return reject(err); + } + if (stats.isFile() || stats.isDirectory()) { + return resolve(true); + } + + return resolve(true); + }); + }); +} + +function createTtsDirectory(ttsDir) { + return Promise.resolve() + .then(() => { + return filePathExists(ttsDir); + }) + .then((ttsExists) => { + if (!ttsExists) { + return fs.mkdirAsync(ttsDir); + } + + return null; + }) + .catch((err) => { + logger.warn(`Could not create tts directory ${ttsDir}, please create it manually for all features to work.`); + logger.warn(`Error : ${err}`); + + return null; + }); +} + +function createDatabaseDirectory(databaseDir) { + return Promise.resolve() + .then(() => { + return filePathExists(databaseDir); + }) + .then((ttsExists) => { + if (!ttsExists) { + return fs.mkdirAsync(databaseDir); + } + + return null; + }) + .catch((err) => { + logger.warn(`Could not create database directory ${databaseDir}, please create it manually for all features to work.`); + logger.warn(`Error : ${err}`); + + return null; + }); +} + +function createRunningSwaggerFile(webRoot) { + // This copies swagger.yaml to production.swagger.yaml and replaces host and schemes if necessary + // It also validates the new swagger file to make sure it is all ok + const orignalSwaggerFile = path.resolve(__dirname, './api/swagger/swagger.yaml'); + const runtimeSwaggerFile = path.resolve(__dirname, './api/swagger/production.swagger.yaml'); + let swaggerJsonObject; + + + return Promise.resolve() + .then(() => { + return fs.readFileAsync(orignalSwaggerFile, 'utf8'); + }) + .then((fileContents) => { + return yaml.safeLoad(fileContents); + }) + .then((swaggerObject) => { + swaggerJsonObject = swaggerObject; + swaggerJsonObject.host = webRoot.replace(/http(s)*:\/\//, ''); + if (webRoot.startsWith('https')) { + swaggerJsonObject.schemes = ['https']; + } + + return sway.create({ + definition: swaggerJsonObject + }); + }) + .then((swaggerObj) => { + const result = swaggerObj.validate(); + let errors = result.errors; + let warnings = result.warnings; + + if (_.isEmpty(errors)) { + errors = null; + } + + if (_.isEmpty(warnings)) { + warnings = null; + } + logger.warn(errors, warnings); + if (errors) { + throw errors; + } + + return fs.writeFileAsync(runtimeSwaggerFile, yaml.safeDump(swaggerJsonObject)); + }) + .catch((error) => { + logger.error('Error occurred modifying swagger file:', error); + + throw error; + }); +} + +function loadConfig() { + // Load settings and set defaults + nconf.file(path.resolve(__dirname, './settings.json')); + nconf.defaults({ + settings: { + port: 10010, + staticWebRootPath: path.resolve(__dirname, 'static'), + ttsProvider: 'google', + webRoot: 'http://localhost:10010', + databasePath: path.resolve(__dirname, 'localDatabase') + } + }); + + // Store settings so they can be easily accessed + return nconf.get('settings'); +} + +function loadDatabases(settings) { + const iplayerPodcastDB = Promise.promisifyAll(new Datastore({ + filename: path.resolve(settings.databasePath, 'iplayerPodcastDB.json'), + autoload: true + })); + const refreshSettingsDB = Promise.promisifyAll(new Datastore({ + filename: path.resolve(settings.databasePath, 'refreshSettingsDB.json'), + autoload: true + })); + const iplayerProgramDB = Promise.promisifyAll(new Datastore({ + filename: path.resolve(settings.databasePath, 'iplayerProgramDB.json'), + autoload: true + })); + const dbSettings = { + iplayerPodcastDB, + refreshSettingsDB, + iplayerProgramDB + }; + + return dbSettings; +} +module.exports = { + createRunningSwaggerFile, + createTtsDirectory, + createDatabaseDirectory, + loadConfig, + loadDatabases +}; diff --git a/static/clips/sample_clip.mp3 b/static/clips/sample_clip.mp3 new file mode 100644 index 0000000..41f76bd Binary files /dev/null and b/static/clips/sample_clip.mp3 differ diff --git a/static/docs/css/print.css b/static/docs/css/print.css new file mode 100644 index 0000000..54b6516 --- /dev/null +++ b/static/docs/css/print.css @@ -0,0 +1 @@ +.swagger-section pre code{display:block;padding:.5em;background:#f0f0f0}.swagger-section pre .clojure .built_in,.swagger-section pre .lisp .title,.swagger-section pre .nginx .title,.swagger-section pre .subst,.swagger-section pre .tag .title,.swagger-section pre code{color:#000}.swagger-section pre .addition,.swagger-section pre .aggregate,.swagger-section pre .apache .cbracket,.swagger-section pre .apache .tag,.swagger-section pre .bash .variable,.swagger-section pre .constant,.swagger-section pre .django .variable,.swagger-section pre .erlang_repl .function_or_atom,.swagger-section pre .flow,.swagger-section pre .markdown .header,.swagger-section pre .parent,.swagger-section pre .preprocessor,.swagger-section pre .ruby .symbol,.swagger-section pre .ruby .symbol .string,.swagger-section pre .rules .value,.swagger-section pre .rules .value .number,.swagger-section pre .smalltalk .class,.swagger-section pre .stream,.swagger-section pre .string,.swagger-section pre .tag .value,.swagger-section pre .template_tag,.swagger-section pre .tex .command,.swagger-section pre .tex .special,.swagger-section pre .title{color:#800}.swagger-section pre .annotation,.swagger-section pre .chunk,.swagger-section pre .comment,.swagger-section pre .diff .header,.swagger-section pre .markdown .blockquote,.swagger-section pre .template_comment{color:#888}.swagger-section pre .change,.swagger-section pre .date,.swagger-section pre .go .constant,.swagger-section pre .literal,.swagger-section pre .markdown .bullet,.swagger-section pre .markdown .link_url,.swagger-section pre .number,.swagger-section pre .regexp,.swagger-section pre .smalltalk .char,.swagger-section pre .smalltalk .symbol{color:#080}.swagger-section pre .apache .sqbracket,.swagger-section pre .array,.swagger-section pre .attr_selector,.swagger-section pre .clojure .attribute,.swagger-section pre .coffeescript .property,.swagger-section pre .decorator,.swagger-section pre .deletion,.swagger-section pre .doctype,.swagger-section pre .envvar,.swagger-section pre .erlang_repl .reserved,.swagger-section pre .filter .argument,.swagger-section pre .important,.swagger-section pre .javadoc,.swagger-section pre .label,.swagger-section pre .localvars,.swagger-section pre .markdown .link_label,.swagger-section pre .nginx .built_in,.swagger-section pre .pi,.swagger-section pre .prompt,.swagger-section pre .pseudo,.swagger-section pre .ruby .string,.swagger-section pre .shebang,.swagger-section pre .tex .formula,.swagger-section pre .vhdl .attribute{color:#88f}.swagger-section pre .aggregate,.swagger-section pre .apache .tag,.swagger-section pre .bash .variable,.swagger-section pre .built_in,.swagger-section pre .css .tag,.swagger-section pre .go .typename,.swagger-section pre .id,.swagger-section pre .javadoctag,.swagger-section pre .keyword,.swagger-section pre .markdown .strong,.swagger-section pre .phpdoc,.swagger-section pre .request,.swagger-section pre .smalltalk .class,.swagger-section pre .status,.swagger-section pre .tex .command,.swagger-section pre .title,.swagger-section pre .winutils,.swagger-section pre .yardoctag{font-weight:700}.swagger-section pre .markdown .emphasis{font-style:italic}.swagger-section pre .nginx .built_in{font-weight:400}.swagger-section pre .coffeescript .javascript,.swagger-section pre .javascript .xml,.swagger-section pre .tex .formula,.swagger-section pre .xml .cdata,.swagger-section pre .xml .css,.swagger-section pre .xml .javascript,.swagger-section pre .xml .vbscript{opacity:.5}.swagger-section .hljs{display:block;overflow-x:auto;padding:.5em;background:#f0f0f0}.swagger-section .hljs,.swagger-section .hljs-subst{color:#444}.swagger-section .hljs-attribute,.swagger-section .hljs-doctag,.swagger-section .hljs-keyword,.swagger-section .hljs-meta-keyword,.swagger-section .hljs-name,.swagger-section .hljs-selector-tag{font-weight:700}.swagger-section .hljs-addition,.swagger-section .hljs-built_in,.swagger-section .hljs-bullet,.swagger-section .hljs-code,.swagger-section .hljs-literal{color:#1f811f}.swagger-section .hljs-link,.swagger-section .hljs-regexp,.swagger-section .hljs-selector-attr,.swagger-section .hljs-selector-pseudo,.swagger-section .hljs-symbol,.swagger-section .hljs-template-variable,.swagger-section .hljs-variable{color:#bc6060}.swagger-section .hljs-deletion,.swagger-section .hljs-number,.swagger-section .hljs-quote,.swagger-section .hljs-selector-class,.swagger-section .hljs-selector-id,.swagger-section .hljs-string,.swagger-section .hljs-template-tag,.swagger-section .hljs-type{color:#800}.swagger-section .hljs-section,.swagger-section .hljs-title{color:#800;font-weight:700}.swagger-section .hljs-comment{color:#888}.swagger-section .hljs-meta{color:#2b6ea1}.swagger-section .hljs-emphasis{font-style:italic}.swagger-section .hljs-strong{font-weight:700}.swagger-section .swagger-ui-wrap{line-height:1;font-family:Droid Sans,sans-serif;min-width:760px;max-width:960px;margin-left:auto;margin-right:auto}.swagger-section .swagger-ui-wrap b,.swagger-section .swagger-ui-wrap strong{font-family:Droid Sans,sans-serif;font-weight:700}.swagger-section .swagger-ui-wrap blockquote,.swagger-section .swagger-ui-wrap q{quotes:none}.swagger-section .swagger-ui-wrap p{line-height:1.4em;padding:0 0 10px;color:#333}.swagger-section .swagger-ui-wrap blockquote:after,.swagger-section .swagger-ui-wrap blockquote:before,.swagger-section .swagger-ui-wrap q:after,.swagger-section .swagger-ui-wrap q:before{content:none}.swagger-section .swagger-ui-wrap .heading_with_menu h1,.swagger-section .swagger-ui-wrap .heading_with_menu h2,.swagger-section .swagger-ui-wrap .heading_with_menu h3,.swagger-section .swagger-ui-wrap .heading_with_menu h4,.swagger-section .swagger-ui-wrap .heading_with_menu h5,.swagger-section .swagger-ui-wrap .heading_with_menu h6{display:block;clear:none;float:left;-ms-box-sizing:border-box;box-sizing:border-box;width:60%}.swagger-section .swagger-ui-wrap table{border-collapse:collapse;border-spacing:0}.swagger-section .swagger-ui-wrap table thead tr th{padding:5px;font-size:.9em;color:#666;border-bottom:1px solid #999}.swagger-section .swagger-ui-wrap table tbody tr:last-child td{border-bottom:none}.swagger-section .swagger-ui-wrap table tbody tr.offset{background-color:#f0f0f0}.swagger-section .swagger-ui-wrap table tbody tr td{padding:6px;font-size:.9em;border-bottom:1px solid #ccc;vertical-align:top;line-height:1.3em}.swagger-section .swagger-ui-wrap ol{margin:0 0 10px;padding:0 0 0 18px;list-style-type:decimal}.swagger-section .swagger-ui-wrap ol li{padding:5px 0;font-size:.9em;color:#333}.swagger-section .swagger-ui-wrap ol,.swagger-section .swagger-ui-wrap ul{list-style:none}.swagger-section .swagger-ui-wrap h1 a,.swagger-section .swagger-ui-wrap h2 a,.swagger-section .swagger-ui-wrap h3 a,.swagger-section .swagger-ui-wrap h4 a,.swagger-section .swagger-ui-wrap h5 a,.swagger-section .swagger-ui-wrap h6 a{text-decoration:none}.swagger-section .swagger-ui-wrap h1 a:hover,.swagger-section .swagger-ui-wrap h2 a:hover,.swagger-section .swagger-ui-wrap h3 a:hover,.swagger-section .swagger-ui-wrap h4 a:hover,.swagger-section .swagger-ui-wrap h5 a:hover,.swagger-section .swagger-ui-wrap h6 a:hover{text-decoration:underline}.swagger-section .swagger-ui-wrap h1 span.divider,.swagger-section .swagger-ui-wrap h2 span.divider,.swagger-section .swagger-ui-wrap h3 span.divider,.swagger-section .swagger-ui-wrap h4 span.divider,.swagger-section .swagger-ui-wrap h5 span.divider,.swagger-section .swagger-ui-wrap h6 span.divider{color:#aaa}.swagger-section .swagger-ui-wrap a{color:#547f00}.swagger-section .swagger-ui-wrap a img{border:none}.swagger-section .swagger-ui-wrap article,.swagger-section .swagger-ui-wrap aside,.swagger-section .swagger-ui-wrap details,.swagger-section .swagger-ui-wrap figcaption,.swagger-section .swagger-ui-wrap figure,.swagger-section .swagger-ui-wrap footer,.swagger-section .swagger-ui-wrap header,.swagger-section .swagger-ui-wrap hgroup,.swagger-section .swagger-ui-wrap menu,.swagger-section .swagger-ui-wrap nav,.swagger-section .swagger-ui-wrap section,.swagger-section .swagger-ui-wrap summary{display:block}.swagger-section .swagger-ui-wrap pre{font-family:Anonymous Pro,Menlo,Consolas,Bitstream Vera Sans Mono,Courier New,monospace;background-color:#fcf6db;border:1px solid #e5e0c6;padding:10px}.swagger-section .swagger-ui-wrap pre code{line-height:1.6em;background:none}.swagger-section .swagger-ui-wrap .content>.content-type>div>label{clear:both;display:block;color:#0f6ab4;font-size:1.1em;margin:0;padding:15px 0 5px}.swagger-section .swagger-ui-wrap .content pre{font-size:12px;margin-top:5px;padding:5px}.swagger-section .swagger-ui-wrap .icon-btn{cursor:pointer}.swagger-section .swagger-ui-wrap .info_title{padding-bottom:10px;font-weight:700;font-size:25px}.swagger-section .swagger-ui-wrap .footer{margin-top:20px}.swagger-section .swagger-ui-wrap div.big p,.swagger-section .swagger-ui-wrap p.big{font-size:1em;margin-bottom:10px}.swagger-section .swagger-ui-wrap form.fullwidth ol li.numeric input,.swagger-section .swagger-ui-wrap form.fullwidth ol li.string input,.swagger-section .swagger-ui-wrap form.fullwidth ol li.text textarea,.swagger-section .swagger-ui-wrap form.fullwidth ol li.url input{width:500px!important}.swagger-section .swagger-ui-wrap .info_license,.swagger-section .swagger-ui-wrap .info_tos{padding-bottom:5px}.swagger-section .swagger-ui-wrap .message-fail{color:#c00}.swagger-section .swagger-ui-wrap .info_email,.swagger-section .swagger-ui-wrap .info_name,.swagger-section .swagger-ui-wrap .info_url{padding-bottom:5px}.swagger-section .swagger-ui-wrap .info_description{padding-bottom:10px;font-size:15px}.swagger-section .swagger-ui-wrap .markdown ol li,.swagger-section .swagger-ui-wrap .markdown ul li{padding:3px 0;line-height:1.4em;color:#333}.swagger-section .swagger-ui-wrap form.formtastic fieldset.inputs ol li.numeric input,.swagger-section .swagger-ui-wrap form.formtastic fieldset.inputs ol li.string input,.swagger-section .swagger-ui-wrap form.formtastic fieldset.inputs ol li.url input{display:block;padding:4px;width:auto;clear:both}.swagger-section .swagger-ui-wrap form.formtastic fieldset.inputs ol li.numeric input.title,.swagger-section .swagger-ui-wrap form.formtastic fieldset.inputs ol li.string input.title,.swagger-section .swagger-ui-wrap form.formtastic fieldset.inputs ol li.url input.title{font-size:1.3em}.swagger-section .swagger-ui-wrap table.fullwidth{width:100%}.swagger-section .swagger-ui-wrap .model-signature{font-family:Droid Sans,sans-serif;font-size:1em;line-height:1.5em}.swagger-section .swagger-ui-wrap .model-signature .signature-nav a{text-decoration:none;color:#aaa}.swagger-section .swagger-ui-wrap .model-signature .signature-nav a:hover{text-decoration:underline;color:#000}.swagger-section .swagger-ui-wrap .model-signature .signature-nav .selected{color:#000;text-decoration:none}.swagger-section .swagger-ui-wrap .model-signature .propType{color:#55a}.swagger-section .swagger-ui-wrap .model-signature pre:hover{background-color:#ffd}.swagger-section .swagger-ui-wrap .model-signature pre{font-size:.85em;line-height:1.2em;overflow:auto;max-height:200px;cursor:pointer}.swagger-section .swagger-ui-wrap .model-signature ul.signature-nav{display:block;min-width:230px;margin:0;padding:0}.swagger-section .swagger-ui-wrap .model-signature ul.signature-nav li:last-child{padding-right:0;border-right:none}.swagger-section .swagger-ui-wrap .model-signature ul.signature-nav li{float:left;margin:0 5px 5px 0;padding:2px 5px 2px 0;border-right:1px solid #ddd}.swagger-section .swagger-ui-wrap .model-signature .propOpt{color:#555}.swagger-section .swagger-ui-wrap .model-signature .snippet small{font-size:.75em}.swagger-section .swagger-ui-wrap .model-signature .propOptKey{font-style:italic}.swagger-section .swagger-ui-wrap .model-signature .description .strong{font-weight:700;color:#000;font-size:.9em}.swagger-section .swagger-ui-wrap .model-signature .description div{font-size:.9em;line-height:1.5em;margin-left:1em}.swagger-section .swagger-ui-wrap .model-signature .description .stronger{font-weight:700;color:#000}.swagger-section .swagger-ui-wrap .model-signature .description .propWrap .optionsWrapper{border-spacing:0;position:absolute;background-color:#fff;border:1px solid #bbb;display:none;font-size:11px;max-width:400px;line-height:30px;color:#000;padding:5px;margin-left:10px}.swagger-section .swagger-ui-wrap .model-signature .description .propWrap .optionsWrapper th{text-align:center;background-color:#eee;border:1px solid #bbb;font-size:11px;color:#666;font-weight:700;padding:5px;line-height:15px}.swagger-section .swagger-ui-wrap .model-signature .description .propWrap .optionsWrapper .optionName{font-weight:700}.swagger-section .swagger-ui-wrap .model-signature .description .propDesc.markdown>p:first-child,.swagger-section .swagger-ui-wrap .model-signature .description .propDesc.markdown>p:last-child{display:inline}.swagger-section .swagger-ui-wrap .model-signature .description .propDesc.markdown>p:not(:first-child):before{display:block;content:''}.swagger-section .swagger-ui-wrap .model-signature .description span:last-of-type.propDesc.markdown>p:only-child{margin-right:-3px}.swagger-section .swagger-ui-wrap .model-signature .propName{font-weight:700}.swagger-section .swagger-ui-wrap .model-signature .signature-container{clear:both}.swagger-section .swagger-ui-wrap .body-textarea{width:300px;height:100px;border:1px solid #aaa}.swagger-section .swagger-ui-wrap .markdown li code,.swagger-section .swagger-ui-wrap .markdown p code{font-family:Anonymous Pro,Menlo,Consolas,Bitstream Vera Sans Mono,Courier New,monospace;background-color:#f0f0f0;color:#000;padding:1px 3px}.swagger-section .swagger-ui-wrap .required{font-weight:700}.swagger-section .swagger-ui-wrap .editor_holder{font-family:Anonymous Pro,Menlo,Consolas,Bitstream Vera Sans Mono,Courier New,monospace;font-size:.9em}.swagger-section .swagger-ui-wrap .editor_holder label{font-weight:400!important}.swagger-section .swagger-ui-wrap .editor_holder label.required{font-weight:700!important}.swagger-section .swagger-ui-wrap input.parameter{width:300px;border:1px solid #aaa}.swagger-section .swagger-ui-wrap h1{color:#000;font-size:1.5em;line-height:1.3em;padding:10px 0;font-family:Droid Sans,sans-serif;font-weight:700}.swagger-section .swagger-ui-wrap .heading_with_menu{float:none;clear:both;overflow:hidden;display:block}.swagger-section .swagger-ui-wrap .heading_with_menu ul{display:block;clear:none;float:right;-ms-box-sizing:border-box;box-sizing:border-box;margin-top:10px}.swagger-section .swagger-ui-wrap h2{color:#000;font-size:1.3em;padding:10px 0}.swagger-section .swagger-ui-wrap h2 a{color:#000}.swagger-section .swagger-ui-wrap h2 span.sub{font-size:.7em;color:#999;font-style:italic}.swagger-section .swagger-ui-wrap h2 span.sub a{color:#777}.swagger-section .swagger-ui-wrap span.weak{color:#666}.swagger-section .swagger-ui-wrap .message-success{color:#89bf04}.swagger-section .swagger-ui-wrap caption,.swagger-section .swagger-ui-wrap td,.swagger-section .swagger-ui-wrap th{text-align:left;font-weight:400;vertical-align:middle}.swagger-section .swagger-ui-wrap .code{font-family:Anonymous Pro,Menlo,Consolas,Bitstream Vera Sans Mono,Courier New,monospace}.swagger-section .swagger-ui-wrap form.formtastic fieldset.inputs ol li.text textarea{font-family:Droid Sans,sans-serif;height:250px;padding:4px;display:block;clear:both}.swagger-section .swagger-ui-wrap form.formtastic fieldset.inputs ol li.select select{display:block;clear:both}.swagger-section .swagger-ui-wrap form.formtastic fieldset.inputs ol li.boolean{float:none;clear:both;overflow:hidden;display:block}.swagger-section .swagger-ui-wrap form.formtastic fieldset.inputs ol li.boolean label{display:block;float:left;clear:none;margin:0;padding:0}.swagger-section .swagger-ui-wrap form.formtastic fieldset.inputs ol li.boolean input{display:block;float:left;clear:none;margin:0 5px 0 0}.swagger-section .swagger-ui-wrap form.formtastic fieldset.inputs ol li.required label{color:#000}.swagger-section .swagger-ui-wrap form.formtastic fieldset.inputs ol li label{display:block;clear:both;width:auto;padding:0 0 3px;color:#666}.swagger-section .swagger-ui-wrap form.formtastic fieldset.inputs ol li label abbr{padding-left:3px;color:#888}.swagger-section .swagger-ui-wrap form.formtastic fieldset.inputs ol li p.inline-hints{margin-left:0;font-style:italic;font-size:.9em;margin:0}.swagger-section .swagger-ui-wrap form.formtastic fieldset.buttons{margin:0;padding:0}.swagger-section .swagger-ui-wrap span.blank,.swagger-section .swagger-ui-wrap span.empty{color:#888;font-style:italic}.swagger-section .swagger-ui-wrap .markdown h3{color:#547f00}.swagger-section .swagger-ui-wrap .markdown h4{color:#666}.swagger-section .swagger-ui-wrap .markdown pre{font-family:Anonymous Pro,Menlo,Consolas,Bitstream Vera Sans Mono,Courier New,monospace;background-color:#fcf6db;border:1px solid #e5e0c6;padding:10px;margin:0 0 10px}.swagger-section .swagger-ui-wrap .markdown pre code{line-height:1.6em;overflow:auto}.swagger-section .swagger-ui-wrap div.gist{margin:20px 0 25px!important}.swagger-section .swagger-ui-wrap ul#resources{font-family:Droid Sans,sans-serif;font-size:.9em}.swagger-section .swagger-ui-wrap ul#resources li.resource{border-bottom:1px solid #ddd}.swagger-section .swagger-ui-wrap ul#resources li.resource.active div.heading h2 a,.swagger-section .swagger-ui-wrap ul#resources li.resource:hover div.heading h2 a{color:#000}.swagger-section .swagger-ui-wrap ul#resources li.resource.active div.heading ul.options li a,.swagger-section .swagger-ui-wrap ul#resources li.resource:hover div.heading ul.options li a{color:#555}.swagger-section .swagger-ui-wrap ul#resources li.resource:last-child{border-bottom:none}.swagger-section .swagger-ui-wrap ul#resources li.resource div.heading{border:1px solid transparent;float:none;clear:both;overflow:hidden;display:block}.swagger-section .swagger-ui-wrap ul#resources li.resource div.heading ul.options{overflow:hidden;padding:0;display:block;clear:none;float:right;margin:14px 10px 0 0}.swagger-section .swagger-ui-wrap ul#resources li.resource div.heading ul.options li{float:left;clear:none;margin:0;padding:2px 10px;border-right:1px solid #ddd;color:#666;font-size:.9em}.swagger-section .swagger-ui-wrap ul#resources li.resource div.heading ul.options li a{color:#aaa;text-decoration:none}.swagger-section .swagger-ui-wrap ul#resources li.resource div.heading ul.options li a:hover{text-decoration:underline;color:#000}.swagger-section .swagger-ui-wrap ul#resources li.resource div.heading ul.options li a.active,.swagger-section .swagger-ui-wrap ul#resources li.resource div.heading ul.options li a:active,.swagger-section .swagger-ui-wrap ul#resources li.resource div.heading ul.options li a:hover{text-decoration:underline}.swagger-section .swagger-ui-wrap ul#resources li.resource div.heading ul.options li.first,.swagger-section .swagger-ui-wrap ul#resources li.resource div.heading ul.options li:first-child{padding-left:0}.swagger-section .swagger-ui-wrap ul#resources li.resource div.heading ul.options li.last,.swagger-section .swagger-ui-wrap ul#resources li.resource div.heading ul.options li:last-child{padding-right:0;border-right:none}.swagger-section .swagger-ui-wrap ul#resources li.resource div.heading ul.options.first,.swagger-section .swagger-ui-wrap ul#resources li.resource div.heading ul.options:first-child{padding-left:0}.swagger-section .swagger-ui-wrap ul#resources li.resource div.heading h2{color:#999;padding-left:0;display:block;clear:none;float:left;font-family:Droid Sans,sans-serif;font-weight:700}.swagger-section .swagger-ui-wrap ul#resources li.resource div.heading h2 a{color:#999}.swagger-section .swagger-ui-wrap ul#resources li.resource div.heading h2 a:hover{color:#000}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation{float:none;clear:both;overflow:hidden;display:block;margin:0 0 10px;padding:0}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation div.heading{float:none;clear:both;overflow:hidden;display:block;margin:0;padding:0}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation div.heading h3{display:block;clear:none;float:left;width:auto;margin:0;padding:0;line-height:1.1em;color:#000}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation div.heading h3 span.path{padding-left:10px}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation div.heading h3 span.path a{color:#000;text-decoration:none}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation div.heading h3 span.path a.toggleOperation.deprecated{text-decoration:line-through}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation div.heading h3 span.path a:hover{text-decoration:underline}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation div.heading h3 span.http_method a{text-transform:uppercase;text-decoration:none;color:#fff;display:inline-block;width:50px;font-size:.7em;text-align:center;padding:7px 0 4px;border-radius:2px}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation div.heading h3 span{margin:0;padding:0}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation div.heading ul.options{overflow:hidden;padding:0;display:block;clear:none;float:right;margin:6px 10px 0 0}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation div.heading ul.options li{float:left;clear:none;margin:0;padding:2px 10px;font-size:.9em}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation div.heading ul.options li a{text-decoration:none}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation div.heading ul.options li a .markdown p{color:inherit;padding:0;line-height:inherit}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation div.heading ul.options li.access{color:#000}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation div.content{border-top:none;padding:10px;border-bottom-left-radius:6px;border-bottom-right-radius:6px;margin:0 0 20px}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation div.content h4{font-size:1.1em;margin:0;padding:15px 0 5px}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation div.content div.sandbox_header{float:none;clear:both;overflow:hidden;display:block}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation div.content div.sandbox_header a{padding:4px 0 0 10px;display:inline-block;font-size:.9em}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation div.content div.sandbox_header input.submit{display:block;clear:none;float:left;padding:6px 8px}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation div.content div.sandbox_header span.response_throbber{background-image:url(../images/throbber.gif);width:128px;height:16px;display:block;clear:none;float:right}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation div.content form input[type=text].error{outline:2px solid #000;outline-color:#c00}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation div.content form select[name=parameterContentType]{max-width:300px}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation div.content div.response div.block pre{font-family:Anonymous Pro,Menlo,Consolas,Bitstream Vera Sans Mono,Courier New,monospace;padding:10px;font-size:.9em;max-height:400px;overflow-y:auto}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.heading{background-color:#f9f2e9;border:1px solid #f0e0ca}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.heading h3 span.http_method a{background-color:#c5862b}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.heading ul.options li{border-right:1px solid #ddd;border-right-color:#f0e0ca;color:#c5862b}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.heading ul.options li a{color:#c5862b}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.content{background-color:#faf5ee;border:1px solid #f0e0ca}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.content h4{color:#c5862b}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.content div.sandbox_header a{color:#dcb67f}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.head div.heading{background-color:#fcffcd;border:1px solid #000;border-color:#ffd20f}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.head div.heading h3 span.http_method a{text-transform:uppercase;background-color:#ffd20f}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.head div.heading ul.options li{border-right:1px solid #ddd;border-right-color:#ffd20f;color:#ffd20f}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.head div.heading ul.options li a{color:#ffd20f}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.head div.content{background-color:#fcffcd;border:1px solid #000;border-color:#ffd20f}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.head div.content h4{color:#ffd20f}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.head div.content div.sandbox_header a{color:#6fc992}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.heading{background-color:#f5e8e8;border:1px solid #e8c6c7}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.heading h3 span.http_method a{text-transform:uppercase;background-color:#a41e22}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.heading ul.options li{border-right:1px solid #ddd;border-right-color:#e8c6c7;color:#a41e22}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.heading ul.options li a{color:#a41e22}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.content{background-color:#f7eded;border:1px solid #e8c6c7}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.content h4{color:#a41e22}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.content div.sandbox_header a{color:#c8787a}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.heading{background-color:#e7f6ec;border:1px solid #c3e8d1}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.heading h3 span.http_method a{background-color:#10a54a}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.heading ul.options li{border-right:1px solid #ddd;border-right-color:#c3e8d1;color:#10a54a}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.heading ul.options li a{color:#10a54a}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.content{background-color:#ebf7f0;border:1px solid #c3e8d1}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.content h4{color:#10a54a}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.content div.sandbox_header a{color:#6fc992}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.patch div.heading{background-color:#fce9e3;border:1px solid #f5d5c3}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.patch div.heading h3 span.http_method a{background-color:#d38042}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.patch div.heading ul.options li{border-right:1px solid #ddd;border-right-color:#f0cecb;color:#d38042}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.patch div.heading ul.options li a{color:#d38042}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.patch div.content{background-color:#faf0ef;border:1px solid #f0cecb}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.patch div.content h4{color:#d38042}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.patch div.content div.sandbox_header a{color:#dcb67f}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.heading{background-color:#e7f0f7;border:1px solid #c3d9ec}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.heading h3 span.http_method a{background-color:#0f6ab4}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.heading ul.options li{border-right:1px solid #ddd;border-right-color:#c3d9ec;color:#0f6ab4}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.heading ul.options li a{color:#0f6ab4}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.content{background-color:#ebf3f9;border:1px solid #c3d9ec}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.content h4{color:#0f6ab4}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.content div.sandbox_header a{color:#6fa5d2}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.options div.heading{background-color:#e7f0f7;border:1px solid #c3d9ec}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.options div.heading h3 span.http_method a{background-color:#0f6ab4}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.options div.heading ul.options li{border-right:1px solid #ddd;border-right-color:#c3d9ec;color:#0f6ab4}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.options div.heading ul.options li a{color:#0f6ab4}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.options div.content{background-color:#ebf3f9;border:1px solid #c3d9ec}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.options div.content h4{color:#0f6ab4}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.options div.content div.sandbox_header a{color:#6fa5d2}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.content,.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.content,.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.head div.content,.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.patch div.content,.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.content,.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.content{border-top:none}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.heading ul.options li.last,.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.heading ul.options li:last-child,.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.heading ul.options li.last,.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.heading ul.options li:last-child,.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.head div.heading ul.options li.last,.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.head div.heading ul.options li:last-child,.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.patch div.heading ul.options li.last,.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.patch div.heading ul.options li:last-child,.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.heading ul.options li.last,.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.heading ul.options li:last-child,.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.heading ul.options li.last,.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.heading ul.options li:last-child{padding-right:0;border-right:none}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations ul.options li a.active,.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations ul.options li a:active,.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations ul.options li a:hover{text-decoration:underline}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations.first,.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations:first-child,.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations ul.options li.first,.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations ul.options li:first-child{padding-left:0}.swagger-section .swagger-ui-wrap p#colophon{margin:0 15px 40px;padding:10px 0;font-size:.8em;border-top:1px solid #ddd;font-family:Droid Sans,sans-serif;color:#999;font-style:italic}.swagger-section .swagger-ui-wrap p#colophon a{text-decoration:none;color:#547f00}.swagger-section .swagger-ui-wrap h3{color:#000;font-size:1.1em;padding:10px 0}.swagger-section .swagger-ui-wrap .markdown ol,.swagger-section .swagger-ui-wrap .markdown ul{font-family:Droid Sans,sans-serif;margin:5px 0 10px;padding:0 0 0 18px;list-style-type:disc}.swagger-section .swagger-ui-wrap form.form_box{background-color:#ebf3f9;border:1px solid #c3d9ec;padding:10px}.swagger-section .swagger-ui-wrap form.form_box label{color:#0f6ab4!important}.swagger-section .swagger-ui-wrap form.form_box input[type=submit]{display:block;padding:10px}.swagger-section .swagger-ui-wrap form.form_box p.weak{font-size:.8em}.swagger-section .swagger-ui-wrap form.form_box p{font-size:.9em;padding:0 0 15px;color:#7e7b6d}.swagger-section .swagger-ui-wrap form.form_box p a{color:#646257}.swagger-section .swagger-ui-wrap form.form_box p strong{color:#000}.swagger-section .swagger-ui-wrap .operation-status td.markdown>p:last-child{padding-bottom:0}.swagger-section .title{font-style:bold}.swagger-section .secondary_form{display:none}.swagger-section .main_image{display:block;margin-left:auto;margin-right:auto}.swagger-section .oauth_body{margin-left:100px;margin-right:100px}.swagger-section .oauth_submit{text-align:center;display:inline-block}.swagger-section .authorize-wrapper{margin:15px 0 10px}.swagger-section .authorize-wrapper_operation{float:right}.swagger-section .authorize__btn:hover{text-decoration:underline;cursor:pointer}.swagger-section .authorize__btn_operation:hover .authorize-scopes{display:block}.swagger-section .authorize-scopes{position:absolute;margin-top:20px;background:#fff;border:1px solid #ccc;border-radius:5px;display:none;font-size:13px;max-width:300px;line-height:30px;color:#000;padding:5px}.swagger-section .authorize-scopes .authorize__scope{text-decoration:none}.swagger-section .authorize__btn_operation{height:18px;vertical-align:middle;display:inline-block;background:url(../images/explorer_icons.png) no-repeat}.swagger-section .authorize__btn_operation_login{background-position:0 0;width:18px;margin-top:-6px;margin-left:4px}.swagger-section .authorize__btn_operation_logout{background-position:-30px 0;width:18px;margin-top:-6px;margin-left:4px}.swagger-section #auth_container{color:#fff;display:inline-block;border:none;padding:5px;width:87px;height:13px}.swagger-section #auth_container .authorize__btn{color:#fff}.swagger-section .auth_container{padding:0 0 10px;margin-bottom:5px;border-bottom:1px solid #ccc;font-size:.9em}.swagger-section .auth_container .auth__title{color:#547f00;font-size:1.2em}.swagger-section .auth_container .basic_auth__label{display:inline-block;width:60px}.swagger-section .auth_container .auth__description{color:#999;margin-bottom:5px}.swagger-section .auth_container .auth__button{margin-top:10px;height:30px}.swagger-section .auth_container .key_auth__field{margin:5px 0}.swagger-section .auth_container .key_auth__label{display:inline-block;width:60px}.swagger-section .api-popup-dialog{position:absolute;display:none}.swagger-section .api-popup-dialog-wrapper{z-index:2;width:500px;background:#fff;padding:20px;border:1px solid #ccc;border-radius:5px;font-size:13px;color:#777;position:fixed;top:50%;left:50%;transform:translate(-50%,-50%)}.swagger-section .api-popup-dialog-shadow{position:fixed;top:0;left:0;width:100%;height:100%;opacity:.2;background-color:gray;z-index:1}.swagger-section .api-popup-dialog .api-popup-title{font-size:24px;padding:10px 0}.swagger-section .api-popup-dialog .error-msg{padding-left:5px;padding-bottom:5px}.swagger-section .api-popup-dialog .api-popup-content{max-height:500px;overflow-y:auto}.swagger-section .api-popup-dialog .api-popup-authbtn,.swagger-section .api-popup-dialog .api-popup-cancel{height:30px}.swagger-section .api-popup-scopes{padding:10px 20px}.swagger-section .api-popup-scopes li{padding:5px 0;line-height:20px}.swagger-section .api-popup-scopes li input{position:relative;top:2px}.swagger-section .api-popup-scopes .api-scope-desc{padding-left:20px;font-style:italic}.swagger-section .api-popup-actions{padding-top:10px}#header{display:none}.swagger-section .swagger-ui-wrap .model-signature pre{max-height:none}.swagger-section .swagger-ui-wrap .body-textarea,.swagger-section .swagger-ui-wrap input.parameter{width:100px}.swagger-section .swagger-ui-wrap ul#resources li.resource div.heading ul.options{display:none}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints,.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation div.content{display:block!important} \ No newline at end of file diff --git a/static/docs/css/reset.css b/static/docs/css/reset.css new file mode 100644 index 0000000..40dc830 --- /dev/null +++ b/static/docs/css/reset.css @@ -0,0 +1 @@ +a,abbr,acronym,address,applet,article,aside,audio,b,big,blockquote,body,canvas,caption,center,cite,code,dd,del,details,dfn,div,dl,dt,em,embed,fieldset,figcaption,figure,footer,form,h1,h2,h3,h4,h5,h6,header,hgroup,html,i,iframe,img,ins,kbd,label,legend,li,mark,menu,nav,object,ol,output,p,pre,q,ruby,s,samp,section,small,span,strike,strong,sub,summary,sup,table,tbody,td,tfoot,th,thead,time,tr,tt,u,ul,var,video{margin:0;padding:0;border:0;font-size:100%;font:inherit;vertical-align:baseline}article,aside,details,figcaption,figure,footer,header,hgroup,menu,nav,section{display:block}body{line-height:1}ol,ul{list-style:none}blockquote,q{quotes:none}blockquote:after,blockquote:before,q:after,q:before{content:'';content:none}table{border-collapse:collapse;border-spacing:0} \ No newline at end of file diff --git a/static/docs/css/screen.css b/static/docs/css/screen.css new file mode 100644 index 0000000..8c2cd5b --- /dev/null +++ b/static/docs/css/screen.css @@ -0,0 +1 @@ +.swagger-section pre code{display:block;padding:.5em;background:#f0f0f0}.swagger-section pre .clojure .built_in,.swagger-section pre .lisp .title,.swagger-section pre .nginx .title,.swagger-section pre .subst,.swagger-section pre .tag .title,.swagger-section pre code{color:#000}.swagger-section pre .addition,.swagger-section pre .aggregate,.swagger-section pre .apache .cbracket,.swagger-section pre .apache .tag,.swagger-section pre .bash .variable,.swagger-section pre .constant,.swagger-section pre .django .variable,.swagger-section pre .erlang_repl .function_or_atom,.swagger-section pre .flow,.swagger-section pre .markdown .header,.swagger-section pre .parent,.swagger-section pre .preprocessor,.swagger-section pre .ruby .symbol,.swagger-section pre .ruby .symbol .string,.swagger-section pre .rules .value,.swagger-section pre .rules .value .number,.swagger-section pre .smalltalk .class,.swagger-section pre .stream,.swagger-section pre .string,.swagger-section pre .tag .value,.swagger-section pre .template_tag,.swagger-section pre .tex .command,.swagger-section pre .tex .special,.swagger-section pre .title{color:#800}.swagger-section pre .annotation,.swagger-section pre .chunk,.swagger-section pre .comment,.swagger-section pre .diff .header,.swagger-section pre .markdown .blockquote,.swagger-section pre .template_comment{color:#888}.swagger-section pre .change,.swagger-section pre .date,.swagger-section pre .go .constant,.swagger-section pre .literal,.swagger-section pre .markdown .bullet,.swagger-section pre .markdown .link_url,.swagger-section pre .number,.swagger-section pre .regexp,.swagger-section pre .smalltalk .char,.swagger-section pre .smalltalk .symbol{color:#080}.swagger-section pre .apache .sqbracket,.swagger-section pre .array,.swagger-section pre .attr_selector,.swagger-section pre .clojure .attribute,.swagger-section pre .coffeescript .property,.swagger-section pre .decorator,.swagger-section pre .deletion,.swagger-section pre .doctype,.swagger-section pre .envvar,.swagger-section pre .erlang_repl .reserved,.swagger-section pre .filter .argument,.swagger-section pre .important,.swagger-section pre .javadoc,.swagger-section pre .label,.swagger-section pre .localvars,.swagger-section pre .markdown .link_label,.swagger-section pre .nginx .built_in,.swagger-section pre .pi,.swagger-section pre .prompt,.swagger-section pre .pseudo,.swagger-section pre .ruby .string,.swagger-section pre .shebang,.swagger-section pre .tex .formula,.swagger-section pre .vhdl .attribute{color:#88f}.swagger-section pre .aggregate,.swagger-section pre .apache .tag,.swagger-section pre .bash .variable,.swagger-section pre .built_in,.swagger-section pre .css .tag,.swagger-section pre .go .typename,.swagger-section pre .id,.swagger-section pre .javadoctag,.swagger-section pre .keyword,.swagger-section pre .markdown .strong,.swagger-section pre .phpdoc,.swagger-section pre .request,.swagger-section pre .smalltalk .class,.swagger-section pre .status,.swagger-section pre .tex .command,.swagger-section pre .title,.swagger-section pre .winutils,.swagger-section pre .yardoctag{font-weight:700}.swagger-section pre .markdown .emphasis{font-style:italic}.swagger-section pre .nginx .built_in{font-weight:400}.swagger-section pre .coffeescript .javascript,.swagger-section pre .javascript .xml,.swagger-section pre .tex .formula,.swagger-section pre .xml .cdata,.swagger-section pre .xml .css,.swagger-section pre .xml .javascript,.swagger-section pre .xml .vbscript{opacity:.5}.swagger-section .hljs{display:block;overflow-x:auto;padding:.5em;background:#f0f0f0}.swagger-section .hljs,.swagger-section .hljs-subst{color:#444}.swagger-section .hljs-attribute,.swagger-section .hljs-doctag,.swagger-section .hljs-keyword,.swagger-section .hljs-meta-keyword,.swagger-section .hljs-name,.swagger-section .hljs-selector-tag{font-weight:700}.swagger-section .hljs-addition,.swagger-section .hljs-built_in,.swagger-section .hljs-bullet,.swagger-section .hljs-code,.swagger-section .hljs-literal{color:#1f811f}.swagger-section .hljs-link,.swagger-section .hljs-regexp,.swagger-section .hljs-selector-attr,.swagger-section .hljs-selector-pseudo,.swagger-section .hljs-symbol,.swagger-section .hljs-template-variable,.swagger-section .hljs-variable{color:#bc6060}.swagger-section .hljs-deletion,.swagger-section .hljs-number,.swagger-section .hljs-quote,.swagger-section .hljs-selector-class,.swagger-section .hljs-selector-id,.swagger-section .hljs-string,.swagger-section .hljs-template-tag,.swagger-section .hljs-type{color:#800}.swagger-section .hljs-section,.swagger-section .hljs-title{color:#800;font-weight:700}.swagger-section .hljs-comment{color:#888}.swagger-section .hljs-meta{color:#2b6ea1}.swagger-section .hljs-emphasis{font-style:italic}.swagger-section .hljs-strong{font-weight:700}.swagger-section .swagger-ui-wrap{line-height:1;font-family:Droid Sans,sans-serif;min-width:760px;max-width:960px;margin-left:auto;margin-right:auto}.swagger-section .swagger-ui-wrap b,.swagger-section .swagger-ui-wrap strong{font-family:Droid Sans,sans-serif;font-weight:700}.swagger-section .swagger-ui-wrap blockquote,.swagger-section .swagger-ui-wrap q{quotes:none}.swagger-section .swagger-ui-wrap p{line-height:1.4em;padding:0 0 10px;color:#333}.swagger-section .swagger-ui-wrap blockquote:after,.swagger-section .swagger-ui-wrap blockquote:before,.swagger-section .swagger-ui-wrap q:after,.swagger-section .swagger-ui-wrap q:before{content:none}.swagger-section .swagger-ui-wrap .heading_with_menu h1,.swagger-section .swagger-ui-wrap .heading_with_menu h2,.swagger-section .swagger-ui-wrap .heading_with_menu h3,.swagger-section .swagger-ui-wrap .heading_with_menu h4,.swagger-section .swagger-ui-wrap .heading_with_menu h5,.swagger-section .swagger-ui-wrap .heading_with_menu h6{display:block;clear:none;float:left;-ms-box-sizing:border-box;box-sizing:border-box;width:60%}.swagger-section .swagger-ui-wrap table{border-collapse:collapse;border-spacing:0}.swagger-section .swagger-ui-wrap table thead tr th{padding:5px;font-size:.9em;color:#666;border-bottom:1px solid #999}.swagger-section .swagger-ui-wrap table tbody tr:last-child td{border-bottom:none}.swagger-section .swagger-ui-wrap table tbody tr.offset{background-color:#f0f0f0}.swagger-section .swagger-ui-wrap table tbody tr td{padding:6px;font-size:.9em;border-bottom:1px solid #ccc;vertical-align:top;line-height:1.3em}.swagger-section .swagger-ui-wrap ol{margin:0 0 10px;padding:0 0 0 18px;list-style-type:decimal}.swagger-section .swagger-ui-wrap ol li{padding:5px 0;font-size:.9em;color:#333}.swagger-section .swagger-ui-wrap ol,.swagger-section .swagger-ui-wrap ul{list-style:none}.swagger-section .swagger-ui-wrap h1 a,.swagger-section .swagger-ui-wrap h2 a,.swagger-section .swagger-ui-wrap h3 a,.swagger-section .swagger-ui-wrap h4 a,.swagger-section .swagger-ui-wrap h5 a,.swagger-section .swagger-ui-wrap h6 a{text-decoration:none}.swagger-section .swagger-ui-wrap h1 a:hover,.swagger-section .swagger-ui-wrap h2 a:hover,.swagger-section .swagger-ui-wrap h3 a:hover,.swagger-section .swagger-ui-wrap h4 a:hover,.swagger-section .swagger-ui-wrap h5 a:hover,.swagger-section .swagger-ui-wrap h6 a:hover{text-decoration:underline}.swagger-section .swagger-ui-wrap h1 span.divider,.swagger-section .swagger-ui-wrap h2 span.divider,.swagger-section .swagger-ui-wrap h3 span.divider,.swagger-section .swagger-ui-wrap h4 span.divider,.swagger-section .swagger-ui-wrap h5 span.divider,.swagger-section .swagger-ui-wrap h6 span.divider{color:#aaa}.swagger-section .swagger-ui-wrap a{color:#547f00}.swagger-section .swagger-ui-wrap a img{border:none}.swagger-section .swagger-ui-wrap article,.swagger-section .swagger-ui-wrap aside,.swagger-section .swagger-ui-wrap details,.swagger-section .swagger-ui-wrap figcaption,.swagger-section .swagger-ui-wrap figure,.swagger-section .swagger-ui-wrap footer,.swagger-section .swagger-ui-wrap header,.swagger-section .swagger-ui-wrap hgroup,.swagger-section .swagger-ui-wrap menu,.swagger-section .swagger-ui-wrap nav,.swagger-section .swagger-ui-wrap section,.swagger-section .swagger-ui-wrap summary{display:block}.swagger-section .swagger-ui-wrap pre{font-family:Anonymous Pro,Menlo,Consolas,Bitstream Vera Sans Mono,Courier New,monospace;background-color:#fcf6db;border:1px solid #e5e0c6;padding:10px}.swagger-section .swagger-ui-wrap pre code{line-height:1.6em;background:none}.swagger-section .swagger-ui-wrap .content>.content-type>div>label{clear:both;display:block;color:#0f6ab4;font-size:1.1em;margin:0;padding:15px 0 5px}.swagger-section .swagger-ui-wrap .content pre{font-size:12px;margin-top:5px;padding:5px}.swagger-section .swagger-ui-wrap .icon-btn{cursor:pointer}.swagger-section .swagger-ui-wrap .info_title{padding-bottom:10px;font-weight:700;font-size:25px}.swagger-section .swagger-ui-wrap .footer{margin-top:20px}.swagger-section .swagger-ui-wrap div.big p,.swagger-section .swagger-ui-wrap p.big{font-size:1em;margin-bottom:10px}.swagger-section .swagger-ui-wrap form.fullwidth ol li.numeric input,.swagger-section .swagger-ui-wrap form.fullwidth ol li.string input,.swagger-section .swagger-ui-wrap form.fullwidth ol li.text textarea,.swagger-section .swagger-ui-wrap form.fullwidth ol li.url input{width:500px!important}.swagger-section .swagger-ui-wrap .info_license,.swagger-section .swagger-ui-wrap .info_tos{padding-bottom:5px}.swagger-section .swagger-ui-wrap .message-fail{color:#c00}.swagger-section .swagger-ui-wrap .info_email,.swagger-section .swagger-ui-wrap .info_name,.swagger-section .swagger-ui-wrap .info_url{padding-bottom:5px}.swagger-section .swagger-ui-wrap .info_description{padding-bottom:10px;font-size:15px}.swagger-section .swagger-ui-wrap .markdown ol li,.swagger-section .swagger-ui-wrap .markdown ul li{padding:3px 0;line-height:1.4em;color:#333}.swagger-section .swagger-ui-wrap form.formtastic fieldset.inputs ol li.numeric input,.swagger-section .swagger-ui-wrap form.formtastic fieldset.inputs ol li.string input,.swagger-section .swagger-ui-wrap form.formtastic fieldset.inputs ol li.url input{display:block;padding:4px;width:auto;clear:both}.swagger-section .swagger-ui-wrap form.formtastic fieldset.inputs ol li.numeric input.title,.swagger-section .swagger-ui-wrap form.formtastic fieldset.inputs ol li.string input.title,.swagger-section .swagger-ui-wrap form.formtastic fieldset.inputs ol li.url input.title{font-size:1.3em}.swagger-section .swagger-ui-wrap table.fullwidth{width:100%}.swagger-section .swagger-ui-wrap .model-signature{font-family:Droid Sans,sans-serif;font-size:1em;line-height:1.5em}.swagger-section .swagger-ui-wrap .model-signature .signature-nav a{text-decoration:none;color:#aaa}.swagger-section .swagger-ui-wrap .model-signature .signature-nav a:hover{text-decoration:underline;color:#000}.swagger-section .swagger-ui-wrap .model-signature .signature-nav .selected{color:#000;text-decoration:none}.swagger-section .swagger-ui-wrap .model-signature .propType{color:#55a}.swagger-section .swagger-ui-wrap .model-signature pre:hover{background-color:#ffd}.swagger-section .swagger-ui-wrap .model-signature pre{font-size:.85em;line-height:1.2em;overflow:auto;max-height:200px;cursor:pointer}.swagger-section .swagger-ui-wrap .model-signature ul.signature-nav{display:block;min-width:230px;margin:0;padding:0}.swagger-section .swagger-ui-wrap .model-signature ul.signature-nav li:last-child{padding-right:0;border-right:none}.swagger-section .swagger-ui-wrap .model-signature ul.signature-nav li{float:left;margin:0 5px 5px 0;padding:2px 5px 2px 0;border-right:1px solid #ddd}.swagger-section .swagger-ui-wrap .model-signature .propOpt{color:#555}.swagger-section .swagger-ui-wrap .model-signature .snippet small{font-size:.75em}.swagger-section .swagger-ui-wrap .model-signature .propOptKey{font-style:italic}.swagger-section .swagger-ui-wrap .model-signature .description .strong{font-weight:700;color:#000;font-size:.9em}.swagger-section .swagger-ui-wrap .model-signature .description div{font-size:.9em;line-height:1.5em;margin-left:1em}.swagger-section .swagger-ui-wrap .model-signature .description .stronger{font-weight:700;color:#000}.swagger-section .swagger-ui-wrap .model-signature .description .propWrap .optionsWrapper{border-spacing:0;position:absolute;background-color:#fff;border:1px solid #bbb;display:none;font-size:11px;max-width:400px;line-height:30px;color:#000;padding:5px;margin-left:10px}.swagger-section .swagger-ui-wrap .model-signature .description .propWrap .optionsWrapper th{text-align:center;background-color:#eee;border:1px solid #bbb;font-size:11px;color:#666;font-weight:700;padding:5px;line-height:15px}.swagger-section .swagger-ui-wrap .model-signature .description .propWrap .optionsWrapper .optionName{font-weight:700}.swagger-section .swagger-ui-wrap .model-signature .description .propDesc.markdown>p:first-child,.swagger-section .swagger-ui-wrap .model-signature .description .propDesc.markdown>p:last-child{display:inline}.swagger-section .swagger-ui-wrap .model-signature .description .propDesc.markdown>p:not(:first-child):before{display:block;content:''}.swagger-section .swagger-ui-wrap .model-signature .description span:last-of-type.propDesc.markdown>p:only-child{margin-right:-3px}.swagger-section .swagger-ui-wrap .model-signature .propName{font-weight:700}.swagger-section .swagger-ui-wrap .model-signature .signature-container{clear:both}.swagger-section .swagger-ui-wrap .body-textarea{width:300px;height:100px;border:1px solid #aaa}.swagger-section .swagger-ui-wrap .markdown li code,.swagger-section .swagger-ui-wrap .markdown p code{font-family:Anonymous Pro,Menlo,Consolas,Bitstream Vera Sans Mono,Courier New,monospace;background-color:#f0f0f0;color:#000;padding:1px 3px}.swagger-section .swagger-ui-wrap .required{font-weight:700}.swagger-section .swagger-ui-wrap .editor_holder{font-family:Anonymous Pro,Menlo,Consolas,Bitstream Vera Sans Mono,Courier New,monospace;font-size:.9em}.swagger-section .swagger-ui-wrap .editor_holder label{font-weight:400!important}.swagger-section .swagger-ui-wrap .editor_holder label.required{font-weight:700!important}.swagger-section .swagger-ui-wrap input.parameter{width:300px;border:1px solid #aaa}.swagger-section .swagger-ui-wrap h1{color:#000;font-size:1.5em;line-height:1.3em;padding:10px 0;font-family:Droid Sans,sans-serif;font-weight:700}.swagger-section .swagger-ui-wrap .heading_with_menu{float:none;clear:both;overflow:hidden;display:block}.swagger-section .swagger-ui-wrap .heading_with_menu ul{display:block;clear:none;float:right;-ms-box-sizing:border-box;box-sizing:border-box;margin-top:10px}.swagger-section .swagger-ui-wrap h2{color:#000;font-size:1.3em;padding:10px 0}.swagger-section .swagger-ui-wrap h2 a{color:#000}.swagger-section .swagger-ui-wrap h2 span.sub{font-size:.7em;color:#999;font-style:italic}.swagger-section .swagger-ui-wrap h2 span.sub a{color:#777}.swagger-section .swagger-ui-wrap span.weak{color:#666}.swagger-section .swagger-ui-wrap .message-success{color:#89bf04}.swagger-section .swagger-ui-wrap caption,.swagger-section .swagger-ui-wrap td,.swagger-section .swagger-ui-wrap th{text-align:left;font-weight:400;vertical-align:middle}.swagger-section .swagger-ui-wrap .code{font-family:Anonymous Pro,Menlo,Consolas,Bitstream Vera Sans Mono,Courier New,monospace}.swagger-section .swagger-ui-wrap form.formtastic fieldset.inputs ol li.text textarea{font-family:Droid Sans,sans-serif;height:250px;padding:4px;display:block;clear:both}.swagger-section .swagger-ui-wrap form.formtastic fieldset.inputs ol li.select select{display:block;clear:both}.swagger-section .swagger-ui-wrap form.formtastic fieldset.inputs ol li.boolean{float:none;clear:both;overflow:hidden;display:block}.swagger-section .swagger-ui-wrap form.formtastic fieldset.inputs ol li.boolean label{display:block;float:left;clear:none;margin:0;padding:0}.swagger-section .swagger-ui-wrap form.formtastic fieldset.inputs ol li.boolean input{display:block;float:left;clear:none;margin:0 5px 0 0}.swagger-section .swagger-ui-wrap form.formtastic fieldset.inputs ol li.required label{color:#000}.swagger-section .swagger-ui-wrap form.formtastic fieldset.inputs ol li label{display:block;clear:both;width:auto;padding:0 0 3px;color:#666}.swagger-section .swagger-ui-wrap form.formtastic fieldset.inputs ol li label abbr{padding-left:3px;color:#888}.swagger-section .swagger-ui-wrap form.formtastic fieldset.inputs ol li p.inline-hints{margin-left:0;font-style:italic;font-size:.9em;margin:0}.swagger-section .swagger-ui-wrap form.formtastic fieldset.buttons{margin:0;padding:0}.swagger-section .swagger-ui-wrap span.blank,.swagger-section .swagger-ui-wrap span.empty{color:#888;font-style:italic}.swagger-section .swagger-ui-wrap .markdown h3{color:#547f00}.swagger-section .swagger-ui-wrap .markdown h4{color:#666}.swagger-section .swagger-ui-wrap .markdown pre{font-family:Anonymous Pro,Menlo,Consolas,Bitstream Vera Sans Mono,Courier New,monospace;background-color:#fcf6db;border:1px solid #e5e0c6;padding:10px;margin:0 0 10px}.swagger-section .swagger-ui-wrap .markdown pre code{line-height:1.6em;overflow:auto}.swagger-section .swagger-ui-wrap div.gist{margin:20px 0 25px!important}.swagger-section .swagger-ui-wrap ul#resources{font-family:Droid Sans,sans-serif;font-size:.9em}.swagger-section .swagger-ui-wrap ul#resources li.resource{border-bottom:1px solid #ddd}.swagger-section .swagger-ui-wrap ul#resources li.resource.active div.heading h2 a,.swagger-section .swagger-ui-wrap ul#resources li.resource:hover div.heading h2 a{color:#000}.swagger-section .swagger-ui-wrap ul#resources li.resource.active div.heading ul.options li a,.swagger-section .swagger-ui-wrap ul#resources li.resource:hover div.heading ul.options li a{color:#555}.swagger-section .swagger-ui-wrap ul#resources li.resource:last-child{border-bottom:none}.swagger-section .swagger-ui-wrap ul#resources li.resource div.heading{border:1px solid transparent;float:none;clear:both;overflow:hidden;display:block}.swagger-section .swagger-ui-wrap ul#resources li.resource div.heading ul.options{overflow:hidden;padding:0;display:block;clear:none;float:right;margin:14px 10px 0 0}.swagger-section .swagger-ui-wrap ul#resources li.resource div.heading ul.options li{float:left;clear:none;margin:0;padding:2px 10px;border-right:1px solid #ddd;color:#666;font-size:.9em}.swagger-section .swagger-ui-wrap ul#resources li.resource div.heading ul.options li a{color:#aaa;text-decoration:none}.swagger-section .swagger-ui-wrap ul#resources li.resource div.heading ul.options li a:hover{text-decoration:underline;color:#000}.swagger-section .swagger-ui-wrap ul#resources li.resource div.heading ul.options li a.active,.swagger-section .swagger-ui-wrap ul#resources li.resource div.heading ul.options li a:active,.swagger-section .swagger-ui-wrap ul#resources li.resource div.heading ul.options li a:hover{text-decoration:underline}.swagger-section .swagger-ui-wrap ul#resources li.resource div.heading ul.options li.first,.swagger-section .swagger-ui-wrap ul#resources li.resource div.heading ul.options li:first-child{padding-left:0}.swagger-section .swagger-ui-wrap ul#resources li.resource div.heading ul.options li.last,.swagger-section .swagger-ui-wrap ul#resources li.resource div.heading ul.options li:last-child{padding-right:0;border-right:none}.swagger-section .swagger-ui-wrap ul#resources li.resource div.heading ul.options.first,.swagger-section .swagger-ui-wrap ul#resources li.resource div.heading ul.options:first-child{padding-left:0}.swagger-section .swagger-ui-wrap ul#resources li.resource div.heading h2{color:#999;padding-left:0;display:block;clear:none;float:left;font-family:Droid Sans,sans-serif;font-weight:700}.swagger-section .swagger-ui-wrap ul#resources li.resource div.heading h2 a{color:#999}.swagger-section .swagger-ui-wrap ul#resources li.resource div.heading h2 a:hover{color:#000}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation{float:none;clear:both;overflow:hidden;display:block;margin:0 0 10px;padding:0}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation div.heading{float:none;clear:both;overflow:hidden;display:block;margin:0;padding:0}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation div.heading h3{display:block;clear:none;float:left;width:auto;margin:0;padding:0;line-height:1.1em;color:#000}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation div.heading h3 span.path{padding-left:10px}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation div.heading h3 span.path a{color:#000;text-decoration:none}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation div.heading h3 span.path a.toggleOperation.deprecated{text-decoration:line-through}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation div.heading h3 span.path a:hover{text-decoration:underline}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation div.heading h3 span.http_method a{text-transform:uppercase;text-decoration:none;color:#fff;display:inline-block;width:50px;font-size:.7em;text-align:center;padding:7px 0 4px;border-radius:2px}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation div.heading h3 span{margin:0;padding:0}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation div.heading ul.options{overflow:hidden;padding:0;display:block;clear:none;float:right;margin:6px 10px 0 0}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation div.heading ul.options li{float:left;clear:none;margin:0;padding:2px 10px;font-size:.9em}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation div.heading ul.options li a{text-decoration:none}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation div.heading ul.options li a .markdown p{color:inherit;padding:0;line-height:inherit}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation div.heading ul.options li.access{color:#000}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation div.content{border-top:none;padding:10px;border-bottom-left-radius:6px;border-bottom-right-radius:6px;margin:0 0 20px}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation div.content h4{font-size:1.1em;margin:0;padding:15px 0 5px}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation div.content div.sandbox_header{float:none;clear:both;overflow:hidden;display:block}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation div.content div.sandbox_header a{padding:4px 0 0 10px;display:inline-block;font-size:.9em}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation div.content div.sandbox_header input.submit{display:block;clear:none;float:left;padding:6px 8px}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation div.content div.sandbox_header span.response_throbber{background-image:url(../images/throbber.gif);width:128px;height:16px;display:block;clear:none;float:right}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation div.content form input[type=text].error{outline:2px solid #000;outline-color:#c00}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation div.content form select[name=parameterContentType]{max-width:300px}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation div.content div.response div.block pre{font-family:Anonymous Pro,Menlo,Consolas,Bitstream Vera Sans Mono,Courier New,monospace;padding:10px;font-size:.9em;max-height:400px;overflow-y:auto}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.heading{background-color:#f9f2e9;border:1px solid #f0e0ca}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.heading h3 span.http_method a{background-color:#c5862b}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.heading ul.options li{border-right:1px solid #ddd;border-right-color:#f0e0ca;color:#c5862b}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.heading ul.options li a{color:#c5862b}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.content{background-color:#faf5ee;border:1px solid #f0e0ca}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.content h4{color:#c5862b}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.content div.sandbox_header a{color:#dcb67f}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.head div.heading{background-color:#fcffcd;border:1px solid #000;border-color:#ffd20f}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.head div.heading h3 span.http_method a{text-transform:uppercase;background-color:#ffd20f}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.head div.heading ul.options li{border-right:1px solid #ddd;border-right-color:#ffd20f;color:#ffd20f}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.head div.heading ul.options li a{color:#ffd20f}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.head div.content{background-color:#fcffcd;border:1px solid #000;border-color:#ffd20f}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.head div.content h4{color:#ffd20f}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.head div.content div.sandbox_header a{color:#6fc992}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.heading{background-color:#f5e8e8;border:1px solid #e8c6c7}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.heading h3 span.http_method a{text-transform:uppercase;background-color:#a41e22}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.heading ul.options li{border-right:1px solid #ddd;border-right-color:#e8c6c7;color:#a41e22}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.heading ul.options li a{color:#a41e22}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.content{background-color:#f7eded;border:1px solid #e8c6c7}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.content h4{color:#a41e22}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.content div.sandbox_header a{color:#c8787a}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.heading{background-color:#e7f6ec;border:1px solid #c3e8d1}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.heading h3 span.http_method a{background-color:#10a54a}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.heading ul.options li{border-right:1px solid #ddd;border-right-color:#c3e8d1;color:#10a54a}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.heading ul.options li a{color:#10a54a}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.content{background-color:#ebf7f0;border:1px solid #c3e8d1}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.content h4{color:#10a54a}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.content div.sandbox_header a{color:#6fc992}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.patch div.heading{background-color:#fce9e3;border:1px solid #f5d5c3}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.patch div.heading h3 span.http_method a{background-color:#d38042}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.patch div.heading ul.options li{border-right:1px solid #ddd;border-right-color:#f0cecb;color:#d38042}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.patch div.heading ul.options li a{color:#d38042}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.patch div.content{background-color:#faf0ef;border:1px solid #f0cecb}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.patch div.content h4{color:#d38042}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.patch div.content div.sandbox_header a{color:#dcb67f}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.heading{background-color:#e7f0f7;border:1px solid #c3d9ec}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.heading h3 span.http_method a{background-color:#0f6ab4}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.heading ul.options li{border-right:1px solid #ddd;border-right-color:#c3d9ec;color:#0f6ab4}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.heading ul.options li a{color:#0f6ab4}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.content{background-color:#ebf3f9;border:1px solid #c3d9ec}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.content h4{color:#0f6ab4}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.content div.sandbox_header a{color:#6fa5d2}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.options div.heading{background-color:#e7f0f7;border:1px solid #c3d9ec}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.options div.heading h3 span.http_method a{background-color:#0f6ab4}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.options div.heading ul.options li{border-right:1px solid #ddd;border-right-color:#c3d9ec;color:#0f6ab4}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.options div.heading ul.options li a{color:#0f6ab4}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.options div.content{background-color:#ebf3f9;border:1px solid #c3d9ec}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.options div.content h4{color:#0f6ab4}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.options div.content div.sandbox_header a{color:#6fa5d2}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.content,.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.content,.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.head div.content,.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.patch div.content,.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.content,.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.content{border-top:none}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.heading ul.options li.last,.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.heading ul.options li:last-child,.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.heading ul.options li.last,.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.heading ul.options li:last-child,.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.head div.heading ul.options li.last,.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.head div.heading ul.options li:last-child,.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.patch div.heading ul.options li.last,.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.patch div.heading ul.options li:last-child,.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.heading ul.options li.last,.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.heading ul.options li:last-child,.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.heading ul.options li.last,.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.heading ul.options li:last-child{padding-right:0;border-right:none}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations ul.options li a.active,.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations ul.options li a:active,.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations ul.options li a:hover{text-decoration:underline}.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations.first,.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations:first-child,.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations ul.options li.first,.swagger-section .swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations ul.options li:first-child{padding-left:0}.swagger-section .swagger-ui-wrap p#colophon{margin:0 15px 40px;padding:10px 0;font-size:.8em;border-top:1px solid #ddd;font-family:Droid Sans,sans-serif;color:#999;font-style:italic}.swagger-section .swagger-ui-wrap p#colophon a{text-decoration:none;color:#547f00}.swagger-section .swagger-ui-wrap h3{color:#000;font-size:1.1em;padding:10px 0}.swagger-section .swagger-ui-wrap .markdown ol,.swagger-section .swagger-ui-wrap .markdown ul{font-family:Droid Sans,sans-serif;margin:5px 0 10px;padding:0 0 0 18px;list-style-type:disc}.swagger-section .swagger-ui-wrap form.form_box{background-color:#ebf3f9;border:1px solid #c3d9ec;padding:10px}.swagger-section .swagger-ui-wrap form.form_box label{color:#0f6ab4!important}.swagger-section .swagger-ui-wrap form.form_box input[type=submit]{display:block;padding:10px}.swagger-section .swagger-ui-wrap form.form_box p.weak{font-size:.8em}.swagger-section .swagger-ui-wrap form.form_box p{font-size:.9em;padding:0 0 15px;color:#7e7b6d}.swagger-section .swagger-ui-wrap form.form_box p a{color:#646257}.swagger-section .swagger-ui-wrap form.form_box p strong{color:#000}.swagger-section .swagger-ui-wrap .operation-status td.markdown>p:last-child{padding-bottom:0}.swagger-section .title{font-style:bold}.swagger-section .secondary_form{display:none}.swagger-section .main_image{display:block;margin-left:auto;margin-right:auto}.swagger-section .oauth_body{margin-left:100px;margin-right:100px}.swagger-section .oauth_submit{text-align:center;display:inline-block}.swagger-section .authorize-wrapper{margin:15px 0 10px}.swagger-section .authorize-wrapper_operation{float:right}.swagger-section .authorize__btn:hover{text-decoration:underline;cursor:pointer}.swagger-section .authorize__btn_operation:hover .authorize-scopes{display:block}.swagger-section .authorize-scopes{position:absolute;margin-top:20px;background:#fff;border:1px solid #ccc;border-radius:5px;display:none;font-size:13px;max-width:300px;line-height:30px;color:#000;padding:5px}.swagger-section .authorize-scopes .authorize__scope{text-decoration:none}.swagger-section .authorize__btn_operation{height:18px;vertical-align:middle;display:inline-block;background:url(../images/explorer_icons.png) no-repeat}.swagger-section .authorize__btn_operation_login{background-position:0 0;width:18px;margin-top:-6px;margin-left:4px}.swagger-section .authorize__btn_operation_logout{background-position:-30px 0;width:18px;margin-top:-6px;margin-left:4px}.swagger-section #auth_container{color:#fff;display:inline-block;border:none;padding:5px;width:87px;height:13px}.swagger-section #auth_container .authorize__btn{color:#fff}.swagger-section .auth_container{padding:0 0 10px;margin-bottom:5px;border-bottom:1px solid #ccc;font-size:.9em}.swagger-section .auth_container .auth__title{color:#547f00;font-size:1.2em}.swagger-section .auth_container .basic_auth__label{display:inline-block;width:60px}.swagger-section .auth_container .auth__description{color:#999;margin-bottom:5px}.swagger-section .auth_container .auth__button{margin-top:10px;height:30px}.swagger-section .auth_container .key_auth__field{margin:5px 0}.swagger-section .auth_container .key_auth__label{display:inline-block;width:60px}.swagger-section .api-popup-dialog{position:absolute;display:none}.swagger-section .api-popup-dialog-wrapper{z-index:2;width:500px;background:#fff;padding:20px;border:1px solid #ccc;border-radius:5px;font-size:13px;color:#777;position:fixed;top:50%;left:50%;transform:translate(-50%,-50%)}.swagger-section .api-popup-dialog-shadow{position:fixed;top:0;left:0;width:100%;height:100%;opacity:.2;background-color:gray;z-index:1}.swagger-section .api-popup-dialog .api-popup-title{font-size:24px;padding:10px 0}.swagger-section .api-popup-dialog .error-msg{padding-left:5px;padding-bottom:5px}.swagger-section .api-popup-dialog .api-popup-content{max-height:500px;overflow-y:auto}.swagger-section .api-popup-dialog .api-popup-authbtn,.swagger-section .api-popup-dialog .api-popup-cancel{height:30px}.swagger-section .api-popup-scopes{padding:10px 20px}.swagger-section .api-popup-scopes li{padding:5px 0;line-height:20px}.swagger-section .api-popup-scopes li input{position:relative;top:2px}.swagger-section .api-popup-scopes .api-scope-desc{padding-left:20px;font-style:italic}.swagger-section .api-popup-actions{padding-top:10px}.swagger-section .access,.swagger-section .auth{float:right}.swagger-section .api-ic{height:18px;vertical-align:middle;display:inline-block;background:url(../images/explorer_icons.png) no-repeat}.swagger-section .api-ic .api_information_panel{position:relative;margin-top:20px;margin-left:-5px;background:#fff;border:1px solid #ccc;border-radius:5px;display:none;font-size:13px;max-width:300px;line-height:30px;color:#000;padding:5px}.swagger-section .api-ic .api_information_panel p .api-msg-enabled{color:green}.swagger-section .api-ic .api_information_panel p .api-msg-disabled{color:red}.swagger-section .api-ic:hover .api_information_panel{position:absolute;display:block}.swagger-section .ic-info{background-position:0 0;width:18px;margin-top:-6px;margin-left:4px}.swagger-section .ic-warning{background-position:-60px 0;width:18px;margin-top:-6px;margin-left:4px}.swagger-section .ic-error{background-position:-30px 0;width:18px;margin-top:-6px;margin-left:4px}.swagger-section .ic-off{background-position:-90px 0;width:58px;margin-top:-4px;cursor:pointer}.swagger-section .ic-on{background-position:-160px 0;width:58px;margin-top:-4px;cursor:pointer}.swagger-section #header{background-color:#89bf04;padding:9px 14px 19px;height:23px;min-width:775px}.swagger-section #input_baseUrl{width:400px}.swagger-section #api_selector{display:block;clear:none;float:right}.swagger-section #api_selector .input{display:inline-block;clear:none;margin:0 10px 0 0}.swagger-section #api_selector input{font-size:.9em;padding:3px;margin:0}.swagger-section #input_apiKey{width:200px}.swagger-section #auth_container .authorize__btn,.swagger-section #explore{display:block;text-decoration:none;font-weight:700;padding:6px 8px;font-size:.9em;color:#fff;background-color:#547f00;border-radius:4px}.swagger-section #auth_container .authorize__btn:hover,.swagger-section #explore:hover{background-color:#547f00}.swagger-section #header #logo{font-size:1.5em;font-weight:700;text-decoration:none;color:#fff}.swagger-section #header #logo .logo__img{display:block;float:left;margin-top:2px}.swagger-section #header #logo .logo__title{display:inline-block;padding:5px 0 0 10px}.swagger-section #content_message{margin:10px 15px;font-style:italic;color:#999}.swagger-section #message-bar{min-height:30px;text-align:center;padding-top:10px}.swagger-section .swagger-collapse:before{content:"-"}.swagger-section .swagger-expand:before{content:"+"}.swagger-section .error{outline-color:#c00;background-color:#f2dede} \ No newline at end of file diff --git a/static/docs/css/style.css b/static/docs/css/style.css new file mode 100644 index 0000000..6766e27 --- /dev/null +++ b/static/docs/css/style.css @@ -0,0 +1 @@ +.swagger-section #header a#logo{font-size:1.5em;font-weight:700;text-decoration:none;background:transparent url(../images/logo.png) no-repeat 0;padding:20px 0 20px 40px}#text-head{font-size:80px;font-family:Roboto,sans-serif;color:#fff;float:right;margin-right:20%}.navbar-fixed-top .navbar-brand,.navbar-fixed-top .navbar-nav,.navbar-header{height:auto}.navbar-inverse{background-color:#000;border-color:#000}#navbar-brand{margin-left:20%}.navtext{font-size:10px}.h1,h1{font-size:60px}.navbar-default .navbar-header .navbar-brand{color:#a2dfee}.swagger-section .swagger-ui-wrap ul#resources li.resource div.heading h2 a{color:#393939;font-family:Arvo,serif;font-size:1.5em}.swagger-section .swagger-ui-wrap ul#resources li.resource div.heading h2 a:hover{color:#000}.swagger-section .swagger-ui-wrap ul#resources li.resource div.heading h2{color:#525252;padding-left:0;display:block;clear:none;float:left;font-family:Arvo,serif;font-weight:700}.navbar-default .navbar-collapse,.navbar-default .navbar-form{border-color:#0a0a0a}.container1{width:1500px;margin:auto;margin-top:0;background-image:url(../images/shield.png);background-repeat:no-repeat;background-position:-40px -20px;margin-bottom:210px}.container-inner{width:1200px;margin:auto;background-color:hsla(192,8%,88%,.75);padding-bottom:40px;padding-top:40px;border-radius:15px}.header-content{padding:0;width:1000px}.title1{font-size:80px;font-family:Vollkorn,serif;color:#404040;text-align:center;padding-top:40px;padding-bottom:100px}#icon{margin-top:-18px}.subtext{font-size:25px;font-style:italic;color:#08b;text-align:right;padding-right:250px}.bg-primary{background-color:#00468b}.navbar-default .nav>li>a,.navbar-default .nav>li>a:focus,.navbar-default .nav>li>a:focus:hover,.navbar-default .nav>li>a:hover{color:#08b}.text-faded{font-size:25px;font-family:Vollkorn,serif}.section-heading{font-family:Vollkorn,serif;font-size:45px;padding-bottom:10px}hr{border-color:#00468b;padding-bottom:10px}.description{margin-top:20px;padding-bottom:200px}.description li{font-family:Vollkorn,serif;font-size:25px;color:#525252;margin-left:28%;padding-top:5px}.gap{margin-top:200px}.troubleshootingtext{color:hsla(0,0%,100%,.7);padding-left:30%}.troubleshootingtext li{list-style-type:circle;font-size:25px;padding-bottom:5px}.overlay{position:absolute;top:0;left:0;width:100%;height:100%;z-index:1}.block.response_body.json:hover{cursor:pointer}.backdrop{color:blue}#myModal{height:100%}.modal-backdrop{bottom:0;position:fixed}.curl{padding:10px;font-family:Anonymous Pro,Menlo,Consolas,Bitstream Vera Sans Mono,Courier New,monospace;font-size:.9em;max-height:400px;margin-top:5px;overflow-y:auto;background-color:#fcf6db;border:1px solid #e5e0c6;border-radius:4px}.curl_title{font-size:1.1em;margin:0;padding:15px 0 5px;font-family:Open Sans,Helvetica Neue,Arial,sans-serif;font-weight:500;line-height:1.1}.footer{display:none}.swagger-section .swagger-ui-wrap h2{padding:0}h2{margin:0;margin-bottom:5px}.markdown p,.swagger-section .swagger-ui-wrap .code{font-size:15px;font-family:Arvo,serif}.swagger-section .swagger-ui-wrap b{font-family:Arvo,serif}#signin:hover{cursor:pointer}.dropdown-menu{padding:15px}.navbar-right .dropdown-menu{left:0;right:auto}#signinbutton{width:100%;height:32px;font-size:13px;font-weight:700;color:#08b}.navbar-default .nav>li .details{color:#000;text-transform:none;font-size:15px;font-weight:400;font-family:Open Sans,sans-serif;font-style:italic;line-height:20px;top:-2px}.navbar-default .nav>li .details:hover{color:#000}#signout{width:100%;height:32px;font-size:13px;font-weight:700;color:#08b} \ No newline at end of file diff --git a/static/docs/css/typography.css b/static/docs/css/typography.css new file mode 100644 index 0000000..e69de29 diff --git a/static/docs/fonts/DroidSans-Bold.ttf b/static/docs/fonts/DroidSans-Bold.ttf new file mode 100644 index 0000000..036c4d1 Binary files /dev/null and b/static/docs/fonts/DroidSans-Bold.ttf differ diff --git a/static/docs/fonts/DroidSans.ttf b/static/docs/fonts/DroidSans.ttf new file mode 100644 index 0000000..e517a0c Binary files /dev/null and b/static/docs/fonts/DroidSans.ttf differ diff --git a/static/docs/images/collapse.gif b/static/docs/images/collapse.gif new file mode 100644 index 0000000..8843e8c Binary files /dev/null and b/static/docs/images/collapse.gif differ diff --git a/static/docs/images/expand.gif b/static/docs/images/expand.gif new file mode 100644 index 0000000..477bf13 Binary files /dev/null and b/static/docs/images/expand.gif differ diff --git a/static/docs/images/explorer_icons.png b/static/docs/images/explorer_icons.png new file mode 100644 index 0000000..be43b27 Binary files /dev/null and b/static/docs/images/explorer_icons.png differ diff --git a/static/docs/images/favicon-16x16.png b/static/docs/images/favicon-16x16.png new file mode 100644 index 0000000..0f7e13b Binary files /dev/null and b/static/docs/images/favicon-16x16.png differ diff --git a/static/docs/images/favicon-32x32.png b/static/docs/images/favicon-32x32.png new file mode 100644 index 0000000..b0a3352 Binary files /dev/null and b/static/docs/images/favicon-32x32.png differ diff --git a/static/docs/images/favicon.ico b/static/docs/images/favicon.ico new file mode 100644 index 0000000..8b60bcf Binary files /dev/null and b/static/docs/images/favicon.ico differ diff --git a/static/docs/images/logo_small.png b/static/docs/images/logo_small.png new file mode 100644 index 0000000..ce3908e Binary files /dev/null and b/static/docs/images/logo_small.png differ diff --git a/static/docs/images/pet_store_api.png b/static/docs/images/pet_store_api.png new file mode 100644 index 0000000..1192ad8 Binary files /dev/null and b/static/docs/images/pet_store_api.png differ diff --git a/static/docs/images/throbber.gif b/static/docs/images/throbber.gif new file mode 100644 index 0000000..0639388 Binary files /dev/null and b/static/docs/images/throbber.gif differ diff --git a/static/docs/images/wordnik_api.png b/static/docs/images/wordnik_api.png new file mode 100644 index 0000000..dc0ddab Binary files /dev/null and b/static/docs/images/wordnik_api.png differ diff --git a/static/docs/index.html b/static/docs/index.html new file mode 100644 index 0000000..550bba0 --- /dev/null +++ b/static/docs/index.html @@ -0,0 +1,107 @@ + + + + + Swagger UI + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
 
+
+ + diff --git a/static/docs/lang/ca.js b/static/docs/lang/ca.js new file mode 100644 index 0000000..f8c815a --- /dev/null +++ b/static/docs/lang/ca.js @@ -0,0 +1,53 @@ +'use strict'; + +/* jshint quotmark: double */ +window.SwaggerTranslator.learn({ + "Warning: Deprecated":"Advertència: Obsolet", + "Implementation Notes":"Notes d'implementació", + "Response Class":"Classe de la Resposta", + "Status":"Estatus", + "Parameters":"Paràmetres", + "Parameter":"Paràmetre", + "Value":"Valor", + "Description":"Descripció", + "Parameter Type":"Tipus del Paràmetre", + "Data Type":"Tipus de la Dada", + "Response Messages":"Missatges de la Resposta", + "HTTP Status Code":"Codi d'Estatus HTTP", + "Reason":"Raó", + "Response Model":"Model de la Resposta", + "Request URL":"URL de la Sol·licitud", + "Response Body":"Cos de la Resposta", + "Response Code":"Codi de la Resposta", + "Response Headers":"Capçaleres de la Resposta", + "Hide Response":"Amagar Resposta", + "Try it out!":"Prova-ho!", + "Show/Hide":"Mostrar/Amagar", + "List Operations":"Llista Operacions", + "Expand Operations":"Expandir Operacions", + "Raw":"Cru", + "can't parse JSON. Raw result":"no puc analitzar el JSON. Resultat cru", + "Example Value":"Valor d'Exemple", + "Model Schema":"Esquema del Model", + "Model":"Model", + "apply":"aplicar", + "Username":"Nom d'usuari", + "Password":"Contrasenya", + "Terms of service":"Termes del servei", + "Created by":"Creat per", + "See more at":"Veure més en", + "Contact the developer":"Contactar amb el desenvolupador", + "api version":"versió de la api", + "Response Content Type":"Tipus de Contingut de la Resposta", + "fetching resource":"recollint recurs", + "fetching resource list":"recollins llista de recursos", + "Explore":"Explorant", + "Show Swagger Petstore Example Apis":"Mostrar API d'Exemple Swagger Petstore", + "Can't read from server. It may not have the appropriate access-control-origin settings.":"No es pot llegir del servidor. Potser no teniu la configuració de control d'accés apropiada.", + "Please specify the protocol for":"Si us plau, especifiqueu el protocol per a", + "Can't read swagger JSON from":"No es pot llegir el JSON de swagger des de", + "Finished Loading Resource Information. Rendering Swagger UI":"Finalitzada la càrrega del recurs informatiu. Renderitzant Swagger UI", + "Unable to read api":"No es pot llegir l'api", + "from path":"des de la ruta", + "server returned":"el servidor ha retornat" +}); diff --git a/static/docs/lang/el.js b/static/docs/lang/el.js new file mode 100644 index 0000000..fcd1ffd --- /dev/null +++ b/static/docs/lang/el.js @@ -0,0 +1,56 @@ +'use strict'; + +/* jshint quotmark: double */ +window.SwaggerTranslator.learn({ + "Warning: Deprecated":"Προειδοποίηση: Έχει αποσυρθεί", + "Implementation Notes":"Σημειώσεις Υλοποίησης", + "Response Class":"Απόκριση", + "Status":"Κατάσταση", + "Parameters":"Παράμετροι", + "Parameter":"Παράμετρος", + "Value":"Τιμή", + "Description":"Περιγραφή", + "Parameter Type":"Τύπος Παραμέτρου", + "Data Type":"Τύπος Δεδομένων", + "Response Messages":"Μηνύματα Απόκρισης", + "HTTP Status Code":"Κωδικός Κατάστασης HTTP", + "Reason":"Αιτιολογία", + "Response Model":"Μοντέλο Απόκρισης", + "Request URL":"URL Αιτήματος", + "Response Body":"Σώμα Απόκρισης", + "Response Code":"Κωδικός Απόκρισης", + "Response Headers":"Επικεφαλίδες Απόκρισης", + "Hide Response":"Απόκρυψη Απόκρισης", + "Headers":"Επικεφαλίδες", + "Try it out!":"Δοκιμάστε το!", + "Show/Hide":"Εμφάνιση/Απόκρυψη", + "List Operations":"Λίστα Λειτουργιών", + "Expand Operations":"Ανάπτυξη Λειτουργιών", + "Raw":"Ακατέργαστο", + "can't parse JSON. Raw result":"αδυναμία ανάλυσης JSON. Ακατέργαστο αποτέλεσμα", + "Example Value":"Παράδειγμα Τιμής", + "Model Schema":"Σχήμα Μοντέλου", + "Model":"Μοντέλο", + "Click to set as parameter value":"Πατήστε για να θέσετε τιμή παραμέτρου", + "apply":"εφαρμογή", + "Username":"Όνομα χρήση", + "Password":"Κωδικός πρόσβασης", + "Terms of service":"Όροι χρήσης", + "Created by":"Δημιουργήθηκε από", + "See more at":"Δείτε περισσότερα στο", + "Contact the developer":"Επικοινωνήστε με τον προγραμματιστή", + "api version":"έκδοση api", + "Response Content Type":"Τύπος Περιεχομένου Απόκρισης", + "Parameter content type:":"Τύπος περιεχομένου παραμέτρου:", + "fetching resource":"παραλαβή πόρου", + "fetching resource list":"παραλαβή λίστας πόρων", + "Explore":"Εξερεύνηση", + "Show Swagger Petstore Example Apis":"Εμφάνιση Api Δειγμάτων Petstore του Swagger", + "Can't read from server. It may not have the appropriate access-control-origin settings.":"Αδυναμία ανάγνωσης από τον εξυπηρετητή. Μπορεί να μην έχει κατάλληλες ρυθμίσεις για access-control-origin.", + "Please specify the protocol for":"Παρακαλώ προσδιορίστε το πρωτόκολλο για", + "Can't read swagger JSON from":"Αδυναμία ανάγνωσης swagger JSON από", + "Finished Loading Resource Information. Rendering Swagger UI":"Ολοκλήρωση Φόρτωσης Πληροφορικών Πόρου. Παρουσίαση Swagger UI", + "Unable to read api":"Αδυναμία ανάγνωσης api", + "from path":"από το μονοπάτι", + "server returned":"ο εξυπηρετηρής επέστρεψε" +}); diff --git a/static/docs/lang/en.js b/static/docs/lang/en.js new file mode 100644 index 0000000..9183136 --- /dev/null +++ b/static/docs/lang/en.js @@ -0,0 +1,56 @@ +'use strict'; + +/* jshint quotmark: double */ +window.SwaggerTranslator.learn({ + "Warning: Deprecated":"Warning: Deprecated", + "Implementation Notes":"Implementation Notes", + "Response Class":"Response Class", + "Status":"Status", + "Parameters":"Parameters", + "Parameter":"Parameter", + "Value":"Value", + "Description":"Description", + "Parameter Type":"Parameter Type", + "Data Type":"Data Type", + "Response Messages":"Response Messages", + "HTTP Status Code":"HTTP Status Code", + "Reason":"Reason", + "Response Model":"Response Model", + "Request URL":"Request URL", + "Response Body":"Response Body", + "Response Code":"Response Code", + "Response Headers":"Response Headers", + "Hide Response":"Hide Response", + "Headers":"Headers", + "Try it out!":"Try it out!", + "Show/Hide":"Show/Hide", + "List Operations":"List Operations", + "Expand Operations":"Expand Operations", + "Raw":"Raw", + "can't parse JSON. Raw result":"can't parse JSON. Raw result", + "Example Value":"Example Value", + "Model Schema":"Model Schema", + "Model":"Model", + "Click to set as parameter value":"Click to set as parameter value", + "apply":"apply", + "Username":"Username", + "Password":"Password", + "Terms of service":"Terms of service", + "Created by":"Created by", + "See more at":"See more at", + "Contact the developer":"Contact the developer", + "api version":"api version", + "Response Content Type":"Response Content Type", + "Parameter content type:":"Parameter content type:", + "fetching resource":"fetching resource", + "fetching resource list":"fetching resource list", + "Explore":"Explore", + "Show Swagger Petstore Example Apis":"Show Swagger Petstore Example Apis", + "Can't read from server. It may not have the appropriate access-control-origin settings.":"Can't read from server. It may not have the appropriate access-control-origin settings.", + "Please specify the protocol for":"Please specify the protocol for", + "Can't read swagger JSON from":"Can't read swagger JSON from", + "Finished Loading Resource Information. Rendering Swagger UI":"Finished Loading Resource Information. Rendering Swagger UI", + "Unable to read api":"Unable to read api", + "from path":"from path", + "server returned":"server returned" +}); diff --git a/static/docs/lang/es.js b/static/docs/lang/es.js new file mode 100644 index 0000000..13fa015 --- /dev/null +++ b/static/docs/lang/es.js @@ -0,0 +1,53 @@ +'use strict'; + +/* jshint quotmark: double */ +window.SwaggerTranslator.learn({ + "Warning: Deprecated":"Advertencia: Obsoleto", + "Implementation Notes":"Notas de implementación", + "Response Class":"Clase de la Respuesta", + "Status":"Status", + "Parameters":"Parámetros", + "Parameter":"Parámetro", + "Value":"Valor", + "Description":"Descripción", + "Parameter Type":"Tipo del Parámetro", + "Data Type":"Tipo del Dato", + "Response Messages":"Mensajes de la Respuesta", + "HTTP Status Code":"Código de Status HTTP", + "Reason":"Razón", + "Response Model":"Modelo de la Respuesta", + "Request URL":"URL de la Solicitud", + "Response Body":"Cuerpo de la Respuesta", + "Response Code":"Código de la Respuesta", + "Response Headers":"Encabezados de la Respuesta", + "Hide Response":"Ocultar Respuesta", + "Try it out!":"Pruébalo!", + "Show/Hide":"Mostrar/Ocultar", + "List Operations":"Listar Operaciones", + "Expand Operations":"Expandir Operaciones", + "Raw":"Crudo", + "can't parse JSON. Raw result":"no puede parsear el JSON. Resultado crudo", + "Example Value":"Valor de Ejemplo", + "Model Schema":"Esquema del Modelo", + "Model":"Modelo", + "apply":"aplicar", + "Username":"Nombre de usuario", + "Password":"Contraseña", + "Terms of service":"Términos de Servicio", + "Created by":"Creado por", + "See more at":"Ver más en", + "Contact the developer":"Contactar al desarrollador", + "api version":"versión de la api", + "Response Content Type":"Tipo de Contenido (Content Type) de la Respuesta", + "fetching resource":"buscando recurso", + "fetching resource list":"buscando lista del recurso", + "Explore":"Explorar", + "Show Swagger Petstore Example Apis":"Mostrar Api Ejemplo de Swagger Petstore", + "Can't read from server. It may not have the appropriate access-control-origin settings.":"No se puede leer del servidor. Tal vez no tiene la configuración de control de acceso de origen (access-control-origin) apropiado.", + "Please specify the protocol for":"Por favor, especificar el protocola para", + "Can't read swagger JSON from":"No se puede leer el JSON de swagger desde", + "Finished Loading Resource Information. Rendering Swagger UI":"Finalizada la carga del recurso de Información. Mostrando Swagger UI", + "Unable to read api":"No se puede leer la api", + "from path":"desde ruta", + "server returned":"el servidor retornó" +}); diff --git a/static/docs/lang/fr.js b/static/docs/lang/fr.js new file mode 100644 index 0000000..388dff1 --- /dev/null +++ b/static/docs/lang/fr.js @@ -0,0 +1,54 @@ +'use strict'; + +/* jshint quotmark: double */ +window.SwaggerTranslator.learn({ + "Warning: Deprecated":"Avertissement : Obsolète", + "Implementation Notes":"Notes d'implémentation", + "Response Class":"Classe de la réponse", + "Status":"Statut", + "Parameters":"Paramètres", + "Parameter":"Paramètre", + "Value":"Valeur", + "Description":"Description", + "Parameter Type":"Type du paramètre", + "Data Type":"Type de données", + "Response Messages":"Messages de la réponse", + "HTTP Status Code":"Code de statut HTTP", + "Reason":"Raison", + "Response Model":"Modèle de réponse", + "Request URL":"URL appelée", + "Response Body":"Corps de la réponse", + "Response Code":"Code de la réponse", + "Response Headers":"En-têtes de la réponse", + "Hide Response":"Cacher la réponse", + "Headers":"En-têtes", + "Try it out!":"Testez !", + "Show/Hide":"Afficher/Masquer", + "List Operations":"Liste des opérations", + "Expand Operations":"Développer les opérations", + "Raw":"Brut", + "can't parse JSON. Raw result":"impossible de décoder le JSON. Résultat brut", + "Example Value":"Exemple la valeur", + "Model Schema":"Définition du modèle", + "Model":"Modèle", + "apply":"appliquer", + "Username":"Nom d'utilisateur", + "Password":"Mot de passe", + "Terms of service":"Conditions de service", + "Created by":"Créé par", + "See more at":"Voir plus sur", + "Contact the developer":"Contacter le développeur", + "api version":"version de l'api", + "Response Content Type":"Content Type de la réponse", + "fetching resource":"récupération de la ressource", + "fetching resource list":"récupération de la liste de ressources", + "Explore":"Explorer", + "Show Swagger Petstore Example Apis":"Montrer les Apis de l'exemple Petstore de Swagger", + "Can't read from server. It may not have the appropriate access-control-origin settings.":"Impossible de lire à partir du serveur. Il se peut que les réglages access-control-origin ne soient pas appropriés.", + "Please specify the protocol for":"Veuillez spécifier un protocole pour", + "Can't read swagger JSON from":"Impossible de lire le JSON swagger à partir de", + "Finished Loading Resource Information. Rendering Swagger UI":"Chargement des informations terminé. Affichage de Swagger UI", + "Unable to read api":"Impossible de lire l'api", + "from path":"à partir du chemin", + "server returned":"réponse du serveur" +}); diff --git a/static/docs/lang/geo.js b/static/docs/lang/geo.js new file mode 100644 index 0000000..609c20d --- /dev/null +++ b/static/docs/lang/geo.js @@ -0,0 +1,56 @@ +'use strict'; + +/* jshint quotmark: double */ +window.SwaggerTranslator.learn({ + "Warning: Deprecated":"ყურადღება: აღარ გამოიყენება", + "Implementation Notes":"იმპლემენტაციის აღწერა", + "Response Class":"რესპონს კლასი", + "Status":"სტატუსი", + "Parameters":"პარამეტრები", + "Parameter":"პარამეტრი", + "Value":"მნიშვნელობა", + "Description":"აღწერა", + "Parameter Type":"პარამეტრის ტიპი", + "Data Type":"მონაცემის ტიპი", + "Response Messages":"პასუხი", + "HTTP Status Code":"HTTP სტატუსი", + "Reason":"მიზეზი", + "Response Model":"რესპონს მოდელი", + "Request URL":"მოთხოვნის URL", + "Response Body":"პასუხის სხეული", + "Response Code":"პასუხის კოდი", + "Response Headers":"პასუხის ჰედერები", + "Hide Response":"დამალე პასუხი", + "Headers":"ჰედერები", + "Try it out!":"ცადე !", + "Show/Hide":"გამოჩენა/დამალვა", + "List Operations":"ოპერაციების სია", + "Expand Operations":"ოპერაციები ვრცლად", + "Raw":"ნედლი", + "can't parse JSON. Raw result":"JSON-ის დამუშავება ვერ მოხერხდა. ნედლი პასუხი", + "Example Value":"მაგალითი", + "Model Schema":"მოდელის სტრუქტურა", + "Model":"მოდელი", + "Click to set as parameter value":"პარამეტრისთვის მნიშვნელობის მისანიჭებლად, დააკლიკე", + "apply":"გამოყენება", + "Username":"მოხმარებელი", + "Password":"პაროლი", + "Terms of service":"მომსახურების პირობები", + "Created by":"შექმნა", + "See more at":"ნახე ვრცლად", + "Contact the developer":"დაუკავშირდი დეველოპერს", + "api version":"api ვერსია", + "Response Content Type":"პასუხის კონტენტის ტიპი", + "Parameter content type:":"პარამეტრის კონტენტის ტიპი:", + "fetching resource":"რესურსების მიღება", + "fetching resource list":"რესურსების სიის მიღება", + "Explore":"ნახვა", + "Show Swagger Petstore Example Apis":"ნახე Swagger Petstore სამაგალითო Api", + "Can't read from server. It may not have the appropriate access-control-origin settings.":"სერვერთან დაკავშირება ვერ ხერხდება. შეამოწმეთ access-control-origin.", + "Please specify the protocol for":"მიუთითეთ პროტოკოლი", + "Can't read swagger JSON from":"swagger JSON წაკითხვა ვერ მოხერხდა", + "Finished Loading Resource Information. Rendering Swagger UI":"რესურსების ჩატვირთვა სრულდება. Swagger UI რენდერდება", + "Unable to read api":"api წაკითხვა ვერ მოხერხდა", + "from path":"მისამართიდან", + "server returned":"სერვერმა დააბრუნა" +}); diff --git a/static/docs/lang/it.js b/static/docs/lang/it.js new file mode 100644 index 0000000..8529c2a --- /dev/null +++ b/static/docs/lang/it.js @@ -0,0 +1,52 @@ +'use strict'; + +/* jshint quotmark: double */ +window.SwaggerTranslator.learn({ + "Warning: Deprecated":"Attenzione: Deprecato", + "Implementation Notes":"Note di implementazione", + "Response Class":"Classe della risposta", + "Status":"Stato", + "Parameters":"Parametri", + "Parameter":"Parametro", + "Value":"Valore", + "Description":"Descrizione", + "Parameter Type":"Tipo di parametro", + "Data Type":"Tipo di dato", + "Response Messages":"Messaggi della risposta", + "HTTP Status Code":"Codice stato HTTP", + "Reason":"Motivo", + "Response Model":"Modello di risposta", + "Request URL":"URL della richiesta", + "Response Body":"Corpo della risposta", + "Response Code":"Oggetto della risposta", + "Response Headers":"Intestazioni della risposta", + "Hide Response":"Nascondi risposta", + "Try it out!":"Provalo!", + "Show/Hide":"Mostra/Nascondi", + "List Operations":"Mostra operazioni", + "Expand Operations":"Espandi operazioni", + "Raw":"Grezzo (raw)", + "can't parse JSON. Raw result":"non è possibile parsare il JSON. Risultato grezzo (raw).", + "Model Schema":"Schema del modello", + "Model":"Modello", + "apply":"applica", + "Username":"Nome utente", + "Password":"Password", + "Terms of service":"Condizioni del servizio", + "Created by":"Creato da", + "See more at":"Informazioni aggiuntive:", + "Contact the developer":"Contatta lo sviluppatore", + "api version":"versione api", + "Response Content Type":"Tipo di contenuto (content type) della risposta", + "fetching resource":"recuperando la risorsa", + "fetching resource list":"recuperando lista risorse", + "Explore":"Esplora", + "Show Swagger Petstore Example Apis":"Mostra le api di esempio di Swagger Petstore", + "Can't read from server. It may not have the appropriate access-control-origin settings.":"Non è possibile leggere dal server. Potrebbe non avere le impostazioni di controllo accesso origine (access-control-origin) appropriate.", + "Please specify the protocol for":"Si prega di specificare il protocollo per", + "Can't read swagger JSON from":"Impossibile leggere JSON swagger da:", + "Finished Loading Resource Information. Rendering Swagger UI":"Lettura informazioni risorse termianta. Swagger UI viene mostrata", + "Unable to read api":"Impossibile leggere la api", + "from path":"da cartella", + "server returned":"il server ha restituito" +}); diff --git a/static/docs/lang/ja.js b/static/docs/lang/ja.js new file mode 100644 index 0000000..1cbeb37 --- /dev/null +++ b/static/docs/lang/ja.js @@ -0,0 +1,56 @@ +'use strict'; + +/* jshint quotmark: double */ +window.SwaggerTranslator.learn({ + "Warning: Deprecated":"警告: 廃止予定", + "Implementation Notes":"実装メモ", + "Response Class":"レスポンスクラス", + "Status":"ステータス", + "Parameters":"パラメータ群", + "Parameter":"パラメータ", + "Value":"値", + "Description":"説明", + "Parameter Type":"パラメータタイプ", + "Data Type":"データタイプ", + "Response Messages":"レスポンスメッセージ", + "HTTP Status Code":"HTTPステータスコード", + "Reason":"理由", + "Response Model":"レスポンスモデル", + "Request URL":"リクエストURL", + "Response Body":"レスポンスボディ", + "Response Code":"レスポンスコード", + "Response Headers":"レスポンスヘッダ", + "Hide Response":"レスポンスを隠す", + "Headers":"ヘッダ", + "Try it out!":"実際に実行!", + "Show/Hide":"表示/非表示", + "List Operations":"操作一覧", + "Expand Operations":"操作の展開", + "Raw":"未加工", + "can't parse JSON. Raw result":"JSONへ解釈できません. 未加工の結果", + "Example Value":"値の例", + "Model Schema":"モデルスキーマ", + "Model":"モデル", + "Click to set as parameter value":"パラメータ値と設定するにはクリック", + "apply":"実行", + "Username":"ユーザ名", + "Password":"パスワード", + "Terms of service":"サービス利用規約", + "Created by":"Created by", + "See more at":"詳細を見る", + "Contact the developer":"開発者に連絡", + "api version":"APIバージョン", + "Response Content Type":"レスポンス コンテンツタイプ", + "Parameter content type:":"パラメータコンテンツタイプ:", + "fetching resource":"リソースの取得", + "fetching resource list":"リソース一覧の取得", + "Explore":"調査", + "Show Swagger Petstore Example Apis":"SwaggerペットストアAPIの表示", + "Can't read from server. It may not have the appropriate access-control-origin settings.":"サーバから読み込めません. 適切なaccess-control-origin設定を持っていない可能性があります.", + "Please specify the protocol for":"プロトコルを指定してください", + "Can't read swagger JSON from":"次からswagger JSONを読み込めません", + "Finished Loading Resource Information. Rendering Swagger UI":"リソース情報の読み込みが完了しました. Swagger UIを描画しています", + "Unable to read api":"APIを読み込めません", + "from path":"次のパスから", + "server returned":"サーバからの返答" +}); diff --git a/static/docs/lang/ko-kr.js b/static/docs/lang/ko-kr.js new file mode 100644 index 0000000..03c7626 --- /dev/null +++ b/static/docs/lang/ko-kr.js @@ -0,0 +1,53 @@ +'use strict'; + +/* jshint quotmark: double */ +window.SwaggerTranslator.learn({ + "Warning: Deprecated":"경고:폐기예정됨", + "Implementation Notes":"구현 노트", + "Response Class":"응답 클래스", + "Status":"상태", + "Parameters":"매개변수들", + "Parameter":"매개변수", + "Value":"값", + "Description":"설명", + "Parameter Type":"매개변수 타입", + "Data Type":"데이터 타입", + "Response Messages":"응답 메세지", + "HTTP Status Code":"HTTP 상태 코드", + "Reason":"원인", + "Response Model":"응답 모델", + "Request URL":"요청 URL", + "Response Body":"응답 본문", + "Response Code":"응답 코드", + "Response Headers":"응답 헤더", + "Hide Response":"응답 숨기기", + "Headers":"헤더", + "Try it out!":"써보기!", + "Show/Hide":"보이기/숨기기", + "List Operations":"목록 작업", + "Expand Operations":"전개 작업", + "Raw":"원본", + "can't parse JSON. Raw result":"JSON을 파싱할수 없음. 원본결과:", + "Model Schema":"모델 스키마", + "Model":"모델", + "apply":"적용", + "Username":"사용자 이름", + "Password":"암호", + "Terms of service":"이용약관", + "Created by":"작성자", + "See more at":"추가정보:", + "Contact the developer":"개발자에게 문의", + "api version":"api버전", + "Response Content Type":"응답Content Type", + "fetching resource":"리소스 가져오기", + "fetching resource list":"리소스 목록 가져오기", + "Explore":"탐색", + "Show Swagger Petstore Example Apis":"Swagger Petstore 예제 보기", + "Can't read from server. It may not have the appropriate access-control-origin settings.":"서버로부터 읽어들일수 없습니다. access-control-origin 설정이 올바르지 않을수 있습니다.", + "Please specify the protocol for":"다음을 위한 프로토콜을 정하세요", + "Can't read swagger JSON from":"swagger JSON 을 다음으로 부터 읽을수 없습니다", + "Finished Loading Resource Information. Rendering Swagger UI":"리소스 정보 불러오기 완료. Swagger UI 랜더링", + "Unable to read api":"api를 읽을 수 없습니다.", + "from path":"다음 경로로 부터", + "server returned":"서버 응답함." +}); diff --git a/static/docs/lang/pl.js b/static/docs/lang/pl.js new file mode 100644 index 0000000..ce41e91 --- /dev/null +++ b/static/docs/lang/pl.js @@ -0,0 +1,53 @@ +'use strict'; + +/* jshint quotmark: double */ +window.SwaggerTranslator.learn({ + "Warning: Deprecated":"Uwaga: Wycofane", + "Implementation Notes":"Uwagi Implementacji", + "Response Class":"Klasa Odpowiedzi", + "Status":"Status", + "Parameters":"Parametry", + "Parameter":"Parametr", + "Value":"Wartość", + "Description":"Opis", + "Parameter Type":"Typ Parametru", + "Data Type":"Typ Danych", + "Response Messages":"Wiadomości Odpowiedzi", + "HTTP Status Code":"Kod Statusu HTTP", + "Reason":"Przyczyna", + "Response Model":"Model Odpowiedzi", + "Request URL":"URL Wywołania", + "Response Body":"Treść Odpowiedzi", + "Response Code":"Kod Odpowiedzi", + "Response Headers":"Nagłówki Odpowiedzi", + "Hide Response":"Ukryj Odpowiedź", + "Headers":"Nagłówki", + "Try it out!":"Wypróbuj!", + "Show/Hide":"Pokaż/Ukryj", + "List Operations":"Lista Operacji", + "Expand Operations":"Rozwiń Operacje", + "Raw":"Nieprzetworzone", + "can't parse JSON. Raw result":"nie można przetworzyć pliku JSON. Nieprzetworzone dane", + "Model Schema":"Schemat Modelu", + "Model":"Model", + "apply":"użyj", + "Username":"Nazwa użytkownika", + "Password":"Hasło", + "Terms of service":"Warunki używania", + "Created by":"Utworzone przez", + "See more at":"Zobacz więcej na", + "Contact the developer":"Kontakt z deweloperem", + "api version":"wersja api", + "Response Content Type":"Typ Zasobu Odpowiedzi", + "fetching resource":"ładowanie zasobu", + "fetching resource list":"ładowanie listy zasobów", + "Explore":"Eksploruj", + "Show Swagger Petstore Example Apis":"Pokaż Przykładowe Api Swagger Petstore", + "Can't read from server. It may not have the appropriate access-control-origin settings.":"Brak połączenia z serwerem. Może on nie mieć odpowiednich ustawień access-control-origin.", + "Please specify the protocol for":"Proszę podać protokół dla", + "Can't read swagger JSON from":"Nie można odczytać swagger JSON z", + "Finished Loading Resource Information. Rendering Swagger UI":"Ukończono Ładowanie Informacji o Zasobie. Renderowanie Swagger UI", + "Unable to read api":"Nie można odczytać api", + "from path":"ze ścieżki", + "server returned":"serwer zwrócił" +}); diff --git a/static/docs/lang/pt.js b/static/docs/lang/pt.js new file mode 100644 index 0000000..f2e7c13 --- /dev/null +++ b/static/docs/lang/pt.js @@ -0,0 +1,53 @@ +'use strict'; + +/* jshint quotmark: double */ +window.SwaggerTranslator.learn({ + "Warning: Deprecated":"Aviso: Depreciado", + "Implementation Notes":"Notas de Implementação", + "Response Class":"Classe de resposta", + "Status":"Status", + "Parameters":"Parâmetros", + "Parameter":"Parâmetro", + "Value":"Valor", + "Description":"Descrição", + "Parameter Type":"Tipo de parâmetro", + "Data Type":"Tipo de dados", + "Response Messages":"Mensagens de resposta", + "HTTP Status Code":"Código de status HTTP", + "Reason":"Razão", + "Response Model":"Modelo resposta", + "Request URL":"URL requisição", + "Response Body":"Corpo da resposta", + "Response Code":"Código da resposta", + "Response Headers":"Cabeçalho da resposta", + "Headers":"Cabeçalhos", + "Hide Response":"Esconder resposta", + "Try it out!":"Tente agora!", + "Show/Hide":"Mostrar/Esconder", + "List Operations":"Listar operações", + "Expand Operations":"Expandir operações", + "Raw":"Cru", + "can't parse JSON. Raw result":"Falha ao analisar JSON. Resulto cru", + "Model Schema":"Modelo esquema", + "Model":"Modelo", + "apply":"Aplicar", + "Username":"Usuário", + "Password":"Senha", + "Terms of service":"Termos do serviço", + "Created by":"Criado por", + "See more at":"Veja mais em", + "Contact the developer":"Contate o desenvolvedor", + "api version":"Versão api", + "Response Content Type":"Tipo de conteúdo da resposta", + "fetching resource":"busca recurso", + "fetching resource list":"buscando lista de recursos", + "Explore":"Explorar", + "Show Swagger Petstore Example Apis":"Show Swagger Petstore Example Apis", + "Can't read from server. It may not have the appropriate access-control-origin settings.":"Não é possível ler do servidor. Pode não ter as apropriadas configurações access-control-origin", + "Please specify the protocol for":"Por favor especifique o protocolo", + "Can't read swagger JSON from":"Não é possível ler o JSON Swagger de", + "Finished Loading Resource Information. Rendering Swagger UI":"Carregar informação de recurso finalizada. Renderizando Swagger UI", + "Unable to read api":"Não foi possível ler api", + "from path":"do caminho", + "server returned":"servidor retornou" +}); diff --git a/static/docs/lang/ru.js b/static/docs/lang/ru.js new file mode 100644 index 0000000..592744e --- /dev/null +++ b/static/docs/lang/ru.js @@ -0,0 +1,56 @@ +'use strict'; + +/* jshint quotmark: double */ +window.SwaggerTranslator.learn({ + "Warning: Deprecated":"Предупреждение: Устарело", + "Implementation Notes":"Заметки", + "Response Class":"Пример ответа", + "Status":"Статус", + "Parameters":"Параметры", + "Parameter":"Параметр", + "Value":"Значение", + "Description":"Описание", + "Parameter Type":"Тип параметра", + "Data Type":"Тип данных", + "HTTP Status Code":"HTTP код", + "Reason":"Причина", + "Response Model":"Структура ответа", + "Request URL":"URL запроса", + "Response Body":"Тело ответа", + "Response Code":"HTTP код ответа", + "Response Headers":"Заголовки ответа", + "Hide Response":"Спрятать ответ", + "Headers":"Заголовки", + "Response Messages":"Что может прийти в ответ", + "Try it out!":"Попробовать!", + "Show/Hide":"Показать/Скрыть", + "List Operations":"Операции кратко", + "Expand Operations":"Операции подробно", + "Raw":"В сыром виде", + "can't parse JSON. Raw result":"Не удается распарсить ответ:", + "Example Value":"Пример", + "Model Schema":"Структура", + "Model":"Описание", + "Click to set as parameter value":"Нажмите, чтобы испльзовать в качестве значения параметра", + "apply":"применить", + "Username":"Имя пользователя", + "Password":"Пароль", + "Terms of service":"Условия использования", + "Created by":"Разработано", + "See more at":"Еще тут", + "Contact the developer":"Связаться с разработчиком", + "api version":"Версия API", + "Response Content Type":"Content Type ответа", + "Parameter content type:":"Content Type параметра:", + "fetching resource":"Получение ресурса", + "fetching resource list":"Получение ресурсов", + "Explore":"Показать", + "Show Swagger Petstore Example Apis":"Показать примеры АПИ", + "Can't read from server. It may not have the appropriate access-control-origin settings.":"Не удается получить ответ от сервера. Возможно, проблема с настройками доступа", + "Please specify the protocol for":"Пожалуйста, укажите протокол для", + "Can't read swagger JSON from":"Не получается прочитать swagger json из", + "Finished Loading Resource Information. Rendering Swagger UI":"Загрузка информации о ресурсах завершена. Рендерим", + "Unable to read api":"Не удалось прочитать api", + "from path":"по адресу", + "server returned":"сервер сказал" +}); diff --git a/static/docs/lang/tr.js b/static/docs/lang/tr.js new file mode 100644 index 0000000..16426a9 --- /dev/null +++ b/static/docs/lang/tr.js @@ -0,0 +1,53 @@ +'use strict'; + +/* jshint quotmark: double */ +window.SwaggerTranslator.learn({ + "Warning: Deprecated":"Uyarı: Deprecated", + "Implementation Notes":"Gerçekleştirim Notları", + "Response Class":"Dönen Sınıf", + "Status":"Statü", + "Parameters":"Parametreler", + "Parameter":"Parametre", + "Value":"Değer", + "Description":"Açıklama", + "Parameter Type":"Parametre Tipi", + "Data Type":"Veri Tipi", + "Response Messages":"Dönüş Mesajı", + "HTTP Status Code":"HTTP Statü Kodu", + "Reason":"Gerekçe", + "Response Model":"Dönüş Modeli", + "Request URL":"İstek URL", + "Response Body":"Dönüş İçeriği", + "Response Code":"Dönüş Kodu", + "Response Headers":"Dönüş Üst Bilgileri", + "Hide Response":"Dönüşü Gizle", + "Headers":"Üst Bilgiler", + "Try it out!":"Dene!", + "Show/Hide":"Göster/Gizle", + "List Operations":"Operasyonları Listele", + "Expand Operations":"Operasyonları Aç", + "Raw":"Ham", + "can't parse JSON. Raw result":"JSON çözümlenemiyor. Ham sonuç", + "Model Schema":"Model Şema", + "Model":"Model", + "apply":"uygula", + "Username":"Kullanıcı Adı", + "Password":"Parola", + "Terms of service":"Servis şartları", + "Created by":"Oluşturan", + "See more at":"Daha fazlası için", + "Contact the developer":"Geliştirici ile İletişime Geçin", + "api version":"api versiyon", + "Response Content Type":"Dönüş İçerik Tipi", + "fetching resource":"kaynak getiriliyor", + "fetching resource list":"kaynak listesi getiriliyor", + "Explore":"Keşfet", + "Show Swagger Petstore Example Apis":"Swagger Petstore Örnek Api'yi Gör", + "Can't read from server. It may not have the appropriate access-control-origin settings.":"Sunucudan okuma yapılamıyor. Sunucu access-control-origin ayarlarınızı kontrol edin.", + "Please specify the protocol for":"Lütfen istenen adres için protokol belirtiniz", + "Can't read swagger JSON from":"Swagger JSON bu kaynaktan okunamıyor", + "Finished Loading Resource Information. Rendering Swagger UI":"Kaynak baglantısı tamamlandı. Swagger UI gösterime hazırlanıyor", + "Unable to read api":"api okunamadı", + "from path":"yoldan", + "server returned":"sunucuya dönüldü" +}); diff --git a/static/docs/lang/translator.js b/static/docs/lang/translator.js new file mode 100644 index 0000000..ffb879f --- /dev/null +++ b/static/docs/lang/translator.js @@ -0,0 +1,39 @@ +'use strict'; + +/** + * Translator for documentation pages. + * + * To enable translation you should include one of language-files in your index.html + * after . + * For example - + * + * If you wish to translate some new texts you should do two things: + * 1. Add a new phrase pair ("New Phrase": "New Translation") into your language file (for example lang/ru.js). It will be great if you add it in other language files too. + * 2. Mark that text it templates this way New Phrase or . + * The main thing here is attribute data-sw-translate. Only inner html, title-attribute and value-attribute are going to translate. + * + */ +window.SwaggerTranslator = { + + _words:[], + + translate: function(sel) { + var $this = this; + sel = sel || '[data-sw-translate]'; + + $(sel).each(function() { + $(this).html($this._tryTranslate($(this).html())); + + $(this).val($this._tryTranslate($(this).val())); + $(this).attr('title', $this._tryTranslate($(this).attr('title'))); + }); + }, + + _tryTranslate: function(word) { + return this._words[$.trim(word)] !== undefined ? this._words[$.trim(word)] : word; + }, + + learn: function(wordsMap) { + this._words = wordsMap; + } +}; diff --git a/static/docs/lang/zh-cn.js b/static/docs/lang/zh-cn.js new file mode 100644 index 0000000..3af61ad --- /dev/null +++ b/static/docs/lang/zh-cn.js @@ -0,0 +1,56 @@ +'use strict'; + +/* jshint quotmark: double */ +window.SwaggerTranslator.learn({ + "Warning: Deprecated":"警告:已过时", + "Implementation Notes":"实现备注", + "Response Class":"响应类", + "Status":"状态", + "Parameters":"参数", + "Parameter":"参数", + "Value":"值", + "Description":"描述", + "Parameter Type":"参数类型", + "Data Type":"数据类型", + "Response Messages":"响应消息", + "HTTP Status Code":"HTTP状态码", + "Reason":"原因", + "Response Model":"响应模型", + "Request URL":"请求URL", + "Response Body":"响应体", + "Response Code":"响应码", + "Response Headers":"响应头", + "Hide Response":"隐藏响应", + "Headers":"头", + "Try it out!":"试一下!", + "Show/Hide":"显示/隐藏", + "List Operations":"显示操作", + "Expand Operations":"展开操作", + "Raw":"原始", + "can't parse JSON. Raw result":"无法解析JSON. 原始结果", + "Example Value":"示例", + "Click to set as parameter value":"点击设置参数", + "Model Schema":"模型架构", + "Model":"模型", + "apply":"应用", + "Username":"用户名", + "Password":"密码", + "Terms of service":"服务条款", + "Created by":"创建者", + "See more at":"查看更多:", + "Contact the developer":"联系开发者", + "api version":"api版本", + "Response Content Type":"响应Content Type", + "Parameter content type:":"参数类型:", + "fetching resource":"正在获取资源", + "fetching resource list":"正在获取资源列表", + "Explore":"浏览", + "Show Swagger Petstore Example Apis":"显示 Swagger Petstore 示例 Apis", + "Can't read from server. It may not have the appropriate access-control-origin settings.":"无法从服务器读取。可能没有正确设置access-control-origin。", + "Please specify the protocol for":"请指定协议:", + "Can't read swagger JSON from":"无法读取swagger JSON于", + "Finished Loading Resource Information. Rendering Swagger UI":"已加载资源信息。正在渲染Swagger UI", + "Unable to read api":"无法读取api", + "from path":"从路径", + "server returned":"服务器返回" +}); diff --git a/static/docs/lib/backbone-min.js b/static/docs/lib/backbone-min.js new file mode 100644 index 0000000..8eff02e --- /dev/null +++ b/static/docs/lib/backbone-min.js @@ -0,0 +1 @@ +!function(t,e){if("function"==typeof define&&define.amd)define(["underscore","jquery","exports"],function(i,n,s){t.Backbone=e(t,s,i,n)});else if("undefined"!=typeof exports){var i=require("underscore");e(t,exports,i)}else t.Backbone=e(t,{},t._,t.jQuery||t.Zepto||t.ender||t.$)}(this,function(t,e,i,n){var s=t.Backbone,r=[],a=(r.push,r.slice);r.splice;e.VERSION="1.1.2",e.$=n,e.noConflict=function(){return t.Backbone=s,this},e.emulateHTTP=!1,e.emulateJSON=!1;var o=e.Events={on:function(t,e,i){if(!c(this,"on",t,[e,i])||!e)return this;this._events||(this._events={});var n=this._events[t]||(this._events[t]=[]);return n.push({callback:e,context:i,ctx:i||this}),this},once:function(t,e,n){if(!c(this,"once",t,[e,n])||!e)return this;var s=this,r=i.once(function(){s.off(t,r),e.apply(this,arguments)});return r._callback=e,this.on(t,r,n)},off:function(t,e,n){var s,r,a,o,h,u,l,d;if(!this._events||!c(this,"off",t,[e,n]))return this;if(!t&&!e&&!n)return this._events=void 0,this;for(o=t?[t]:i.keys(this._events),h=0,u=o.length;h").attr(t);this.setElement(n,!1)}}}),e.sync=function(t,n,s){var r=E[t];i.defaults(s||(s={}),{emulateHTTP:e.emulateHTTP,emulateJSON:e.emulateJSON});var a={type:r,dataType:"json"};if(s.url||(a.url=i.result(n,"url")||j()),null!=s.data||!n||"create"!==t&&"update"!==t&&"patch"!==t||(a.contentType="application/json",a.data=JSON.stringify(s.attrs||n.toJSON(s))),s.emulateJSON&&(a.contentType="application/x-www-form-urlencoded",a.data=a.data?{model:a.data}:{}),s.emulateHTTP&&("PUT"===r||"DELETE"===r||"PATCH"===r)){a.type="POST",s.emulateJSON&&(a.data._method=r);var o=s.beforeSend;s.beforeSend=function(t){if(t.setRequestHeader("X-HTTP-Method-Override",r),o)return o.apply(this,arguments)}}"GET"===a.type||s.emulateJSON||(a.processData=!1),"PATCH"===a.type&&x&&(a.xhr=function(){return new ActiveXObject("Microsoft.XMLHTTP")});var h=s.xhr=e.ajax(i.extend(a,s));return n.trigger("request",n,h,s),h};var x=!("undefined"==typeof window||!window.ActiveXObject||window.XMLHttpRequest&&(new XMLHttpRequest).dispatchEvent),E={create:"POST",update:"PUT",patch:"PATCH","delete":"DELETE",read:"GET"};e.ajax=function(){return e.$.ajax.apply(e.$,arguments)};var k=e.Router=function(t){t||(t={}),t.routes&&(this.routes=t.routes),this._bindRoutes(),this.initialize.apply(this,arguments)},T=/\((.*?)\)/g,$=/(\(\?)?:\w+/g,S=/\*\w+/g,H=/[\-{}\[\]+?.,\\\^$|#\s]/g;i.extend(k.prototype,o,{initialize:function(){},route:function(t,n,s){i.isRegExp(t)||(t=this._routeToRegExp(t)),i.isFunction(n)&&(s=n,n=""),s||(s=this[n]);var r=this;return e.history.route(t,function(i){var a=r._extractParameters(t,i);r.execute(s,a),r.trigger.apply(r,["route:"+n].concat(a)),r.trigger("route",n,a),e.history.trigger("route",r,n,a)}),this},execute:function(t,e){t&&t.apply(this,e)},navigate:function(t,i){return e.history.navigate(t,i),this},_bindRoutes:function(){if(this.routes){this.routes=i.result(this,"routes");for(var t,e=i.keys(this.routes);null!=(t=e.pop());)this.route(t,this.routes[t])}},_routeToRegExp:function(t){return t=t.replace(H,"\\$&").replace(T,"(?:$1)?").replace($,function(t,e){return e?t:"([^/?]+)"}).replace(S,"([^?]*?)"),new RegExp("^"+t+"(?:\\?([\\s\\S]*))?$")},_extractParameters:function(t,e){var n=t.exec(e).slice(1);return i.map(n,function(t,e){return e===n.length-1?t||null:t?decodeURIComponent(t):null})}});var A=e.History=function(){this.handlers=[],i.bindAll(this,"checkUrl"),"undefined"!=typeof window&&(this.location=window.location,this.history=window.history)},I=/^[#\/]|\s+$/g,N=/^\/+|\/+$/g,R=/msie [\w.]+/,O=/\/$/,P=/#.*$/;A.started=!1,i.extend(A.prototype,o,{interval:50,atRoot:function(){return this.location.pathname.replace(/[^\/]$/,"$&/")===this.root},getHash:function(t){var e=(t||this).location.href.match(/#(.*)$/);return e?e[1]:""},getFragment:function(t,e){if(null==t)if(this._hasPushState||!this._wantsHashChange||e){t=decodeURI(this.location.pathname+this.location.search);var i=this.root.replace(O,"");t.indexOf(i)||(t=t.slice(i.length))}else t=this.getHash();return t.replace(I,"")},start:function(t){if(A.started)throw new Error("Backbone.history has already been started");A.started=!0,this.options=i.extend({root:"/"},this.options,t),this.root=this.options.root,this._wantsHashChange=this.options.hashChange!==!1,this._wantsPushState=!!this.options.pushState,this._hasPushState=!!(this.options.pushState&&this.history&&this.history.pushState);var n=this.getFragment(),s=document.documentMode,r=R.exec(navigator.userAgent.toLowerCase())&&(!s||s<=7);if(this.root=("/"+this.root+"/").replace(N,"/"),r&&this._wantsHashChange){var a=e.$('