├── .github └── FUNDING.yml ├── .gitignore ├── LICENSE ├── README.md ├── index.js ├── package-lock.json ├── package.json └── src ├── app.js ├── config.js ├── core ├── images.js ├── servers.js └── sessions.js ├── database ├── index.js ├── postgresql.js └── sqlite.js ├── routes ├── api.js ├── index.js ├── proxy.js ├── resize.js └── transcode.js ├── store ├── index.js ├── local.js └── redis.js └── utils.js /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | ko_fi: UnicornTranscoder 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 UnicornTranscoder 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](https://raw.githubusercontent.com/UnicornTranscoder/Logo/master/transparent.png) 2 | ## UnicornLoadBalancer 3 | 4 | This software is a part of __UnicornTranscoder__ project, it's the LoadBalancer that will catch Plex requests and send them to a __UnicornTranscoder__. 5 | 6 | ## UnicornTranscoder Project 7 | 8 | * [UnicornTranscoder](https://github.com/UnicornTranscoder/UnicornTranscoder) 9 | * [UnicornLoadBalancer](https://github.com/UnicornTranscoder/UnicornLoadBalancer) 10 | * [UnicornFFMPEG](https://github.com/UnicornTranscoder/UnicornFFMPEG) 11 | 12 | ## Support us! 13 | 14 | The UnicornTranscoder project is an open source software, maintained by @drouarb and @Maxou44. If you want to support us, you can tip us on Ko-fi: https://ko-fi.com/unicorntranscoder ☕ 15 | 16 | ## Dependencies 17 | 18 | * Plex Media Server 19 | * NodeJS 20 | * RedisCache (Optionnal) 21 | * Postgresql (Optionnal) 22 | 23 | ## Setup 24 | 25 | ### 1. Installation 26 | 27 | * Clone the repository 28 | * Install with `npm install` 29 | * Setup some environnement variables to configure the *UnicornLoadBlancer* 30 | * Type `npm start` to launch the load-balancer 31 | 32 | | Name | Description | Type | Default | 33 | | ----------------- | ------------------------------------------------------------ | ------| ------- | 34 | | **SERVER_PORT** | Port used by the *UnicornLoadBalancer* | `int` | `3001` | 35 | | **SERVER_PUBLIC** | Public url where the *UnicornLoadBalancer* can be called, **with** a slash at the end | `string` | `http://127.0.0.1:3001/` | 36 | | **PLEX_HOST** | Host to access to Plex | `string` | `127.0.0.1` | 37 | | **PLEX_PORT** | Port used by Plex | `int` | `32400` | 38 | | **PLEX_PATH_USR** | The Plex's path | `string` | `/usr/lib/plexmediaserver/` | 39 | | **PLEX_PATH_SESSIONS** | The path where Plex store sessions (to grab external subtitles) | `string` | `/var/lib/plexmediaserver/Library/Application Support/Plex Media Server/Cache/Transcode/Sessions` | 40 | | **DATABASE_MODE** | Kind of database to use with Plex, can be `sqlite` or `postgresql` | `string` | `sqlite` | 41 | | **DATABASE_SQLITE_PATH** | The path of the Plex database | `string` | `/var/lib/plexmediaserver/Library/Application Support/Plex Media Server/Plug-in Support/Databases/com.plexapp.plugins.library.db` | 42 | | **DATABASE_POSTGRESQL_HOST** | Host of the Postgresql server | `string` | ` ` | 43 | | **DATABASE_POSTGRESQL_DATABASE** | Name of the postgresql database | `string` | ` ` | 44 | | **DATABASE_POSTGRESQL_USER** | User used by the Postgresql database| `string` | ` ` | 45 | | **DATABASE_POSTGRESQL_PASSWORD** | Password used by the Postgresql database | `string` | ` ` | 46 | | **DATABASE_POSTGRESQL_PORT** | Port used by the Postgresql database | `int` | `5432` | 47 | | **REDIS_HOST** | The host of the redis database | `string` `undefined` | `undefined` | 48 | | **REDIS_PORT** | Port used by Redis | `int` | `6379` | 49 | | **REDIS_PASSWORD** | The password of the redis database | `string` | ` ` | 50 | | **REDIS_DB** | The index of the redis database | `int` | `0` | 51 | | **CUSTOM_SCORES_TIMEOUT** | Seconds to consider a not-pinged server as unavailable | `int` | `10` | 52 | | **CUSTOM_IMAGE_PROXY** | URL of an alternative server to convert images, this proxy must be compliant with the *images.weserv.nl* API (original or self-hosted). Eg supported value: `https://images.weserv.nl/` | `string` | ` ` | 53 | | **CUSTOM_DOWNLOAD_FORWARD** | Enable or disable 302 for download links and direct play, if enabled, transcoders need to have access to media files | `bool` | `false` | 54 | | **CUSTOM_SERVERS_LIST** | Transcoder servers set by default, **with** a slash at the end, separate servers with a **comma** | `string array` | `[]` | 55 | 56 | * Configure Plex Media Server access address 57 | * In Settings -> Server -> Network 58 | * Set `Custom server access URLs` to the address to access the UnicornLoadBalancer 59 | * Run with npm start 60 | 61 | ### 2. Notes 62 | 63 | All requests to the Plex Media Server should pass through the *UnicornLoadBalancer*, if someone reach the server directly he will not be able to start a stream, since FFMPEG binary has been replaced. To solve this problem it is recomended to configure an iptable rule to drop direct access from port **32400**. You also need to disable the relay feature in Plex settings ***AND*** remove the **Plex Relay** binary. 64 | It is also recomended to setup a nginx reverse proxy in front of the *UnicornLoadBalancer* to setup a SSL certificate. 65 | 66 | ``` 67 | #Example iptable: Deny 32400 port 68 | iptables -A INPUT -p tcp --dport 32400 -i [your-network-interface] -j DROP 69 | ``` 70 | 71 | After a 32400 port drop, you must define your server url and the port used (443 or 80) in Plex settings, if it's not configured you will not be able to access to your server from https://app.plex.tv 72 | 73 | ### Disclamer 74 | 75 | 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. 76 | 77 | __Pull Requests are welcome 😉__ 78 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // This file is the ES6 module loader, don't edit it, go to src/app.js 2 | require = require("esm")(module/*, options*/) 3 | module.exports = require("./src/app.js") 4 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "unicorn-load-balancer", 3 | "version": "2.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "abbrev": { 8 | "version": "1.1.1", 9 | "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", 10 | "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" 11 | }, 12 | "accepts": { 13 | "version": "1.3.7", 14 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", 15 | "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", 16 | "requires": { 17 | "mime-types": "~2.1.24", 18 | "negotiator": "0.6.2" 19 | } 20 | }, 21 | "ajv": { 22 | "version": "6.12.0", 23 | "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.0.tgz", 24 | "integrity": "sha512-D6gFiFA0RRLyUbvijN74DWAjXSFxWKaWP7mldxkVhyhAV3+SWA9HEJPHQ2c9soIeTFJqcSdFDGFgdqs1iUU2Hw==", 25 | "requires": { 26 | "fast-deep-equal": "^3.1.1", 27 | "fast-json-stable-stringify": "^2.0.0", 28 | "json-schema-traverse": "^0.4.1", 29 | "uri-js": "^4.2.2" 30 | } 31 | }, 32 | "ansi-regex": { 33 | "version": "2.1.1", 34 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", 35 | "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" 36 | }, 37 | "aproba": { 38 | "version": "1.2.0", 39 | "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", 40 | "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" 41 | }, 42 | "are-we-there-yet": { 43 | "version": "1.1.5", 44 | "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", 45 | "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", 46 | "requires": { 47 | "delegates": "^1.0.0", 48 | "readable-stream": "^2.0.6" 49 | } 50 | }, 51 | "array-flatten": { 52 | "version": "1.1.1", 53 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", 54 | "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" 55 | }, 56 | "asn1": { 57 | "version": "0.2.4", 58 | "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", 59 | "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", 60 | "requires": { 61 | "safer-buffer": "~2.1.0" 62 | } 63 | }, 64 | "assert-plus": { 65 | "version": "1.0.0", 66 | "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", 67 | "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" 68 | }, 69 | "asynckit": { 70 | "version": "0.4.0", 71 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", 72 | "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" 73 | }, 74 | "aws-sign2": { 75 | "version": "0.7.0", 76 | "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", 77 | "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" 78 | }, 79 | "aws4": { 80 | "version": "1.9.1", 81 | "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.9.1.tgz", 82 | "integrity": "sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug==" 83 | }, 84 | "balanced-match": { 85 | "version": "1.0.0", 86 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", 87 | "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" 88 | }, 89 | "bcrypt-pbkdf": { 90 | "version": "1.0.2", 91 | "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", 92 | "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", 93 | "requires": { 94 | "tweetnacl": "^0.14.3" 95 | } 96 | }, 97 | "body-parser": { 98 | "version": "1.19.0", 99 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", 100 | "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", 101 | "requires": { 102 | "bytes": "3.1.0", 103 | "content-type": "~1.0.4", 104 | "debug": "2.6.9", 105 | "depd": "~1.1.2", 106 | "http-errors": "1.7.2", 107 | "iconv-lite": "0.4.24", 108 | "on-finished": "~2.3.0", 109 | "qs": "6.7.0", 110 | "raw-body": "2.4.0", 111 | "type-is": "~1.6.17" 112 | }, 113 | "dependencies": { 114 | "debug": { 115 | "version": "2.6.9", 116 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 117 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 118 | "requires": { 119 | "ms": "2.0.0" 120 | } 121 | }, 122 | "ms": { 123 | "version": "2.0.0", 124 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 125 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 126 | } 127 | } 128 | }, 129 | "brace-expansion": { 130 | "version": "1.1.11", 131 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 132 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 133 | "requires": { 134 | "balanced-match": "^1.0.0", 135 | "concat-map": "0.0.1" 136 | } 137 | }, 138 | "buffer-writer": { 139 | "version": "2.0.0", 140 | "resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz", 141 | "integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==" 142 | }, 143 | "bytes": { 144 | "version": "3.1.0", 145 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", 146 | "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" 147 | }, 148 | "caseless": { 149 | "version": "0.12.0", 150 | "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", 151 | "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" 152 | }, 153 | "charenc": { 154 | "version": "0.0.2", 155 | "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", 156 | "integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc=" 157 | }, 158 | "chownr": { 159 | "version": "1.1.4", 160 | "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", 161 | "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" 162 | }, 163 | "code-point-at": { 164 | "version": "1.1.0", 165 | "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", 166 | "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" 167 | }, 168 | "color": { 169 | "version": "3.1.2", 170 | "resolved": "https://registry.npmjs.org/color/-/color-3.1.2.tgz", 171 | "integrity": "sha512-vXTJhHebByxZn3lDvDJYw4lR5+uB3vuoHsuYA5AKuxRVn5wzzIfQKGLBmgdVRHKTJYeK5rvJcHnrd0Li49CFpg==", 172 | "requires": { 173 | "color-convert": "^1.9.1", 174 | "color-string": "^1.5.2" 175 | } 176 | }, 177 | "color-convert": { 178 | "version": "1.9.3", 179 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", 180 | "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", 181 | "requires": { 182 | "color-name": "1.1.3" 183 | } 184 | }, 185 | "color-name": { 186 | "version": "1.1.3", 187 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", 188 | "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" 189 | }, 190 | "color-string": { 191 | "version": "1.5.3", 192 | "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.3.tgz", 193 | "integrity": "sha512-dC2C5qeWoYkxki5UAXapdjqO672AM4vZuPGRQfO8b5HKuKGBbKWpITyDYN7TOFKvRW7kOgAn3746clDBMDJyQw==", 194 | "requires": { 195 | "color-name": "^1.0.0", 196 | "simple-swizzle": "^0.2.2" 197 | } 198 | }, 199 | "combined-stream": { 200 | "version": "1.0.8", 201 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", 202 | "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", 203 | "requires": { 204 | "delayed-stream": "~1.0.0" 205 | } 206 | }, 207 | "concat-map": { 208 | "version": "0.0.1", 209 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 210 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" 211 | }, 212 | "console-control-strings": { 213 | "version": "1.1.0", 214 | "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", 215 | "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" 216 | }, 217 | "content-disposition": { 218 | "version": "0.5.3", 219 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", 220 | "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", 221 | "requires": { 222 | "safe-buffer": "5.1.2" 223 | } 224 | }, 225 | "content-type": { 226 | "version": "1.0.4", 227 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", 228 | "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" 229 | }, 230 | "cookie": { 231 | "version": "0.4.0", 232 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", 233 | "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" 234 | }, 235 | "cookie-signature": { 236 | "version": "1.0.6", 237 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", 238 | "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" 239 | }, 240 | "core-util-is": { 241 | "version": "1.0.2", 242 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", 243 | "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" 244 | }, 245 | "cors": { 246 | "version": "2.8.5", 247 | "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", 248 | "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", 249 | "requires": { 250 | "object-assign": "^4", 251 | "vary": "^1" 252 | } 253 | }, 254 | "cross-env": { 255 | "version": "6.0.3", 256 | "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-6.0.3.tgz", 257 | "integrity": "sha512-+KqxF6LCvfhWvADcDPqo64yVIB31gv/jQulX2NGzKS/g3GEVz6/pt4wjHFtFWsHMddebWD/sDthJemzM4MaAag==", 258 | "requires": { 259 | "cross-spawn": "^7.0.0" 260 | } 261 | }, 262 | "cross-spawn": { 263 | "version": "7.0.1", 264 | "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.1.tgz", 265 | "integrity": "sha512-u7v4o84SwFpD32Z8IIcPZ6z1/ie24O6RU3RbtL5Y316l3KuHVPx9ItBgWQ6VlfAFnRnTtMUrsQ9MUUTuEZjogg==", 266 | "requires": { 267 | "path-key": "^3.1.0", 268 | "shebang-command": "^2.0.0", 269 | "which": "^2.0.1" 270 | } 271 | }, 272 | "crypt": { 273 | "version": "0.0.2", 274 | "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", 275 | "integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs=" 276 | }, 277 | "dashdash": { 278 | "version": "1.14.1", 279 | "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", 280 | "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", 281 | "requires": { 282 | "assert-plus": "^1.0.0" 283 | } 284 | }, 285 | "debug": { 286 | "version": "4.1.1", 287 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", 288 | "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", 289 | "requires": { 290 | "ms": "^2.1.1" 291 | } 292 | }, 293 | "decode-uri-component": { 294 | "version": "0.2.0", 295 | "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", 296 | "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=" 297 | }, 298 | "deep-extend": { 299 | "version": "0.6.0", 300 | "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", 301 | "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" 302 | }, 303 | "delayed-stream": { 304 | "version": "1.0.0", 305 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", 306 | "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" 307 | }, 308 | "delegates": { 309 | "version": "1.0.0", 310 | "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", 311 | "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" 312 | }, 313 | "depd": { 314 | "version": "1.1.2", 315 | "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", 316 | "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" 317 | }, 318 | "destroy": { 319 | "version": "1.0.4", 320 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", 321 | "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" 322 | }, 323 | "detect-browser": { 324 | "version": "4.8.0", 325 | "resolved": "https://registry.npmjs.org/detect-browser/-/detect-browser-4.8.0.tgz", 326 | "integrity": "sha512-f4h2dFgzHUIpjpBLjhnDIteXv8VQiUm8XzAuzQtYUqECX/eKh67ykuiVoyb7Db7a0PUSmJa3OGXStG0CbQFUVw==" 327 | }, 328 | "detect-libc": { 329 | "version": "1.0.3", 330 | "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", 331 | "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=" 332 | }, 333 | "double-ended-queue": { 334 | "version": "2.1.0-0", 335 | "resolved": "https://registry.npmjs.org/double-ended-queue/-/double-ended-queue-2.1.0-0.tgz", 336 | "integrity": "sha1-ED01J/0xUo9AGIEwyEHv3XgmTlw=" 337 | }, 338 | "ecc-jsbn": { 339 | "version": "0.1.2", 340 | "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", 341 | "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", 342 | "requires": { 343 | "jsbn": "~0.1.0", 344 | "safer-buffer": "^2.1.0" 345 | } 346 | }, 347 | "ee-first": { 348 | "version": "1.1.1", 349 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 350 | "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" 351 | }, 352 | "encodeurl": { 353 | "version": "1.0.2", 354 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", 355 | "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" 356 | }, 357 | "escape-html": { 358 | "version": "1.0.3", 359 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 360 | "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" 361 | }, 362 | "esm": { 363 | "version": "3.2.25", 364 | "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz", 365 | "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==" 366 | }, 367 | "etag": { 368 | "version": "1.8.1", 369 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", 370 | "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" 371 | }, 372 | "eventemitter3": { 373 | "version": "4.0.0", 374 | "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.0.tgz", 375 | "integrity": "sha512-qerSRB0p+UDEssxTtm6EDKcE7W4OaoisfIMl4CngyEhjpYglocpNg6UEqCvemdGhosAsg4sO2dXJOdyBifPGCg==" 376 | }, 377 | "express": { 378 | "version": "4.17.1", 379 | "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", 380 | "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", 381 | "requires": { 382 | "accepts": "~1.3.7", 383 | "array-flatten": "1.1.1", 384 | "body-parser": "1.19.0", 385 | "content-disposition": "0.5.3", 386 | "content-type": "~1.0.4", 387 | "cookie": "0.4.0", 388 | "cookie-signature": "1.0.6", 389 | "debug": "2.6.9", 390 | "depd": "~1.1.2", 391 | "encodeurl": "~1.0.2", 392 | "escape-html": "~1.0.3", 393 | "etag": "~1.8.1", 394 | "finalhandler": "~1.1.2", 395 | "fresh": "0.5.2", 396 | "merge-descriptors": "1.0.1", 397 | "methods": "~1.1.2", 398 | "on-finished": "~2.3.0", 399 | "parseurl": "~1.3.3", 400 | "path-to-regexp": "0.1.7", 401 | "proxy-addr": "~2.0.5", 402 | "qs": "6.7.0", 403 | "range-parser": "~1.2.1", 404 | "safe-buffer": "5.1.2", 405 | "send": "0.17.1", 406 | "serve-static": "1.14.1", 407 | "setprototypeof": "1.1.1", 408 | "statuses": "~1.5.0", 409 | "type-is": "~1.6.18", 410 | "utils-merge": "1.0.1", 411 | "vary": "~1.1.2" 412 | }, 413 | "dependencies": { 414 | "debug": { 415 | "version": "2.6.9", 416 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 417 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 418 | "requires": { 419 | "ms": "2.0.0" 420 | } 421 | }, 422 | "ms": { 423 | "version": "2.0.0", 424 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 425 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 426 | } 427 | } 428 | }, 429 | "extend": { 430 | "version": "3.0.2", 431 | "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", 432 | "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" 433 | }, 434 | "extsprintf": { 435 | "version": "1.3.0", 436 | "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", 437 | "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" 438 | }, 439 | "fast-deep-equal": { 440 | "version": "3.1.1", 441 | "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz", 442 | "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==" 443 | }, 444 | "fast-json-stable-stringify": { 445 | "version": "2.1.0", 446 | "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", 447 | "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" 448 | }, 449 | "finalhandler": { 450 | "version": "1.1.2", 451 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", 452 | "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", 453 | "requires": { 454 | "debug": "2.6.9", 455 | "encodeurl": "~1.0.2", 456 | "escape-html": "~1.0.3", 457 | "on-finished": "~2.3.0", 458 | "parseurl": "~1.3.3", 459 | "statuses": "~1.5.0", 460 | "unpipe": "~1.0.0" 461 | }, 462 | "dependencies": { 463 | "debug": { 464 | "version": "2.6.9", 465 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 466 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 467 | "requires": { 468 | "ms": "2.0.0" 469 | } 470 | }, 471 | "ms": { 472 | "version": "2.0.0", 473 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 474 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 475 | } 476 | } 477 | }, 478 | "follow-redirects": { 479 | "version": "1.10.0", 480 | "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.10.0.tgz", 481 | "integrity": "sha512-4eyLK6s6lH32nOvLLwlIOnr9zrL8Sm+OvW4pVTJNoXeGzYIkHVf+pADQi+OJ0E67hiuSLezPVPyBcIZO50TmmQ==", 482 | "requires": { 483 | "debug": "^3.0.0" 484 | }, 485 | "dependencies": { 486 | "debug": { 487 | "version": "3.2.6", 488 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", 489 | "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", 490 | "requires": { 491 | "ms": "^2.1.1" 492 | } 493 | } 494 | } 495 | }, 496 | "forever-agent": { 497 | "version": "0.6.1", 498 | "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", 499 | "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" 500 | }, 501 | "form-data": { 502 | "version": "2.3.3", 503 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", 504 | "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", 505 | "requires": { 506 | "asynckit": "^0.4.0", 507 | "combined-stream": "^1.0.6", 508 | "mime-types": "^2.1.12" 509 | } 510 | }, 511 | "forwarded": { 512 | "version": "0.1.2", 513 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", 514 | "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" 515 | }, 516 | "fresh": { 517 | "version": "0.5.2", 518 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", 519 | "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" 520 | }, 521 | "fs-minipass": { 522 | "version": "1.2.7", 523 | "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz", 524 | "integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==", 525 | "requires": { 526 | "minipass": "^2.6.0" 527 | } 528 | }, 529 | "fs.realpath": { 530 | "version": "1.0.0", 531 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 532 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" 533 | }, 534 | "gauge": { 535 | "version": "2.7.4", 536 | "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", 537 | "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", 538 | "requires": { 539 | "aproba": "^1.0.3", 540 | "console-control-strings": "^1.0.0", 541 | "has-unicode": "^2.0.0", 542 | "object-assign": "^4.1.0", 543 | "signal-exit": "^3.0.0", 544 | "string-width": "^1.0.1", 545 | "strip-ansi": "^3.0.1", 546 | "wide-align": "^1.1.0" 547 | } 548 | }, 549 | "getenv": { 550 | "version": "1.0.0", 551 | "resolved": "https://registry.npmjs.org/getenv/-/getenv-1.0.0.tgz", 552 | "integrity": "sha512-7yetJWqbS9sbn0vIfliPsFgoXMKn/YMF+Wuiog97x+urnSRRRZ7xB+uVkwGKzRgq9CDFfMQnE9ruL5DHv9c6Xg==" 553 | }, 554 | "getpass": { 555 | "version": "0.1.7", 556 | "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", 557 | "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", 558 | "requires": { 559 | "assert-plus": "^1.0.0" 560 | } 561 | }, 562 | "glob": { 563 | "version": "7.1.6", 564 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", 565 | "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", 566 | "requires": { 567 | "fs.realpath": "^1.0.0", 568 | "inflight": "^1.0.4", 569 | "inherits": "2", 570 | "minimatch": "^3.0.4", 571 | "once": "^1.3.0", 572 | "path-is-absolute": "^1.0.0" 573 | } 574 | }, 575 | "har-schema": { 576 | "version": "2.0.0", 577 | "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", 578 | "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" 579 | }, 580 | "har-validator": { 581 | "version": "5.1.3", 582 | "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", 583 | "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", 584 | "requires": { 585 | "ajv": "^6.5.5", 586 | "har-schema": "^2.0.0" 587 | } 588 | }, 589 | "has-unicode": { 590 | "version": "2.0.1", 591 | "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", 592 | "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" 593 | }, 594 | "http-errors": { 595 | "version": "1.7.2", 596 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", 597 | "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", 598 | "requires": { 599 | "depd": "~1.1.2", 600 | "inherits": "2.0.3", 601 | "setprototypeof": "1.1.1", 602 | "statuses": ">= 1.5.0 < 2", 603 | "toidentifier": "1.0.0" 604 | } 605 | }, 606 | "http-proxy": { 607 | "version": "1.18.0", 608 | "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.0.tgz", 609 | "integrity": "sha512-84I2iJM/n1d4Hdgc6y2+qY5mDaz2PUVjlg9znE9byl+q0uC3DeByqBGReQu5tpLK0TAqTIXScRUV+dg7+bUPpQ==", 610 | "requires": { 611 | "eventemitter3": "^4.0.0", 612 | "follow-redirects": "^1.0.0", 613 | "requires-port": "^1.0.0" 614 | } 615 | }, 616 | "http-signature": { 617 | "version": "1.2.0", 618 | "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", 619 | "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", 620 | "requires": { 621 | "assert-plus": "^1.0.0", 622 | "jsprim": "^1.2.2", 623 | "sshpk": "^1.7.0" 624 | } 625 | }, 626 | "iconv-lite": { 627 | "version": "0.4.24", 628 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", 629 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", 630 | "requires": { 631 | "safer-buffer": ">= 2.1.2 < 3" 632 | } 633 | }, 634 | "ignore-walk": { 635 | "version": "3.0.3", 636 | "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.3.tgz", 637 | "integrity": "sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw==", 638 | "requires": { 639 | "minimatch": "^3.0.4" 640 | } 641 | }, 642 | "inflight": { 643 | "version": "1.0.6", 644 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 645 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 646 | "requires": { 647 | "once": "^1.3.0", 648 | "wrappy": "1" 649 | } 650 | }, 651 | "inherits": { 652 | "version": "2.0.3", 653 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 654 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" 655 | }, 656 | "ini": { 657 | "version": "1.3.5", 658 | "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", 659 | "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==" 660 | }, 661 | "ipaddr.js": { 662 | "version": "1.9.1", 663 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", 664 | "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" 665 | }, 666 | "is-arrayish": { 667 | "version": "0.3.2", 668 | "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", 669 | "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" 670 | }, 671 | "is-buffer": { 672 | "version": "1.1.6", 673 | "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", 674 | "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" 675 | }, 676 | "is-fullwidth-code-point": { 677 | "version": "1.0.0", 678 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", 679 | "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", 680 | "requires": { 681 | "number-is-nan": "^1.0.0" 682 | } 683 | }, 684 | "is-typedarray": { 685 | "version": "1.0.0", 686 | "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", 687 | "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" 688 | }, 689 | "isarray": { 690 | "version": "1.0.0", 691 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", 692 | "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" 693 | }, 694 | "isexe": { 695 | "version": "2.0.0", 696 | "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 697 | "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" 698 | }, 699 | "isstream": { 700 | "version": "0.1.2", 701 | "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", 702 | "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" 703 | }, 704 | "jsbn": { 705 | "version": "0.1.1", 706 | "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", 707 | "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" 708 | }, 709 | "json-schema": { 710 | "version": "0.2.3", 711 | "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", 712 | "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" 713 | }, 714 | "json-schema-traverse": { 715 | "version": "0.4.1", 716 | "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", 717 | "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" 718 | }, 719 | "json-stringify-safe": { 720 | "version": "5.0.1", 721 | "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", 722 | "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" 723 | }, 724 | "jsprim": { 725 | "version": "1.4.1", 726 | "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", 727 | "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", 728 | "requires": { 729 | "assert-plus": "1.0.0", 730 | "extsprintf": "1.3.0", 731 | "json-schema": "0.2.3", 732 | "verror": "1.10.0" 733 | } 734 | }, 735 | "md5": { 736 | "version": "2.2.1", 737 | "resolved": "https://registry.npmjs.org/md5/-/md5-2.2.1.tgz", 738 | "integrity": "sha1-U6s41f48iJG6RlMp6iP6wFQBJvk=", 739 | "requires": { 740 | "charenc": "~0.0.1", 741 | "crypt": "~0.0.1", 742 | "is-buffer": "~1.1.1" 743 | } 744 | }, 745 | "media-typer": { 746 | "version": "0.3.0", 747 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", 748 | "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" 749 | }, 750 | "merge-descriptors": { 751 | "version": "1.0.1", 752 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", 753 | "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" 754 | }, 755 | "methods": { 756 | "version": "1.1.2", 757 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", 758 | "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" 759 | }, 760 | "mime": { 761 | "version": "1.6.0", 762 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", 763 | "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" 764 | }, 765 | "mime-db": { 766 | "version": "1.43.0", 767 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.43.0.tgz", 768 | "integrity": "sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ==" 769 | }, 770 | "mime-types": { 771 | "version": "2.1.26", 772 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.26.tgz", 773 | "integrity": "sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ==", 774 | "requires": { 775 | "mime-db": "1.43.0" 776 | } 777 | }, 778 | "minimatch": { 779 | "version": "3.0.4", 780 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 781 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 782 | "requires": { 783 | "brace-expansion": "^1.1.7" 784 | } 785 | }, 786 | "minimist": { 787 | "version": "1.2.5", 788 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", 789 | "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" 790 | }, 791 | "minipass": { 792 | "version": "2.9.0", 793 | "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz", 794 | "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", 795 | "requires": { 796 | "safe-buffer": "^5.1.2", 797 | "yallist": "^3.0.0" 798 | } 799 | }, 800 | "minizlib": { 801 | "version": "1.3.3", 802 | "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz", 803 | "integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==", 804 | "requires": { 805 | "minipass": "^2.9.0" 806 | } 807 | }, 808 | "mkdirp": { 809 | "version": "1.0.3", 810 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.3.tgz", 811 | "integrity": "sha512-6uCP4Qc0sWsgMLy1EOqqS/3rjDHOEnsStVr/4vtAIK2Y5i2kA7lFFejYrpIyiN9w0pYf4ckeCYT9f1r1P9KX5g==" 812 | }, 813 | "ms": { 814 | "version": "2.1.2", 815 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 816 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" 817 | }, 818 | "nan": { 819 | "version": "2.14.0", 820 | "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", 821 | "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==" 822 | }, 823 | "needle": { 824 | "version": "2.3.3", 825 | "resolved": "https://registry.npmjs.org/needle/-/needle-2.3.3.tgz", 826 | "integrity": "sha512-EkY0GeSq87rWp1hoq/sH/wnTWgFVhYlnIkbJ0YJFfRgEFlz2RraCjBpFQ+vrEgEdp0ThfyHADmkChEhcb7PKyw==", 827 | "requires": { 828 | "debug": "^3.2.6", 829 | "iconv-lite": "^0.4.4", 830 | "sax": "^1.2.4" 831 | }, 832 | "dependencies": { 833 | "debug": { 834 | "version": "3.2.6", 835 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", 836 | "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", 837 | "requires": { 838 | "ms": "^2.1.1" 839 | } 840 | } 841 | } 842 | }, 843 | "negotiator": { 844 | "version": "0.6.2", 845 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", 846 | "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" 847 | }, 848 | "node-fetch": { 849 | "version": "2.6.0", 850 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz", 851 | "integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==" 852 | }, 853 | "node-pre-gyp": { 854 | "version": "0.13.0", 855 | "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.13.0.tgz", 856 | "integrity": "sha512-Md1D3xnEne8b/HGVQkZZwV27WUi1ZRuZBij24TNaZwUPU3ZAFtvT6xxJGaUVillfmMKnn5oD1HoGsp2Ftik7SQ==", 857 | "requires": { 858 | "detect-libc": "^1.0.2", 859 | "mkdirp": "^0.5.1", 860 | "needle": "^2.2.1", 861 | "nopt": "^4.0.1", 862 | "npm-packlist": "^1.1.6", 863 | "npmlog": "^4.0.2", 864 | "rc": "^1.2.7", 865 | "rimraf": "^2.6.1", 866 | "semver": "^5.3.0", 867 | "tar": "^4" 868 | }, 869 | "dependencies": { 870 | "mkdirp": { 871 | "version": "0.5.3", 872 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.3.tgz", 873 | "integrity": "sha512-P+2gwrFqx8lhew375MQHHeTlY8AuOJSrGf0R5ddkEndUkmwpgUob/vQuBD1V22/Cw1/lJr4x+EjllSezBThzBg==", 874 | "requires": { 875 | "minimist": "^1.2.5" 876 | } 877 | } 878 | } 879 | }, 880 | "nopt": { 881 | "version": "4.0.3", 882 | "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz", 883 | "integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==", 884 | "requires": { 885 | "abbrev": "1", 886 | "osenv": "^0.1.4" 887 | } 888 | }, 889 | "npm-bundled": { 890 | "version": "1.1.1", 891 | "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.1.tgz", 892 | "integrity": "sha512-gqkfgGePhTpAEgUsGEgcq1rqPXA+tv/aVBlgEzfXwA1yiUJF7xtEt3CtVwOjNYQOVknDk0F20w58Fnm3EtG0fA==", 893 | "requires": { 894 | "npm-normalize-package-bin": "^1.0.1" 895 | } 896 | }, 897 | "npm-normalize-package-bin": { 898 | "version": "1.0.1", 899 | "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz", 900 | "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==" 901 | }, 902 | "npm-packlist": { 903 | "version": "1.4.8", 904 | "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.8.tgz", 905 | "integrity": "sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A==", 906 | "requires": { 907 | "ignore-walk": "^3.0.1", 908 | "npm-bundled": "^1.0.1", 909 | "npm-normalize-package-bin": "^1.0.1" 910 | } 911 | }, 912 | "npmlog": { 913 | "version": "4.1.2", 914 | "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", 915 | "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", 916 | "requires": { 917 | "are-we-there-yet": "~1.1.2", 918 | "console-control-strings": "~1.1.0", 919 | "gauge": "~2.7.3", 920 | "set-blocking": "~2.0.0" 921 | } 922 | }, 923 | "number-is-nan": { 924 | "version": "1.0.1", 925 | "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", 926 | "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" 927 | }, 928 | "oauth-sign": { 929 | "version": "0.9.0", 930 | "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", 931 | "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" 932 | }, 933 | "object-assign": { 934 | "version": "4.1.1", 935 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 936 | "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" 937 | }, 938 | "on-finished": { 939 | "version": "2.3.0", 940 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", 941 | "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", 942 | "requires": { 943 | "ee-first": "1.1.1" 944 | } 945 | }, 946 | "once": { 947 | "version": "1.4.0", 948 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 949 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 950 | "requires": { 951 | "wrappy": "1" 952 | } 953 | }, 954 | "os-homedir": { 955 | "version": "1.0.2", 956 | "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", 957 | "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" 958 | }, 959 | "os-tmpdir": { 960 | "version": "1.0.2", 961 | "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", 962 | "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" 963 | }, 964 | "osenv": { 965 | "version": "0.1.5", 966 | "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", 967 | "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", 968 | "requires": { 969 | "os-homedir": "^1.0.0", 970 | "os-tmpdir": "^1.0.0" 971 | } 972 | }, 973 | "packet-reader": { 974 | "version": "1.0.0", 975 | "resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz", 976 | "integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==" 977 | }, 978 | "parseurl": { 979 | "version": "1.3.3", 980 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", 981 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" 982 | }, 983 | "path-is-absolute": { 984 | "version": "1.0.1", 985 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 986 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" 987 | }, 988 | "path-key": { 989 | "version": "3.1.1", 990 | "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", 991 | "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" 992 | }, 993 | "path-to-regexp": { 994 | "version": "0.1.7", 995 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", 996 | "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" 997 | }, 998 | "performance-now": { 999 | "version": "2.1.0", 1000 | "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", 1001 | "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" 1002 | }, 1003 | "pg": { 1004 | "version": "7.18.2", 1005 | "resolved": "https://registry.npmjs.org/pg/-/pg-7.18.2.tgz", 1006 | "integrity": "sha512-Mvt0dGYMwvEADNKy5PMQGlzPudKcKKzJds/VbOeZJpb6f/pI3mmoXX0JksPgI3l3JPP/2Apq7F36O63J7mgveA==", 1007 | "requires": { 1008 | "buffer-writer": "2.0.0", 1009 | "packet-reader": "1.0.0", 1010 | "pg-connection-string": "0.1.3", 1011 | "pg-packet-stream": "^1.1.0", 1012 | "pg-pool": "^2.0.10", 1013 | "pg-types": "^2.1.0", 1014 | "pgpass": "1.x", 1015 | "semver": "4.3.2" 1016 | }, 1017 | "dependencies": { 1018 | "semver": { 1019 | "version": "4.3.2", 1020 | "resolved": "https://registry.npmjs.org/semver/-/semver-4.3.2.tgz", 1021 | "integrity": "sha1-x6BxWKgL7dBSNVt3DYLWZA+AO+c=" 1022 | } 1023 | } 1024 | }, 1025 | "pg-connection-string": { 1026 | "version": "0.1.3", 1027 | "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-0.1.3.tgz", 1028 | "integrity": "sha1-2hhHsglA5C7hSSvq9l1J2RskXfc=" 1029 | }, 1030 | "pg-int8": { 1031 | "version": "1.0.1", 1032 | "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", 1033 | "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==" 1034 | }, 1035 | "pg-packet-stream": { 1036 | "version": "1.1.0", 1037 | "resolved": "https://registry.npmjs.org/pg-packet-stream/-/pg-packet-stream-1.1.0.tgz", 1038 | "integrity": "sha512-kRBH0tDIW/8lfnnOyTwKD23ygJ/kexQVXZs7gEyBljw4FYqimZFxnMMx50ndZ8In77QgfGuItS5LLclC2TtjYg==" 1039 | }, 1040 | "pg-pool": { 1041 | "version": "2.0.10", 1042 | "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-2.0.10.tgz", 1043 | "integrity": "sha512-qdwzY92bHf3nwzIUcj+zJ0Qo5lpG/YxchahxIN8+ZVmXqkahKXsnl2aiJPHLYN9o5mB/leG+Xh6XKxtP7e0sjg==" 1044 | }, 1045 | "pg-types": { 1046 | "version": "2.2.0", 1047 | "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", 1048 | "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", 1049 | "requires": { 1050 | "pg-int8": "1.0.1", 1051 | "postgres-array": "~2.0.0", 1052 | "postgres-bytea": "~1.0.0", 1053 | "postgres-date": "~1.0.4", 1054 | "postgres-interval": "^1.1.0" 1055 | } 1056 | }, 1057 | "pgpass": { 1058 | "version": "1.0.2", 1059 | "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.2.tgz", 1060 | "integrity": "sha1-Knu0G2BltnkH6R2hsHwYR8h3swY=", 1061 | "requires": { 1062 | "split": "^1.0.0" 1063 | } 1064 | }, 1065 | "postgres-array": { 1066 | "version": "2.0.0", 1067 | "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", 1068 | "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==" 1069 | }, 1070 | "postgres-bytea": { 1071 | "version": "1.0.0", 1072 | "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", 1073 | "integrity": "sha1-AntTPAqokOJtFy1Hz5zOzFIazTU=" 1074 | }, 1075 | "postgres-date": { 1076 | "version": "1.0.4", 1077 | "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.4.tgz", 1078 | "integrity": "sha512-bESRvKVuTrjoBluEcpv2346+6kgB7UlnqWZsnbnCccTNq/pqfj1j6oBaN5+b/NrDXepYUT/HKadqv3iS9lJuVA==" 1079 | }, 1080 | "postgres-interval": { 1081 | "version": "1.2.0", 1082 | "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", 1083 | "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", 1084 | "requires": { 1085 | "xtend": "^4.0.0" 1086 | } 1087 | }, 1088 | "process-nextick-args": { 1089 | "version": "2.0.1", 1090 | "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", 1091 | "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" 1092 | }, 1093 | "proxy-addr": { 1094 | "version": "2.0.6", 1095 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz", 1096 | "integrity": "sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==", 1097 | "requires": { 1098 | "forwarded": "~0.1.2", 1099 | "ipaddr.js": "1.9.1" 1100 | } 1101 | }, 1102 | "psl": { 1103 | "version": "1.7.0", 1104 | "resolved": "https://registry.npmjs.org/psl/-/psl-1.7.0.tgz", 1105 | "integrity": "sha512-5NsSEDv8zY70ScRnOTn7bK7eanl2MvFrOrS/R6x+dBt5g1ghnj9Zv90kO8GwT8gxcu2ANyFprnFYB85IogIJOQ==" 1106 | }, 1107 | "punycode": { 1108 | "version": "2.1.1", 1109 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", 1110 | "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" 1111 | }, 1112 | "qs": { 1113 | "version": "6.7.0", 1114 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", 1115 | "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" 1116 | }, 1117 | "query-string": { 1118 | "version": "6.11.1", 1119 | "resolved": "https://registry.npmjs.org/query-string/-/query-string-6.11.1.tgz", 1120 | "integrity": "sha512-1ZvJOUl8ifkkBxu2ByVM/8GijMIPx+cef7u3yroO3Ogm4DOdZcF5dcrWTIlSHe3Pg/mtlt6/eFjObDfJureZZA==", 1121 | "requires": { 1122 | "decode-uri-component": "^0.2.0", 1123 | "split-on-first": "^1.0.0", 1124 | "strict-uri-encode": "^2.0.0" 1125 | } 1126 | }, 1127 | "range-parser": { 1128 | "version": "1.2.1", 1129 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", 1130 | "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" 1131 | }, 1132 | "raw-body": { 1133 | "version": "2.4.0", 1134 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", 1135 | "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", 1136 | "requires": { 1137 | "bytes": "3.1.0", 1138 | "http-errors": "1.7.2", 1139 | "iconv-lite": "0.4.24", 1140 | "unpipe": "1.0.0" 1141 | } 1142 | }, 1143 | "rc": { 1144 | "version": "1.2.8", 1145 | "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", 1146 | "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", 1147 | "requires": { 1148 | "deep-extend": "^0.6.0", 1149 | "ini": "~1.3.0", 1150 | "minimist": "^1.2.0", 1151 | "strip-json-comments": "~2.0.1" 1152 | } 1153 | }, 1154 | "readable-stream": { 1155 | "version": "2.3.7", 1156 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", 1157 | "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", 1158 | "requires": { 1159 | "core-util-is": "~1.0.0", 1160 | "inherits": "~2.0.3", 1161 | "isarray": "~1.0.0", 1162 | "process-nextick-args": "~2.0.0", 1163 | "safe-buffer": "~5.1.1", 1164 | "string_decoder": "~1.1.1", 1165 | "util-deprecate": "~1.0.1" 1166 | } 1167 | }, 1168 | "redis": { 1169 | "version": "2.8.0", 1170 | "resolved": "https://registry.npmjs.org/redis/-/redis-2.8.0.tgz", 1171 | "integrity": "sha512-M1OkonEQwtRmZv4tEWF2VgpG0JWJ8Fv1PhlgT5+B+uNq2cA3Rt1Yt/ryoR+vQNOQcIEgdCdfH0jr3bDpihAw1A==", 1172 | "requires": { 1173 | "double-ended-queue": "^2.1.0-0", 1174 | "redis-commands": "^1.2.0", 1175 | "redis-parser": "^2.6.0" 1176 | } 1177 | }, 1178 | "redis-commands": { 1179 | "version": "1.5.0", 1180 | "resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.5.0.tgz", 1181 | "integrity": "sha512-6KxamqpZ468MeQC3bkWmCB1fp56XL64D4Kf0zJSwDZbVLLm7KFkoIcHrgRvQ+sk8dnhySs7+yBg94yIkAK7aJg==" 1182 | }, 1183 | "redis-parser": { 1184 | "version": "2.6.0", 1185 | "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-2.6.0.tgz", 1186 | "integrity": "sha1-Uu0J2srBCPGmMcB+m2mUHnoZUEs=" 1187 | }, 1188 | "request": { 1189 | "version": "2.88.2", 1190 | "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", 1191 | "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", 1192 | "requires": { 1193 | "aws-sign2": "~0.7.0", 1194 | "aws4": "^1.8.0", 1195 | "caseless": "~0.12.0", 1196 | "combined-stream": "~1.0.6", 1197 | "extend": "~3.0.2", 1198 | "forever-agent": "~0.6.1", 1199 | "form-data": "~2.3.2", 1200 | "har-validator": "~5.1.3", 1201 | "http-signature": "~1.2.0", 1202 | "is-typedarray": "~1.0.0", 1203 | "isstream": "~0.1.2", 1204 | "json-stringify-safe": "~5.0.1", 1205 | "mime-types": "~2.1.19", 1206 | "oauth-sign": "~0.9.0", 1207 | "performance-now": "^2.1.0", 1208 | "qs": "~6.5.2", 1209 | "safe-buffer": "^5.1.2", 1210 | "tough-cookie": "~2.5.0", 1211 | "tunnel-agent": "^0.6.0", 1212 | "uuid": "^3.3.2" 1213 | }, 1214 | "dependencies": { 1215 | "qs": { 1216 | "version": "6.5.2", 1217 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", 1218 | "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" 1219 | } 1220 | } 1221 | }, 1222 | "requires-port": { 1223 | "version": "1.0.0", 1224 | "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", 1225 | "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=" 1226 | }, 1227 | "rimraf": { 1228 | "version": "2.7.1", 1229 | "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", 1230 | "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", 1231 | "requires": { 1232 | "glob": "^7.1.3" 1233 | } 1234 | }, 1235 | "safe-buffer": { 1236 | "version": "5.1.2", 1237 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 1238 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" 1239 | }, 1240 | "safer-buffer": { 1241 | "version": "2.1.2", 1242 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 1243 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 1244 | }, 1245 | "sax": { 1246 | "version": "1.2.4", 1247 | "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", 1248 | "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" 1249 | }, 1250 | "semver": { 1251 | "version": "5.7.1", 1252 | "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", 1253 | "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" 1254 | }, 1255 | "send": { 1256 | "version": "0.17.1", 1257 | "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", 1258 | "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", 1259 | "requires": { 1260 | "debug": "2.6.9", 1261 | "depd": "~1.1.2", 1262 | "destroy": "~1.0.4", 1263 | "encodeurl": "~1.0.2", 1264 | "escape-html": "~1.0.3", 1265 | "etag": "~1.8.1", 1266 | "fresh": "0.5.2", 1267 | "http-errors": "~1.7.2", 1268 | "mime": "1.6.0", 1269 | "ms": "2.1.1", 1270 | "on-finished": "~2.3.0", 1271 | "range-parser": "~1.2.1", 1272 | "statuses": "~1.5.0" 1273 | }, 1274 | "dependencies": { 1275 | "debug": { 1276 | "version": "2.6.9", 1277 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 1278 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 1279 | "requires": { 1280 | "ms": "2.0.0" 1281 | }, 1282 | "dependencies": { 1283 | "ms": { 1284 | "version": "2.0.0", 1285 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 1286 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 1287 | } 1288 | } 1289 | }, 1290 | "ms": { 1291 | "version": "2.1.1", 1292 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", 1293 | "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" 1294 | } 1295 | } 1296 | }, 1297 | "serve-static": { 1298 | "version": "1.14.1", 1299 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", 1300 | "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", 1301 | "requires": { 1302 | "encodeurl": "~1.0.2", 1303 | "escape-html": "~1.0.3", 1304 | "parseurl": "~1.3.3", 1305 | "send": "0.17.1" 1306 | } 1307 | }, 1308 | "set-blocking": { 1309 | "version": "2.0.0", 1310 | "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", 1311 | "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" 1312 | }, 1313 | "setprototypeof": { 1314 | "version": "1.1.1", 1315 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", 1316 | "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" 1317 | }, 1318 | "shebang-command": { 1319 | "version": "2.0.0", 1320 | "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", 1321 | "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", 1322 | "requires": { 1323 | "shebang-regex": "^3.0.0" 1324 | } 1325 | }, 1326 | "shebang-regex": { 1327 | "version": "3.0.0", 1328 | "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", 1329 | "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" 1330 | }, 1331 | "signal-exit": { 1332 | "version": "3.0.2", 1333 | "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", 1334 | "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" 1335 | }, 1336 | "simple-swizzle": { 1337 | "version": "0.2.2", 1338 | "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", 1339 | "integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=", 1340 | "requires": { 1341 | "is-arrayish": "^0.3.1" 1342 | } 1343 | }, 1344 | "split": { 1345 | "version": "1.0.1", 1346 | "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", 1347 | "integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==", 1348 | "requires": { 1349 | "through": "2" 1350 | } 1351 | }, 1352 | "split-on-first": { 1353 | "version": "1.1.0", 1354 | "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz", 1355 | "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==" 1356 | }, 1357 | "sqlite3": { 1358 | "version": "4.1.1", 1359 | "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-4.1.1.tgz", 1360 | "integrity": "sha512-CvT5XY+MWnn0HkbwVKJAyWEMfzpAPwnTiB3TobA5Mri44SrTovmmh499NPQP+gatkeOipqPlBLel7rn4E/PCQg==", 1361 | "requires": { 1362 | "nan": "^2.12.1", 1363 | "node-pre-gyp": "^0.11.0", 1364 | "request": "^2.87.0" 1365 | }, 1366 | "dependencies": { 1367 | "mkdirp": { 1368 | "version": "0.5.3", 1369 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.3.tgz", 1370 | "integrity": "sha512-P+2gwrFqx8lhew375MQHHeTlY8AuOJSrGf0R5ddkEndUkmwpgUob/vQuBD1V22/Cw1/lJr4x+EjllSezBThzBg==", 1371 | "requires": { 1372 | "minimist": "^1.2.5" 1373 | } 1374 | }, 1375 | "node-pre-gyp": { 1376 | "version": "0.11.0", 1377 | "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.11.0.tgz", 1378 | "integrity": "sha512-TwWAOZb0j7e9eGaf9esRx3ZcLaE5tQ2lvYy1pb5IAaG1a2e2Kv5Lms1Y4hpj+ciXJRofIxxlt5haeQ/2ANeE0Q==", 1379 | "requires": { 1380 | "detect-libc": "^1.0.2", 1381 | "mkdirp": "^0.5.1", 1382 | "needle": "^2.2.1", 1383 | "nopt": "^4.0.1", 1384 | "npm-packlist": "^1.1.6", 1385 | "npmlog": "^4.0.2", 1386 | "rc": "^1.2.7", 1387 | "rimraf": "^2.6.1", 1388 | "semver": "^5.3.0", 1389 | "tar": "^4" 1390 | } 1391 | } 1392 | } 1393 | }, 1394 | "sshpk": { 1395 | "version": "1.16.1", 1396 | "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", 1397 | "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", 1398 | "requires": { 1399 | "asn1": "~0.2.3", 1400 | "assert-plus": "^1.0.0", 1401 | "bcrypt-pbkdf": "^1.0.0", 1402 | "dashdash": "^1.12.0", 1403 | "ecc-jsbn": "~0.1.1", 1404 | "getpass": "^0.1.1", 1405 | "jsbn": "~0.1.0", 1406 | "safer-buffer": "^2.0.2", 1407 | "tweetnacl": "~0.14.0" 1408 | } 1409 | }, 1410 | "statuses": { 1411 | "version": "1.5.0", 1412 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", 1413 | "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" 1414 | }, 1415 | "strict-uri-encode": { 1416 | "version": "2.0.0", 1417 | "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", 1418 | "integrity": "sha1-ucczDHBChi9rFC3CdLvMWGbONUY=" 1419 | }, 1420 | "string-width": { 1421 | "version": "1.0.2", 1422 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", 1423 | "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", 1424 | "requires": { 1425 | "code-point-at": "^1.0.0", 1426 | "is-fullwidth-code-point": "^1.0.0", 1427 | "strip-ansi": "^3.0.0" 1428 | } 1429 | }, 1430 | "string_decoder": { 1431 | "version": "1.1.1", 1432 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", 1433 | "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", 1434 | "requires": { 1435 | "safe-buffer": "~5.1.0" 1436 | } 1437 | }, 1438 | "strip-ansi": { 1439 | "version": "3.0.1", 1440 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", 1441 | "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", 1442 | "requires": { 1443 | "ansi-regex": "^2.0.0" 1444 | } 1445 | }, 1446 | "strip-json-comments": { 1447 | "version": "2.0.1", 1448 | "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", 1449 | "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" 1450 | }, 1451 | "tar": { 1452 | "version": "4.4.13", 1453 | "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.13.tgz", 1454 | "integrity": "sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==", 1455 | "requires": { 1456 | "chownr": "^1.1.1", 1457 | "fs-minipass": "^1.2.5", 1458 | "minipass": "^2.8.6", 1459 | "minizlib": "^1.2.1", 1460 | "mkdirp": "^0.5.0", 1461 | "safe-buffer": "^5.1.2", 1462 | "yallist": "^3.0.3" 1463 | }, 1464 | "dependencies": { 1465 | "mkdirp": { 1466 | "version": "0.5.3", 1467 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.3.tgz", 1468 | "integrity": "sha512-P+2gwrFqx8lhew375MQHHeTlY8AuOJSrGf0R5ddkEndUkmwpgUob/vQuBD1V22/Cw1/lJr4x+EjllSezBThzBg==", 1469 | "requires": { 1470 | "minimist": "^1.2.5" 1471 | } 1472 | } 1473 | } 1474 | }, 1475 | "through": { 1476 | "version": "2.3.8", 1477 | "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", 1478 | "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" 1479 | }, 1480 | "toidentifier": { 1481 | "version": "1.0.0", 1482 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", 1483 | "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" 1484 | }, 1485 | "tough-cookie": { 1486 | "version": "2.5.0", 1487 | "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", 1488 | "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", 1489 | "requires": { 1490 | "psl": "^1.1.28", 1491 | "punycode": "^2.1.1" 1492 | } 1493 | }, 1494 | "tunnel-agent": { 1495 | "version": "0.6.0", 1496 | "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", 1497 | "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", 1498 | "requires": { 1499 | "safe-buffer": "^5.0.1" 1500 | } 1501 | }, 1502 | "tweetnacl": { 1503 | "version": "0.14.5", 1504 | "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", 1505 | "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" 1506 | }, 1507 | "type-is": { 1508 | "version": "1.6.18", 1509 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", 1510 | "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", 1511 | "requires": { 1512 | "media-typer": "0.3.0", 1513 | "mime-types": "~2.1.24" 1514 | } 1515 | }, 1516 | "uniqid": { 1517 | "version": "5.2.0", 1518 | "resolved": "https://registry.npmjs.org/uniqid/-/uniqid-5.2.0.tgz", 1519 | "integrity": "sha512-LH8zsvwJ/GL6YtNfSOmMCrI9piraAUjBfw2MCvleNE6a4pVKJwXjG2+HWhkVeFcSg+nmaPKbMrMOoxwQluZ1Mg==" 1520 | }, 1521 | "unpipe": { 1522 | "version": "1.0.0", 1523 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 1524 | "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" 1525 | }, 1526 | "uri-js": { 1527 | "version": "4.2.2", 1528 | "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", 1529 | "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", 1530 | "requires": { 1531 | "punycode": "^2.1.0" 1532 | } 1533 | }, 1534 | "util-deprecate": { 1535 | "version": "1.0.2", 1536 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 1537 | "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" 1538 | }, 1539 | "utils-merge": { 1540 | "version": "1.0.1", 1541 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", 1542 | "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" 1543 | }, 1544 | "uuid": { 1545 | "version": "3.4.0", 1546 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", 1547 | "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" 1548 | }, 1549 | "vary": { 1550 | "version": "1.1.2", 1551 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 1552 | "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" 1553 | }, 1554 | "verror": { 1555 | "version": "1.10.0", 1556 | "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", 1557 | "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", 1558 | "requires": { 1559 | "assert-plus": "^1.0.0", 1560 | "core-util-is": "1.0.2", 1561 | "extsprintf": "^1.2.0" 1562 | } 1563 | }, 1564 | "which": { 1565 | "version": "2.0.2", 1566 | "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", 1567 | "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", 1568 | "requires": { 1569 | "isexe": "^2.0.0" 1570 | } 1571 | }, 1572 | "wide-align": { 1573 | "version": "1.1.3", 1574 | "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", 1575 | "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", 1576 | "requires": { 1577 | "string-width": "^1.0.2 || 2" 1578 | } 1579 | }, 1580 | "wrappy": { 1581 | "version": "1.0.2", 1582 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 1583 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" 1584 | }, 1585 | "xtend": { 1586 | "version": "4.0.2", 1587 | "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", 1588 | "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" 1589 | }, 1590 | "yallist": { 1591 | "version": "3.1.1", 1592 | "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", 1593 | "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" 1594 | } 1595 | } 1596 | } 1597 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "unicorn-load-balancer", 3 | "version": "2.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "cross-env DEBUG=* node index.js" 8 | }, 9 | "author": "Maxime Baconnais", 10 | "license": "MIT", 11 | "dependencies": { 12 | "color": "^3.1.2", 13 | "cors": "^2.8.5", 14 | "cross-env": "^6.0.3", 15 | "debug": "^4.1.1", 16 | "detect-browser": "^4.7.0", 17 | "esm": "^3.2.25", 18 | "express": "^4.17.1", 19 | "getenv": "^1.0.0", 20 | "http-proxy": "^1.18.0", 21 | "md5": "^2.2.1", 22 | "mkdirp": "^1.0.3", 23 | "node-fetch": "^2.6.0", 24 | "node-pre-gyp": "^0.13.0", 25 | "pg": "^7.12.1", 26 | "query-string": "^6.8.3", 27 | "redis": "^2.8.0", 28 | "sqlite3": "^4.1.0", 29 | "uniqid": "^5.0.3" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import cors from 'cors'; 3 | import bodyParser from 'body-parser'; 4 | 5 | import config from './config'; 6 | import Router from './routes'; 7 | import Proxy from './routes/proxy'; 8 | import { internalUrl } from './utils'; 9 | import ServersManager from './core/servers'; 10 | 11 | import debug from 'debug'; 12 | 13 | // Debugger 14 | const D = debug('UnicornLoadBalancer'); 15 | 16 | // Welcome 17 | D('Version: ' + config.version) 18 | 19 | // Init Express 20 | const app = express(); 21 | 22 | // CORS 23 | app.use(cors()); 24 | 25 | // Body parsing 26 | app.use(bodyParser.json()); 27 | app.use(bodyParser.urlencoded({ 28 | extended: true 29 | })); 30 | app.use((err, _, res, next) => { 31 | if (err instanceof SyntaxError && err.status >= 400 && err.status < 500 && err.message.indexOf('JSON')) 32 | return (res.status(400).send({ error: { code: 'INVALID_BODY', message: 'Syntax error in the JSON body' } })); 33 | next(); 34 | }); 35 | 36 | // Init routes 37 | D('Initializing API routes...'); 38 | 39 | // Routes 40 | Router(app); 41 | 42 | // Load servers available in configuration 43 | ((Array.isArray(config.custom.servers.list)) ? config.custom.servers.list : []).map(e => ({ 44 | name: e, 45 | url: ((e.substr(-1) === '/') ? e.substr(0, e.length - 1) : e), 46 | sessions: [], 47 | settings: { 48 | maxSessions: 0, 49 | maxDownloads: 0, 50 | maxTranscodes: 0 51 | } 52 | })).forEach(e => { 53 | ServersManager.update(e); 54 | }); 55 | 56 | // Create HTTP server 57 | const httpServer = app.listen(config.server.port); 58 | 59 | // Forward websockets 60 | httpServer.on('upgrade', (req, res) => { 61 | Proxy.ws(req, res); 62 | }); 63 | 64 | // Debug 65 | D('Launched on ' + internalUrl()); 66 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | import env from 'getenv'; 2 | 3 | env.disableErrors(); 4 | 5 | export default { 6 | version: '2.0.0', 7 | server: { 8 | port: env.int('SERVER_PORT', 3001), 9 | public: env.string('SERVER_PUBLIC', 'http://127.0.0.1:3001/'), 10 | }, 11 | plex: { 12 | host: env.string('PLEX_HOST', '127.0.0.1'), 13 | port: env.int('PLEX_PORT', 32400), 14 | path: { 15 | usr: env.string('PLEX_PATH_USR', '/usr/lib/plexmediaserver/'), 16 | sessions: env.string('PLEX_PATH_SESSIONS', '/var/lib/plexmediaserver/Library/Application Support/Plex Media Server/Cache/Transcode/Sessions/') 17 | } 18 | }, 19 | database: { 20 | mode: env.string('DATABASE_MODE', 'sqlite'), 21 | sqlite: { 22 | path: env.string('DATABASE_SQLITE_PATH', '/var/lib/plexmediaserver/Library/Application Support/Plex Media Server/Plug-in Support/Databases/com.plexapp.plugins.library.db') 23 | }, 24 | postgresql: { 25 | host: env.string('DATABASE_POSTGRESQL_HOST', ''), 26 | database: env.string('DATABASE_POSTGRESQL_DATABASE', ''), 27 | user: env.string('DATABASE_POSTGRESQL_USER', ''), 28 | password: env.string('DATABASE_POSTGRESQL_PASSWORD', ''), 29 | port: env.int('DATABASE_POSTGRESQL_PORT', 5432) 30 | } 31 | }, 32 | redis: { 33 | host: env('REDIS_HOST', undefined), 34 | port: env.int('REDIS_PORT', 6379), 35 | password: env.string('REDIS_PASSWORD', ''), 36 | db: env.int('REDIS_DB', 0) 37 | }, 38 | custom: { 39 | scores: { 40 | timeout: env.int('CUSTOM_SCORES_TIMEOUT', 10) 41 | }, 42 | image: { 43 | proxy: env.string('CUSTOM_IMAGE_PROXY', '') // 'https://images.weserv.nl/' 44 | }, 45 | download: { 46 | forward: env.boolish('CUSTOM_DOWNLOAD_FORWARD', false) 47 | }, 48 | servers: { 49 | list: env.array('CUSTOM_SERVERS_LIST', 'string', []) 50 | } 51 | } 52 | }; 53 | -------------------------------------------------------------------------------- /src/core/images.js: -------------------------------------------------------------------------------- 1 | import md5 from 'md5'; 2 | import { parseUserAgent } from 'detect-browser'; 3 | 4 | export const parseArguments = (query, basepath = '/', useragent = '') => { 5 | // Parse url 6 | let url = query.url || ''; 7 | url = url.replace('http://127.0.0.1/', '/'); 8 | url = url.replace('http://127.0.0.1:32400/', '/'); 9 | url = url.replace(basepath, '/'); 10 | if (query['X-Plex-Token'] && url && url[0] === '/') { 11 | url += (url.indexOf('?') === -1) ? `?X-Plex-Token=${query['X-Plex-Token']}` : `&X-Plex-Token=${query['X-Plex-Token']}` 12 | } 13 | if (url && url[0] === '/') 14 | url = basepath + url.substring(1); 15 | 16 | // Extract parameters 17 | let params = { 18 | ...((query.width) ? { w: parseInt(query.width) } : {}), 19 | ...((query.height) ? { h: parseInt(query.height) } : {}), 20 | ...((query.blur) ? { blur: parseInt(query.blur * 2), gam: 4 } : {}), 21 | ...((query.quality) ? { quality: parseInt(query.quality) } : ((query.blur) ? { quality: 90 } : { quality: 70 })), 22 | ...((query['X-Plex-Token']) ? { "X-Plex-Token": query['X-Plex-Token'] } : {}), 23 | url 24 | }; 25 | 26 | // Auto select WebP if user-agent support it 27 | const browser = parseUserAgent(useragent); 28 | if (browser && browser.name && browser.name === 'chrome') { 29 | params = { ...params, output: 'webp' }; 30 | } 31 | 32 | // Generate key 33 | params.key = md5(`${(query.url || '').split('?')[0]}|${params.w || ''}|${params.h || ''}|${params.blur || ''}|${params.output || ''}|${params.quality || ''}`.toLowerCase()) 34 | 35 | // Return params 36 | return params; 37 | } -------------------------------------------------------------------------------- /src/core/servers.js: -------------------------------------------------------------------------------- 1 | import fetch from 'node-fetch'; 2 | import { time, publicUrl } from '../utils'; 3 | import config from '../config'; 4 | 5 | let servers = {}; 6 | 7 | let ServersManager = {}; 8 | 9 | // Add or update a server 10 | ServersManager.update = (e) => { 11 | const name = (e.name) ? e.name : (e.url) ? e.url : ''; 12 | if (!name) 13 | return (ServersManager.list()); 14 | servers[name] = { 15 | name, 16 | sessions: ((!Array.isArray(e.sessions)) ? [] : e.sessions.map((s) => ({ 17 | id: ((s.id) ? s.id : false), 18 | status: ((s.status && ['DONE', 'DOWNLOAD', 'TRANSCODE'].indexOf(s.status.toUpperCase()) !== -1) ? s.status.toUpperCase() : false), 19 | codec: ((s.codec) ? s.codec : false), 20 | lastChunkDownload: ((s.lastChunkDownload) ? s.lastChunkDownload : 0) 21 | }))).filter((s) => (s.id !== false && s.status !== false)), 22 | settings: { 23 | maxSessions: ((typeof (e.settings) !== 'undefined' && typeof (e.settings.maxSessions) !== 'undefined') ? parseInt(e.settings.maxSessions) : 0), 24 | maxDownloads: ((typeof (e.settings) !== 'undefined' && typeof (e.settings.maxDownloads) !== 'undefined') ? parseInt(e.settings.maxDownloads) : 0), 25 | maxTranscodes: ((typeof (e.settings) !== 'undefined' && typeof (e.settings.maxTranscodes) !== 'undefined') ? parseInt(e.settings.maxTranscodes) : 0), 26 | }, 27 | url: ((e.url) ? e.url : false), 28 | time: time() 29 | }; 30 | return (ServersManager.list()); 31 | }; 32 | 33 | // Remove a server 34 | ServersManager.remove = (e) => { 35 | const name = (e.name) ? e.name : (e.url) ? e.url : ''; 36 | delete servers[name]; 37 | return (ServersManager.list()); 38 | }; 39 | 40 | // List all the servers with scores 41 | ServersManager.list = () => { 42 | let output = {}; 43 | Object.keys(servers).forEach((i) => { 44 | output[i] = { ...servers[i], score: ServersManager.score(servers[i]) }; 45 | }); 46 | return (output); 47 | } 48 | 49 | // Chose best server 50 | ServersManager.chooseServer = (session, ip = false) => { 51 | return (new Promise((resolve, reject) => { 52 | let tab = []; 53 | const list = ServersManager.list(); 54 | Object.keys(list).forEach((i) => { 55 | tab.push(list[i]); 56 | }); 57 | tab.sort((a, b) => (a.score - b.score)); 58 | if (typeof (tab[0]) === 'undefined') 59 | return resolve(false); 60 | const origin = encodeURIComponent(publicUrl()) 61 | fetch(`${tab[0].url}/api/resolve?session=${session}&ip=${ip}&origin=${origin}`) 62 | .then(res => res.json()) 63 | .then(body => { 64 | return resolve(body.client) 65 | }).catch((err) => { return reject(err) }); 66 | })); 67 | }; 68 | 69 | // Calculate server score 70 | ServersManager.score = (e) => { 71 | // The configuration wasn't updated since X seconds, the server is probably unavailable 72 | if (time() - e.time > config.custom.scores.timeout) 73 | return (100); 74 | 75 | // Default load 0 76 | let load = 0; 77 | 78 | // Add load value for each session 79 | e.sessions.forEach((s) => { 80 | 81 | // Transcode streams 82 | if (s.status === 'TRANSCODE') { 83 | load += 1; 84 | if (s.codec === 'hevc') { 85 | load += 1.5; 86 | } 87 | if (s.codec === 'copy') { 88 | load -= 0.5; 89 | } 90 | } 91 | 92 | // Serving streams 93 | if (s.status === 'DONE') { 94 | load += 0.5; 95 | } 96 | 97 | // Download streams 98 | if (s.status === 'DOWNLOAD') { 99 | load += 0.25; 100 | } 101 | }) 102 | 103 | // Server already have too much sessions 104 | if (e.sessions.filter((s) => (['TRANSCODE', 'DONE'].indexOf(s.status) !== -1)).length > e.settings.maxSessions) 105 | load += 2.5; 106 | 107 | // Server already have too much transcodes 108 | if (e.sessions.filter((s) => (['TRANSCODE'].indexOf(s.status) !== -1)).length > e.settings.maxTranscodes) 109 | load += 5; 110 | 111 | // Server already have too much downloads 112 | if (e.sessions.filter((s) => (['DOWNLOAD'].indexOf(s.status) !== -1)).length > e.settings.maxDownloads) 113 | load += 1; 114 | 115 | // Return load 116 | return (load); 117 | } 118 | 119 | // Returns our ServersManager 120 | export default ServersManager; 121 | -------------------------------------------------------------------------------- /src/core/sessions.js: -------------------------------------------------------------------------------- 1 | import debug from 'debug'; 2 | import config from '../config'; 3 | import { publicUrl, plexUrl, download, replaceAll } from '../utils'; 4 | import { dirname } from 'path'; 5 | import SessionStore from '../store'; 6 | import ServersManager from './servers'; 7 | import Database from '../database'; 8 | import fetch from 'node-fetch'; 9 | import uniqid from 'uniqid'; 10 | import mkdirp from 'mkdirp'; 11 | 12 | // Debugger 13 | const D = debug('UnicornLoadBalancer'); 14 | 15 | let SessionsManager = {}; 16 | 17 | // Plex table to match "session" and "X-Plex-Session-Identifier" 18 | let cache = {}; 19 | 20 | let ffmpegCache = {}; 21 | 22 | // Table to link session to transcoder url 23 | let urls = {}; 24 | 25 | SessionsManager.chooseServer = async (session, ip = false) => { 26 | if (session && urls[session]) 27 | return (urls[session]); 28 | let url = ''; 29 | try { 30 | url = await ServersManager.chooseServer(session, ip); 31 | } 32 | catch (err) { } 33 | D('SERVER ' + session + ' [' + url + ']'); 34 | if (session && url.length) 35 | urls[session] = url; 36 | return (url); 37 | }; 38 | 39 | SessionsManager.cacheSessionFromRequest = (req) => { 40 | if (typeof (req.query['X-Plex-Session-Identifier']) !== 'undefined' && typeof (req.query.session) !== 'undefined') { 41 | cache[req.query['X-Plex-Session-Identifier']] = req.query.session.toString(); 42 | } 43 | } 44 | 45 | SessionsManager.getCacheSession = (xplexsessionidentifier) => { 46 | if (cache[xplexsessionidentifier]) 47 | return (cache[xplexsessionidentifier]); 48 | return (false); 49 | } 50 | 51 | SessionsManager.getSessionFromRequest = (req) => { 52 | if (typeof (req.params.sessionId) !== 'undefined') 53 | return (req.params.sessionId); 54 | if (typeof (req.query.session) !== 'undefined') 55 | return (req.query.session); 56 | if (typeof (req.query['X-Plex-Session-Identifier']) !== 'undefined' && typeof (cache[req.query['X-Plex-Session-Identifier']]) !== 'undefined') 57 | return (cache[req.query['X-Plex-Session-Identifier']]); 58 | if (typeof (req.query['X-Plex-Session-Identifier']) !== 'undefined') 59 | return (req.query['X-Plex-Session-Identifier']); 60 | if (typeof (req.query['X-Plex-Client-Identifier']) !== 'undefined') 61 | return (req.query['X-Plex-Client-Identifier']); 62 | return (false); 63 | } 64 | 65 | // Parse FFmpeg parameters with internal bindings 66 | SessionsManager.parseFFmpegParameters = async (args = [], env = {}, optimizeMode = false) => { 67 | // Extract Session ID 68 | const regex = /^http\:\/\/127.0.0.1:32400\/video\/:\/transcode\/session\/(.*)\/progress$/; 69 | const sessions = args.filter(e => (regex.test(e))).map(e => (e.match(regex)[1])) 70 | const sessionFull = (typeof (sessions[0]) !== 'undefined') ? sessions[0] : false; 71 | const sessionId = (typeof (sessions[0]) !== 'undefined') ? sessions[0].split('/')[0] : false; 72 | 73 | // Check Session Id 74 | if (!sessionId || !sessionFull) 75 | return (false); 76 | 77 | // Debug 78 | D('FFMPEG ' + sessionId + ' [' + sessionFull + ']'); 79 | 80 | // Parse arguments 81 | const parsedArgs = args.map((e) => { 82 | 83 | // Progress 84 | if (e.indexOf('/progress') !== -1) 85 | return (e.replace(plexUrl(), '{INTERNAL_TRANSCODER}').replace('http://127.0.0.1:32400/', '{INTERNAL_TRANSCODER}')); 86 | 87 | // Manifest and seglist 88 | if (e.indexOf('/manifest') !== -1 || e.indexOf('/seglist') !== -1) 89 | return (e.replace(plexUrl(), '{INTERNAL_TRANSCODER}').replace('http://127.0.0.1:32400/', '{INTERNAL_TRANSCODER}')); 90 | 91 | // Other 92 | let parsed = e; 93 | parsed = replaceAll(parsed, plexUrl(), publicUrl()) 94 | parsed = replaceAll(parsed, 'http://127.0.0.1:32400/', publicUrl()) 95 | parsed = replaceAll(parsed, config.plex.path.sessions, publicUrl() + 'api/sessions/') 96 | parsed = replaceAll(parsed, config.plex.path.usr, '{INTERNAL_PLEX_SETUP}') 97 | return parsed; 98 | }); 99 | 100 | // Add seglist to arguments if needed and resolve links if needed 101 | const segList = '{INTERNAL_TRANSCODER}video/:/transcode/session/' + sessionFull + '/seglist'; 102 | let finalArgs = []; 103 | let optimize = {}; 104 | let segListMode = false; 105 | for (let i = 0; i < parsedArgs.length; i++) { 106 | let e = parsedArgs[i]; 107 | 108 | // Seglist 109 | if (e === '-segment_list') { 110 | segListMode = true; 111 | finalArgs.push(e); 112 | continue; 113 | } 114 | if (segListMode) { 115 | finalArgs.push(segList); 116 | if (parsedArgs[i + 1] !== '-segment_list_type') 117 | finalArgs.push('-segment_list_type', 'csv', '-segment_list_size', '2147483647'); 118 | segListMode = false; 119 | continue; 120 | } 121 | 122 | // Optimize, replace optimize path 123 | if (optimizeMode && i > 0 && parsedArgs[i - 1] !== '-i' && e[0] === '/') { 124 | finalArgs.push(`{OPTIMIZE_PATH}${e.split('/').slice(-1).pop()}`); 125 | optimize[e.split('/').slice(-1).pop()] = e; 126 | continue; 127 | } 128 | 129 | // Link resolver (Replace filepath to http plex path) 130 | if (i > 0 && parsedArgs[i - 1] === '-i' && !config.custom.download.forward) { 131 | let file = parsedArgs[i]; 132 | try { 133 | const data = await Database.getPartFromPath(parsedArgs[i]); 134 | if (typeof (data.id) !== 'undefined') 135 | file = `${publicUrl()}library/parts/${data.id}/0/file.stream?download=1`; 136 | } catch (e) { 137 | file = parsedArgs[i]; 138 | } 139 | finalArgs.push(file); 140 | continue; 141 | } 142 | 143 | // Link resolver (Replace Plex file url by direct file) 144 | if (i > 0 && parsedArgs[i - 1] === '-i' && config.custom.download.forward) { 145 | let file = parsedArgs[i]; 146 | let partId = false; 147 | if (file.indexOf('library/parts/') !== -1) { 148 | partId = file.split('library/parts/')[1].split('/')[0]; 149 | } 150 | if (!partId) { 151 | finalArgs.push(file); 152 | continue; 153 | } 154 | try { 155 | const data = await Database.getPartFromId(partId); 156 | if (typeof (data.file) !== 'undefined' && data.file.length) 157 | file = data.file; 158 | } catch (e) { 159 | file = parsedArgs[i]; 160 | } 161 | finalArgs.push(file); 162 | continue 163 | } 164 | 165 | // Ignore parameter 166 | finalArgs.push(e); 167 | }; 168 | return ({ 169 | id: uniqid(), 170 | args: finalArgs, 171 | env, 172 | session: sessionId, 173 | sessionFull, 174 | optimize 175 | }); 176 | }; 177 | 178 | // Store the FFMPEG parameters in RedisCache 179 | SessionsManager.saveSession = (parsed) => { 180 | SessionStore.set(parsed.session, parsed).then(() => { }).catch(() => { }) 181 | }; 182 | 183 | // Call media optimizer on transcoders 184 | SessionsManager.optimizerInit = async (parsed) => { 185 | D(`OPTIMIZER ${parsed.session} [START]`); 186 | const server = await ServersManager.chooseServer(parsed.session, false) 187 | fetch(`${server}/api/optimize`, { 188 | headers: { 189 | 'Accept': 'application/json', 190 | 'Content-Type': 'application/json' 191 | }, 192 | method: 'POST', 193 | body: JSON.stringify(parsed) 194 | }) 195 | return parsed; 196 | }; 197 | 198 | // Call media optimizer on transcoders 199 | SessionsManager.optimizerDelete = async (parsed) => { 200 | D(`OPTIMIZER ${parsed.session} [DELETE]`); 201 | SessionsManager.ffmpegSetCache(parsed.id, 0); 202 | const server = await ServersManager.chooseServer(parsed.session, false) 203 | fetch(`${server}/api/optimize/${parsed.session}`, { 204 | headers: { 205 | 'Accept': 'application/json', 206 | 'Content-Type': 'application/json' 207 | }, 208 | method: 'DELETE', 209 | body: JSON.stringify(parsed) 210 | }); 211 | SessionsManager.cleanSession(parsed.session); 212 | return parsed; 213 | }; 214 | 215 | // Callback of the optimizer server 216 | SessionsManager.optimizerDownload = (parsed) => (new Promise(async (resolve, reject) => { 217 | const files = Object.keys(parsed.optimize); 218 | const server = await SessionsManager.chooseServer(parsed.session); 219 | for (let i = 0; i < files.length; i++) { 220 | D(`OPTIMIZER ${server}/api/optimize/${parsed.session}/${encodeURIComponent(files[i])} [DOWNLOAD]`); 221 | try { 222 | await mkdirp(dirname(parsed.optimize[files[i]])); 223 | } 224 | catch (err) { 225 | D(`OPTIMIZER Failed to create directory`); 226 | } 227 | try { 228 | await download(`${server}/api/optimize/${parsed.session}/${encodeURIComponent(files[i])}`, parsed.optimize[files[i]]) 229 | } 230 | catch (err) { 231 | D(`OPTIMIZER ${server}/api/optimize/${parsed.session}/${encodeURIComponent(files[i])} [FAILED]`); 232 | } 233 | } 234 | resolve(parsed); 235 | })); 236 | 237 | // Clear session 238 | SessionsManager.cleanSession = (sessionId) => { 239 | D('DELETE ' + sessionId); 240 | return SessionStore.delete(sessionId) 241 | }; 242 | 243 | // Set FFmpeg cache 244 | SessionsManager.ffmpegSetCache = (id, status) => { 245 | ffmpegCache[id] = status; 246 | return ffmpegCache[id]; 247 | }; 248 | 249 | // Get FFmpeg cache 250 | SessionsManager.ffmpegGetCache = (id) => { 251 | if (typeof (ffmpegCache[id]) !== 'undefined') 252 | return ffmpegCache[id]; 253 | return false; 254 | }; 255 | 256 | // Export our SessionsManager 257 | export default SessionsManager; 258 | -------------------------------------------------------------------------------- /src/database/index.js: -------------------------------------------------------------------------------- 1 | import config from '../config'; 2 | import SqliteDatabase from './sqlite'; 3 | import PostgresqlDatabase from './postgresql'; 4 | import debug from 'debug'; 5 | 6 | // Debugger 7 | const D = debug('UnicornLoadBalancer'); 8 | 9 | let Database; 10 | 11 | if (config.database.mode === 'sqlite') { 12 | D('Using sqlite as database'); 13 | Database = SqliteDatabase; 14 | } else if (config.database.mode === 'postgresql') { 15 | D('Using postgresql as database'); 16 | Database = PostgresqlDatabase; 17 | } 18 | 19 | export default Database; -------------------------------------------------------------------------------- /src/database/postgresql.js: -------------------------------------------------------------------------------- 1 | import { Client } from 'pg'; 2 | import config from '../config'; 3 | 4 | let PostgresqlDatabase = {}; 5 | 6 | const _getClient = () => (new Promise(async (resolve, reject) => { 7 | const client = new Client({ 8 | user: config.database.postgresql.user, 9 | host: config.database.postgresql.host, 10 | database: config.database.postgresql.database, 11 | password: config.database.postgresql.password, 12 | port: config.database.postgresql.port, 13 | }) 14 | client.on('error', (err) => { 15 | return reject(err); 16 | }) 17 | await client.connect(); 18 | return resolve(client); 19 | })) 20 | 21 | PostgresqlDatabase.getPartFromId = (part_id) => (new Promise((resolve, reject) => { 22 | _getClient().then((client) => { 23 | client.query('SELECT * FROM media_parts WHERE id=$1 LIMIT 1', [part_id], (err, res) => { 24 | if (err) 25 | return reject(err); 26 | client.end() 27 | if (res.rows.length) { 28 | return resolve(res.rows[0]) 29 | } else { 30 | return reject('FILE_NOT_FOUND'); 31 | } 32 | }) 33 | }).catch((err) => { 34 | return reject('DATABASE_ERROR'); 35 | }) 36 | })) 37 | 38 | PostgresqlDatabase.getPartFromPath = (path) => (new Promise((resolve, reject) => { 39 | _getClient().then((client) => { 40 | client.query('SELECT * FROM media_parts WHERE file=$1 LIMIT 1', [path], (err, res) => { 41 | if (err) { 42 | return reject(err); 43 | } 44 | client.end() 45 | if (res.rows.length) { 46 | return resolve(res.rows[0]) 47 | } else { 48 | return reject('FILE_NOT_FOUND'); 49 | } 50 | }) 51 | }).catch((err) => { 52 | return reject('DATABASE_ERROR'); 53 | }) 54 | })) 55 | 56 | export default PostgresqlDatabase; -------------------------------------------------------------------------------- /src/database/sqlite.js: -------------------------------------------------------------------------------- 1 | import sqlite3 from 'sqlite3'; 2 | import config from '../config'; 3 | 4 | let SqliteDatabase = {}; 5 | 6 | SqliteDatabase.getPartFromId = (part_id) => (new Promise((resolve, reject) => { 7 | try { 8 | const db = new (sqlite3.verbose().Database)(config.database.sqlite.path); 9 | db.get('SELECT * FROM media_parts WHERE id=? LIMIT 0, 1', part_id, (err, row) => { 10 | if (row && row.file) 11 | resolve(row); 12 | else 13 | reject('FILE_NOT_FOUND'); 14 | db.close(); 15 | }); 16 | } 17 | catch (err) { 18 | return reject('DATABASE_ERROR'); 19 | } 20 | })) 21 | 22 | SqliteDatabase.getPartFromPath = (path) => (new Promise((resolve, reject) => { 23 | try { 24 | const db = new (sqlite3.verbose().Database)(config.database.sqlite.path); 25 | db.get('SELECT * FROM media_parts WHERE file=? LIMIT 0, 1', path, (err, row) => { 26 | if (row && row.file) 27 | resolve(row); 28 | else 29 | reject('FILE_NOT_FOUND'); 30 | db.close(); 31 | }); 32 | } 33 | catch (err) { 34 | return reject('DATABASE_ERROR'); 35 | } 36 | })) 37 | 38 | export default SqliteDatabase; -------------------------------------------------------------------------------- /src/routes/api.js: -------------------------------------------------------------------------------- 1 | import httpProxy from 'http-proxy'; 2 | import debug from 'debug'; 3 | 4 | import config from '../config'; 5 | import SessionStore from '../store'; 6 | import SessionsManager from '../core/sessions'; 7 | import ServersManager from '../core/servers'; 8 | import Database from '../database'; 9 | 10 | // Debugger 11 | const D = debug('UnicornLoadBalancer'); 12 | 13 | let RoutesAPI = {}; 14 | 15 | // Returns all the stats of all the transcoders 16 | RoutesAPI.stats = (req, res) => { 17 | res.send(ServersManager.list()); 18 | }; 19 | 20 | // Save the stats of a server 21 | RoutesAPI.update = (req, res) => { 22 | res.send(ServersManager.update(req.body)); 23 | }; 24 | 25 | // Catch the FFMPEG arguments 26 | // Body: {args: [], env: []} 27 | RoutesAPI.ffmpeg = async (req, res) => { 28 | if (!req.body || !req.body.arg || !req.body.env) 29 | return (res.status(400).send({ error: { code: 'INVALID_ARGUMENTS', message: 'Invalid UnicornFFMPEG parameters' } })); 30 | 31 | // Detect if we are in optimizer mode 32 | if (req.body.arg.filter(e => (e === '-segment_list' || e === '-manifest_name')).length === 0) { 33 | const parsedArgs = await SessionsManager.parseFFmpegParameters(req.body.arg, req.body.env, true); 34 | SessionsManager.ffmpegSetCache(parsedArgs.id, false); 35 | D('FFMPEG ' + parsedArgs.session + ' [OPTIMIZE]'); 36 | SessionsManager.saveSession(parsedArgs); 37 | SessionsManager.optimizerInit(parsedArgs); 38 | return (res.send(parsedArgs)); 39 | } 40 | // Streaming mode 41 | else { 42 | const parsedArgs = await SessionsManager.parseFFmpegParameters(req.body.arg, req.body.env); 43 | SessionsManager.ffmpegSetCache(parsedArgs.id, false); 44 | D('FFMPEG ' + parsedArgs.session + ' [STREAMING]'); 45 | SessionsManager.saveSession(parsedArgs) 46 | return (res.send(parsedArgs)); 47 | } 48 | }; 49 | 50 | // Get FFMPEG status 51 | RoutesAPI.ffmpegStatus = async (req, res) => { 52 | if (!req.params.id) 53 | return (res.status(400).send({ error: { code: 'INVALID_ARGUMENTS', message: 'Invalid parameters' } })); 54 | D('FFMPEG ' + req.params.id + ' [PING]'); 55 | return (res.send({ 56 | id: req.params.id, 57 | status: SessionsManager.ffmpegGetCache(req.params.id) 58 | })); 59 | }; 60 | 61 | // Resolve path from file id 62 | RoutesAPI.path = (req, res) => { 63 | Database.getPartFromId(req.params.id).then((data) => { 64 | res.send(JSON.stringify(data)); 65 | }).catch((err) => { 66 | res.status(400).send({ error: { code: 'FILE_NOT_FOUND', message: 'File not found in Plex Database' } }); 67 | }) 68 | }; 69 | 70 | // Proxy to Plex 71 | RoutesAPI.plex = (req, res) => { 72 | const proxy = httpProxy.createProxyServer({ 73 | target: { 74 | host: config.plex.host, 75 | port: config.plex.port 76 | } 77 | }).on('error', (err) => { 78 | if (err.code === 'HPE_UNEXPECTED_CONTENT_LENGTH') { 79 | return (res.status(200).send()); 80 | } 81 | res.status(400).send({ error: { code: 'PROXY_TIMEOUT', message: 'Plex not respond in time, proxy request fails' } }); 82 | }); 83 | req.url = req.url.slice('/api/plex'.length); 84 | return (proxy.web(req, res)); 85 | }; 86 | 87 | // Returns session 88 | RoutesAPI.session = (req, res) => { 89 | SessionStore.get(req.params.session).then((data) => { 90 | res.send(data); 91 | }).catch(() => { 92 | res.status(400).send({ error: { code: 'SESSION_TIMEOUT', message: 'The session wasn\'t launched in time, request fails' } }); 93 | }) 94 | }; 95 | 96 | // Optimizer finish 97 | RoutesAPI.optimize = (req, res) => { 98 | SessionStore.get(req.params.session).then((data) => { 99 | SessionsManager.optimizerDownload(data).then((parsedData) => { 100 | SessionsManager.optimizerDelete(parsedData); 101 | }); 102 | res.send(data); 103 | }).catch(() => { 104 | res.status(400).send({ error: { code: 'SESSION_TIMEOUT', message: 'Invalid session' } }); 105 | }) 106 | }; 107 | 108 | // Export all our API routes 109 | export default RoutesAPI; 110 | -------------------------------------------------------------------------------- /src/routes/index.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | 3 | import config from '../config'; 4 | import RoutesAPI from './api'; 5 | import RoutesTranscode from './transcode'; 6 | import RoutesProxy from './proxy'; 7 | import RoutesResize from './resize'; 8 | 9 | export default (app) => { 10 | 11 | // Note for future: 12 | // We NEED to 302/307 the chunk requests because if Plex catchs it with fake transcoder, it stucks 13 | 14 | // UnicornLoadBalancer API 15 | app.use('/api/sessions', express.static(config.plex.path.sessions)); 16 | app.get('/api/stats', RoutesAPI.stats); 17 | app.post('/api/ffmpeg', RoutesAPI.ffmpeg); 18 | app.get('/api/ffmpeg/:id', RoutesAPI.ffmpegStatus); 19 | app.get('/api/path/:id', RoutesAPI.path); 20 | app.post('/api/update', RoutesAPI.update); 21 | app.get('/api/session/:session', RoutesAPI.session); 22 | app.patch('/api/optimize/:session', RoutesAPI.optimize); 23 | app.all('/api/plex/*', RoutesAPI.plex); 24 | 25 | // MPEG Dash support 26 | app.get('/:formatType/:/transcode/universal/start.mpd', RoutesTranscode.dashStart); 27 | app.get('/:formatType/:/transcode/universal/dash/:sessionId/:streamId/initial.mp4', RoutesTranscode.redirect); 28 | app.get('/:formatType/:/transcode/universal/dash/:sessionId/:streamId/:partId.m4s', RoutesTranscode.redirect); 29 | 30 | // Long polling support 31 | app.get('/:formatType/:/transcode/universal/start', RoutesTranscode.lpStart); 32 | app.get('/:formatType/:/transcode/universal/subtitles', RoutesTranscode.redirect); 33 | 34 | // M3U8 support 35 | app.get('/:formatType/:/transcode/universal/start.m3u8', RoutesTranscode.hlsStart); 36 | app.get('/:formatType/:/transcode/universal/session/:sessionId/base/index.m3u8', RoutesTranscode.redirect); 37 | app.get('/:formatType/:/transcode/universal/session/:sessionId/base-x-mc/index.m3u8', RoutesTranscode.redirect); 38 | app.get('/:formatType/:/transcode/universal/session/:sessionId/:fileType/:partId.ts', RoutesTranscode.redirect); 39 | app.get('/:formatType/:/transcode/universal/session/:sessionId/:fileType/:partId.vtt', RoutesTranscode.redirect); 40 | 41 | // Control support 42 | app.get('/:formatType/:/transcode/universal/stop', RoutesTranscode.stop); 43 | app.get('/:formatType/:/transcode/universal/ping', RoutesTranscode.ping); 44 | app.get('/:/timeline', RoutesTranscode.timeline); 45 | 46 | // Download 47 | if (config.custom.download.forward) { 48 | app.get('/library/parts/:id1/:id2/file.*', RoutesTranscode.redirect); 49 | } 50 | if (!config.custom.download.forward) { 51 | app.get('/library/parts/:id1/:id2/file.*', RoutesTranscode.download); 52 | } 53 | 54 | // Image Proxy or Image Resizer 55 | if (config.custom.image.proxy) { 56 | app.get('/photo/:/transcode', RoutesResize.proxy); 57 | } 58 | 59 | // Forward other to Plex 60 | app.all('*', RoutesProxy.plex); 61 | }; 62 | -------------------------------------------------------------------------------- /src/routes/proxy.js: -------------------------------------------------------------------------------- 1 | import httpProxy from 'http-proxy'; 2 | import config from '../config'; 3 | 4 | let RoutesProxy = {}; 5 | 6 | RoutesProxy.plex = (req, res) => { 7 | const proxy = httpProxy.createProxyServer({ 8 | target: { 9 | host: config.plex.host, 10 | port: config.plex.port 11 | }, 12 | proxyTimeout: 60000, 13 | timeout: 60000, 14 | }).on('error', (err) => { 15 | // On some Plex request from FFmpeg, Plex don't create a valid request 16 | if (err.code === 'HPE_UNEXPECTED_CONTENT_LENGTH') 17 | return (res.status(200).send()); 18 | 19 | // Other error 20 | return (res.status(400).send({ error: { code: 'PROXY_TIMEOUT', message: 'Plex not respond in time, proxy request fails' } })); 21 | }); 22 | return (proxy.web(req, res)); 23 | }; 24 | 25 | RoutesProxy.ws = (req, res) => { 26 | const proxy = httpProxy.createProxyServer({ 27 | target: { 28 | host: config.plex.host, 29 | port: config.plex.port 30 | } 31 | }).on('error', () => { 32 | // Fail silently 33 | }); 34 | return (proxy.ws(req, res)); 35 | }; 36 | 37 | export default RoutesProxy; 38 | -------------------------------------------------------------------------------- /src/routes/resize.js: -------------------------------------------------------------------------------- 1 | import debug from 'debug'; 2 | import fetch from 'node-fetch'; 3 | import { publicUrl } from '../utils'; 4 | import { parseArguments } from '../core/images'; 5 | import config from '../config'; 6 | 7 | // Debugger 8 | const D = debug('UnicornLoadBalancer'); 9 | 10 | let RoutesResize = {}; 11 | 12 | /* Forward image request to the image transcode */ 13 | RoutesResize.proxy = (req, res) => { 14 | const params = parseArguments(req.query, publicUrl(), req.get('User-Agent')); 15 | const args = Object.keys(params).map(e => (`${e}=${encodeURIComponent(params[e])}`)).join('&'); 16 | const url = `${config.custom.image.proxy}?${args}`; 17 | fetch(url).then((fet) => { 18 | const headers = fet.headers.raw(); 19 | Object.keys(headers).forEach((h) => { 20 | res.set(h, headers[h][0]); 21 | }) 22 | return fet.buffer(); 23 | }).then((buf) => { 24 | res.send(buf); 25 | }).catch(err => { 26 | console.error(err); 27 | return res.status(400).send({ error: { code: 'RESIZE_ERROR', message: 'Invalid parameters, resize request fails' } }); 28 | }); 29 | } 30 | 31 | export default RoutesResize; 32 | -------------------------------------------------------------------------------- /src/routes/transcode.js: -------------------------------------------------------------------------------- 1 | import debug from 'debug'; 2 | import fetch from 'node-fetch'; 3 | import RoutesProxy from './proxy'; 4 | import Database from '../database'; 5 | import SessionsManager from '../core/sessions'; 6 | 7 | // Debugger 8 | const D = debug('UnicornLoadBalancer'); 9 | 10 | let RoutesTranscode = {}; 11 | 12 | /* Extract IP */ 13 | const getIp = (req) => { 14 | if (req.get('CF-Connecting-IP')) 15 | return req.get('CF-Connecting-IP'); 16 | if (req.get('x-forwarded-for')) 17 | return req.get('x-forwarded-for').split(',')[0]; 18 | return req.connection.remoteAddress 19 | }; 20 | 21 | /* Route to send a 307 to another server */ 22 | RoutesTranscode.redirect = async (req, res) => { 23 | const session = SessionsManager.getSessionFromRequest(req); 24 | const server = await SessionsManager.chooseServer(session, getIp(req)); 25 | if (server) { 26 | res.redirect(307, server + req.url); 27 | D('REDIRECT ' + session + ' [' + server + ']'); 28 | } else { 29 | res.status(500).send({ error: { code: 'SERVER_UNAVAILABLE', message: 'SERVER_UNAVAILABLE' } }); 30 | D('REDIRECT ' + session + ' [UNKNOWN]'); 31 | } 32 | }; 33 | 34 | /* Route called when a DASH stream starts */ 35 | RoutesTranscode.dashStart = (req, res) => { 36 | // By default we don't have the session identifier 37 | let sessionId = false; 38 | 39 | // If we have a cached X-Plex-Session-Identifier, we use it 40 | if (req.query['X-Plex-Session-Identifier'] && SessionsManager.getCacheSession(req.query['X-Plex-Session-Identifier'])) 41 | sessionId = SessionsManager.getCacheSession(req.query['X-Plex-Session-Identifier']); 42 | 43 | // Log 44 | D('START ' + SessionsManager.getSessionFromRequest(req) + ' [DASH]'); 45 | 46 | // Save session 47 | SessionsManager.cacheSessionFromRequest(req); 48 | 49 | // If session id available 50 | if (sessionId) 51 | SessionsManager.cleanSession(sessionId); 52 | 53 | // Redirect 54 | RoutesTranscode.redirect(req, res); 55 | } 56 | 57 | /* Routes called when a long polling stream starts */ 58 | RoutesTranscode.lpStart = (req, res) => { 59 | // Save session 60 | SessionsManager.cacheSessionFromRequest(req); 61 | 62 | // Get sessionId 63 | const sessionId = SessionsManager.getSessionFromRequest(req); 64 | 65 | // Log 66 | D('START ' + sessionId + ' [LP]'); 67 | 68 | // Redirect 69 | RoutesTranscode.redirect(req, res); 70 | } 71 | 72 | /* Route called when a HLS stream starts */ 73 | RoutesTranscode.hlsStart = (req, res) => { 74 | // Proxy to Plex 75 | RoutesProxy.plex(req, res); 76 | 77 | // Save session 78 | SessionsManager.cacheSessionFromRequest(req); 79 | 80 | // Get sessionId 81 | const sessionId = SessionsManager.getSessionFromRequest(req); 82 | 83 | // Log 84 | D('START ' + sessionId + ' [HLS]'); 85 | 86 | // If sessionId is defined 87 | if (sessionId) 88 | SessionsManager.cleanSession(sessionId); 89 | }; 90 | 91 | /* Route ping */ 92 | RoutesTranscode.ping = async (req, res) => { 93 | // Proxy to Plex 94 | RoutesProxy.plex(req, res); 95 | 96 | // Extract sessionId from request parameter 97 | const sessionId = SessionsManager.getSessionFromRequest(req); 98 | 99 | // Choose or get the server url 100 | const serverUrl = await SessionsManager.chooseServer(sessionId, getIp(req)); 101 | 102 | // If a server url is defined, we ping the session 103 | if (serverUrl) { 104 | D('PING ' + sessionId + ' [' + serverUrl + ']'); 105 | fetch(serverUrl + '/api/ping?session=' + sessionId); 106 | } else { 107 | D('PING ' + sessionId + ' [UNKNOWN]'); 108 | } 109 | }; 110 | 111 | /* Route timeline */ 112 | RoutesTranscode.timeline = async (req, res) => { 113 | // Proxy to Plex 114 | RoutesProxy.plex(req, res); 115 | 116 | // Extract sessionId from request parameter 117 | const sessionId = SessionsManager.getSessionFromRequest(req); 118 | 119 | // Choose or get the server url 120 | const serverUrl = await SessionsManager.chooseServer(sessionId, getIp(req)); 121 | 122 | // It's a stop request 123 | if (req.query.state === 'stopped') { 124 | // If a server url is defined, we stop the session 125 | if (serverUrl) { 126 | D('STOP ' + sessionId + ' [' + serverUrl + ']'); 127 | fetch(serverUrl + '/api/stop?session=' + sessionId); 128 | } else { 129 | D('STOP ' + sessionId + ' [UNKNOWN]'); 130 | } 131 | } 132 | // It's a ping request 133 | else { 134 | if (serverUrl) { 135 | D('PING ' + sessionId + ' [' + serverUrl + ']'); 136 | fetch(serverUrl + '/api/ping?session=' + sessionId); 137 | } else { 138 | D('PING ' + sessionId + ' [UNKNOWN]'); 139 | } 140 | } 141 | }; 142 | 143 | /* Route stop */ 144 | RoutesTranscode.stop = async (req, res) => { 145 | // Proxy to plex 146 | RoutesProxy.plex(req, res); 147 | 148 | // Extract sessionId from request parameter 149 | const sessionId = SessionsManager.getSessionFromRequest(req); 150 | 151 | // Choose or get the server url 152 | const serverUrl = await SessionsManager.chooseServer(sessionId, getIp(req)); 153 | 154 | // If a server url is defined, we stop the session 155 | if (serverUrl) { 156 | D('STOP ' + sessionId + ' [' + serverUrl + ']'); 157 | fetch(serverUrl + '/api/stop?session=' + sessionId); 158 | } else { 159 | D('STOP ' + sessionId + ' [UNKNOWN]'); 160 | } 161 | }; 162 | 163 | /* Route download */ 164 | RoutesTranscode.download = (req, res) => { 165 | D('DOWNLOAD ' + req.params.id1 + ' [LB]'); 166 | Database.getPartFromId(req.params.id1).then((data) => { 167 | res.sendFile(data.file, {}, (err) => { 168 | if (err && err.code !== 'ECONNABORTED') 169 | D('DOWNLOAD FAILED ' + req.params.id1 + ' [LB]'); 170 | }) 171 | }).catch((err) => { 172 | res.status(400).send({ error: { code: 'NOT_FOUND', message: 'File not available' } }); 173 | }) 174 | }; 175 | 176 | export default RoutesTranscode; 177 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import config from '../config'; 2 | import RedisSessionStore from './redis'; 3 | import LocalSessionStore from './local'; 4 | import debug from 'debug'; 5 | 6 | // Debugger 7 | const D = debug('UnicornLoadBalancer'); 8 | 9 | let SessionStore; 10 | 11 | if (config.redis.host !== 'undefined') { 12 | D('Using redis as session store'); 13 | SessionStore = new RedisSessionStore(); 14 | } else { 15 | D('Redis not found, fallback on LocalSessionStore'); 16 | D('WARNING: On restart all sessions will be lost'); 17 | SessionStore = new LocalSessionStore(); 18 | } 19 | 20 | export default SessionStore; 21 | -------------------------------------------------------------------------------- /src/store/local.js: -------------------------------------------------------------------------------- 1 | import EventEmitter from 'events'; 2 | 3 | class LocalSessionStore { 4 | constructor() { 5 | this.sessionEvents = new EventEmitter(); 6 | this.sessionStore = {}; 7 | } 8 | 9 | /** 10 | * Get a session, or wait for it for 10s 11 | * @param sessionId 12 | * @returns {Promise} 13 | */ 14 | get(sessionId) { 15 | return new Promise((resolve, reject) => { 16 | if (sessionId in this.sessionStore) 17 | return resolve(this.sessionStore[sessionId]); 18 | 19 | let timeout = null; 20 | 21 | let eventCb = (...args) => { 22 | clearTimeout(timeout); 23 | this.sessionEvents.removeListener(sessionId, eventCb); 24 | resolve(...args); 25 | }; 26 | 27 | let timeoutCb = () => { 28 | this.sessionEvents.removeListener(sessionId, eventCb); 29 | reject('timeout'); 30 | }; 31 | 32 | timeout = setTimeout(timeoutCb, 20000); 33 | this.sessionEvents.on(sessionId, eventCb); 34 | }) 35 | } 36 | 37 | /** 38 | * Store a value in the store and trigger the pending gets 39 | * @param sessionId 40 | * @param value 41 | * @returns {Promise} 42 | */ 43 | set(sessionId, value) { 44 | return new Promise((resolve) => { 45 | this.sessionStore[sessionId] = value; 46 | this.sessionEvents.emit(sessionId, value); 47 | resolve('OK'); 48 | }) 49 | } 50 | 51 | /** 52 | * Delete a session from the store 53 | * @param sessionId 54 | * @returns {Promise} 55 | */ 56 | delete(sessionId) { 57 | return new Promise((resolve) => { 58 | delete this.sessionStore[sessionId]; 59 | resolve('OK'); 60 | }) 61 | } 62 | } 63 | 64 | export default LocalSessionStore; -------------------------------------------------------------------------------- /src/store/redis.js: -------------------------------------------------------------------------------- 1 | import {getRedisClient} from '../utils'; 2 | import config from "../config"; 3 | 4 | class RedisSessionStore { 5 | constructor() { 6 | this.redis = getRedisClient(); 7 | this.redisSubscriber = this.redis.duplicate(); 8 | } 9 | 10 | _parseSession(session) { 11 | return new Promise((resolve, reject) => { 12 | try { 13 | resolve(JSON.parse(session)) 14 | } catch(err) { 15 | reject(err) 16 | } 17 | }) 18 | } 19 | 20 | /** 21 | * Get a session, or wait for it for 10s 22 | * @param sessionId 23 | * @returns {Promise} 24 | */ 25 | get(sessionId) { 26 | return new Promise((resolve, reject) => { 27 | this.redis.get(sessionId, (err, session) => { 28 | if (err) 29 | return reject(err); 30 | if (session != null) 31 | return resolve(this._parseSession(session)); 32 | 33 | let redisSubKey = "__keyspace@" + config.redis.db + "__:" + sessionId; 34 | 35 | let timeout = setTimeout(() => { 36 | this.redisSubscriber.unsubscribe(redisSubKey); 37 | reject('timeout'); 38 | }, 20000); 39 | 40 | this.redisSubscriber.on("message", (eventKey, action) => { 41 | if (action !== 'set' || eventKey !== redisSubKey) 42 | return; 43 | 44 | clearTimeout(timeout); 45 | this.redisSubscriber.unsubscribe(redisSubKey); 46 | this.redis.get(sessionId, (err, session) => { 47 | if (err) 48 | return reject(err); 49 | return resolve(this._parseSession(session)); 50 | }) 51 | }); 52 | this.redisSubscriber.subscribe(redisSubKey) 53 | }) 54 | }) 55 | } 56 | 57 | /** 58 | * Store a value in the store and trigger the pending gets 59 | * @param sessionId 60 | * @param value 61 | * @returns {Promise} 62 | */ 63 | set(sessionId, value) { 64 | return new Promise((resolve, reject) => { 65 | this.redis.set(sessionId, JSON.stringify(value), (err) => { 66 | if (err) 67 | return reject(err); 68 | resolve('OK'); 69 | }) 70 | }) 71 | } 72 | 73 | /** 74 | * Delete a session from the store 75 | * @param sessionId 76 | * @returns {Promise} 77 | */ 78 | delete(sessionId) { 79 | return new Promise((resolve, reject) => { 80 | this.redis.del(sessionId, (err) => { 81 | if (err) 82 | return reject(err); 83 | resolve('OK') 84 | }) 85 | }) 86 | } 87 | } 88 | 89 | export default RedisSessionStore; -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | import redisClient from 'redis'; 2 | import fs from 'fs'; 3 | import fetch from 'node-fetch'; 4 | import mkdirp from 'mkdirp'; 5 | import config from './config'; 6 | 7 | export const publicUrl = () => { 8 | return (config.server.public) 9 | }; 10 | 11 | export const internalUrl = () => { 12 | return ('http://127.0.0.1:' + config.server.port + '/') 13 | }; 14 | 15 | export const plexUrl = () => { 16 | return ('http://' + config.plex.host + ':' + config.plex.port + '/') 17 | }; 18 | 19 | export const getRedisClient = () => { 20 | if (config.redis.password === '') 21 | delete config.redis.password; 22 | 23 | let redis = redisClient.createClient(config.redis); 24 | redis.on('error', (err) => { 25 | if (err.errno === 'ECONNREFUSED') 26 | return console.error('Failed to connect to REDIS, please check your configuration'); 27 | return console.error(err.errno); 28 | }); 29 | 30 | redis.on('connect', () => { 31 | redis.send_command('config', ['set', 'notify-keyspace-events', 'KEA']) 32 | }); 33 | return redis; 34 | }; 35 | 36 | export const time = () => (Math.floor((new Date().getTime()) / 1000)); 37 | 38 | export const download = (url, filepath) => (new Promise(async (resolve, reject) => { 39 | const res = await fetch(url); 40 | const fileStream = fs.createWriteStream(filepath); 41 | res.body.pipe(fileStream); 42 | res.body.on("error", (err) => { 43 | reject(err); 44 | }); 45 | fileStream.on("finish", () => { 46 | resolve(); 47 | }); 48 | })); 49 | 50 | export const replaceAll = (input, search, replace) => { 51 | let str = input; 52 | while (str.indexOf(search) !== -1) 53 | str = str.replace(search, replace); 54 | return str 55 | }; --------------------------------------------------------------------------------