├── .dockerignore ├── .eslintrc.json ├── .gitignore ├── Dockerfile ├── README.md ├── dist ├── HttpClient.js ├── HttpClient.js.map ├── PornClient.js ├── PornClient.js.map ├── adapters │ ├── BaseAdapter.js │ ├── BaseAdapter.js.map │ ├── Chaturbate.js │ ├── Chaturbate.js.map │ ├── EPorner.js │ ├── EPorner.js.map │ ├── HubTrafficAdapter.js │ ├── HubTrafficAdapter.js.map │ ├── PornCom.js │ ├── PornCom.js.map │ ├── PornHub.js │ ├── PornHub.js.map │ ├── RedTube.js │ ├── RedTube.js.map │ ├── SpankWire.js │ ├── SpankWire.js.map │ ├── YouPorn.js │ └── YouPorn.js.map ├── index.js └── index.js.map ├── package.json ├── src ├── HttpClient.js ├── PornClient.js ├── adapters │ ├── BaseAdapter.js │ ├── Chaturbate.js │ ├── EPorner.js │ ├── HubTrafficAdapter.js │ ├── PornCom.js │ ├── PornHub.js │ ├── RedTube.js │ ├── SpankWire.js │ └── YouPorn.js └── index.js ├── static ├── bg.jpg ├── logo.png └── screenshot_discover.jpg ├── tests ├── adapters │ ├── Chaturbate │ │ ├── Chaturbate.test.js │ │ ├── itemPage.html │ │ └── listPage.html │ ├── EPorner │ │ ├── EPorner.test.js │ │ ├── apiResponse.xml │ │ └── moviePage.html │ ├── PornCom │ │ ├── PornCom.test.js │ │ ├── apiResponse.xml │ │ └── embedPage.xml │ ├── PornHub │ │ ├── PornHub.test.js │ │ └── embeddedMoviePage.html │ ├── RedTube │ │ ├── RedTube.test.js │ │ └── embeddedVideoPage.html │ ├── SpankWire │ │ ├── SpankWire.test.js │ │ └── embeddedMoviePage.html │ ├── YouPorn │ │ ├── YouPorn.test.js │ │ └── embeddedMoviePage.html │ └── testAdapter.js └── index.test.js └── yarn.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | ./** 2 | !dist/** 3 | !static/** 4 | !package.json 5 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint:recommended", 4 | "plugin:import/errors", 5 | "plugin:jest/recommended" 6 | ], 7 | "parser": "babel-eslint", 8 | "parserOptions": { 9 | "ecmaVersion": 7, 10 | "sourceType": "module", 11 | "codeFrame": false 12 | }, 13 | "env": { 14 | "es6": true, 15 | "node": true, 16 | "jest": true 17 | }, 18 | "rules": { 19 | "no-constant-condition": "off", 20 | "no-empty": ["error", { 21 | "allowEmptyCatch": true 22 | }], 23 | "array-callback-return": "error", 24 | "curly": "error", 25 | "dot-location": ["error", "property"], 26 | "eqeqeq": "error", 27 | "no-alert": "error", 28 | "no-caller": "error", 29 | "no-eval": "error", 30 | "no-extend-native": "error", 31 | "no-floating-decimal": "error", 32 | "no-implicit-globals": "error", 33 | "no-implicit-coercion": "error", 34 | "no-implied-eval": "error", 35 | "no-iterator": "error", 36 | "no-loop-func": "error", 37 | "no-multi-spaces": "error", 38 | "no-proto": "error", 39 | "no-return-assign": "error", 40 | "no-return-await": "error", 41 | "no-self-compare": "error", 42 | "no-sequences": "error", 43 | "no-throw-literal": "error", 44 | "no-useless-call": "error", 45 | "no-useless-concat": "error", 46 | "no-useless-escape": "error", 47 | "no-useless-return": "error", 48 | "no-void": "error", 49 | "no-warning-comments": "warn", 50 | "no-with": "error", 51 | "prefer-promise-reject-errors": "error", 52 | "wrap-iife": "error", 53 | "yoda": "error", 54 | "no-undef-init": "error", 55 | "no-use-before-define": [ 56 | "error", 57 | { 58 | "functions": false, 59 | "classes": false, 60 | "variables": true 61 | } 62 | ], 63 | "global-require": "error", 64 | "array-bracket-spacing": "error", 65 | "block-spacing": "error", 66 | "brace-style": "error", 67 | "camelcase": "error", 68 | "comma-dangle": ["error", { 69 | "arrays": "always-multiline", 70 | "objects": "always-multiline", 71 | "imports": "never", 72 | "exports": "never", 73 | "functions": "never" 74 | }], 75 | "comma-spacing": "error", 76 | "comma-style": "error", 77 | "computed-property-spacing": "error", 78 | "consistent-this": ["error", "self"], 79 | "eol-last": "error", 80 | "func-call-spacing": "error", 81 | "indent": ["error", 2, { 82 | "SwitchCase": 1 83 | }], 84 | "jsx-quotes": "error", 85 | "key-spacing": "error", 86 | "keyword-spacing": "error", 87 | "linebreak-style": "error", 88 | "lines-around-directive": "error", 89 | "max-depth": ["error", 4], 90 | "max-len": ["error", { 91 | "code": 80, 92 | "ignoreStrings": true 93 | }], 94 | "max-params": ["error", 5], 95 | "new-cap": "error", 96 | "new-parens": "error", 97 | "no-lonely-if": "error", 98 | "no-mixed-operators": "error", 99 | "no-multi-assign": "error", 100 | "no-multiple-empty-lines": ["error", { 101 | "max": 2, 102 | "maxEOF": 0 103 | }], 104 | "no-nested-ternary": "error", 105 | "no-tabs": "error", 106 | "no-trailing-spaces": "error", 107 | "no-unneeded-ternary": "error", 108 | "no-whitespace-before-property": "error", 109 | "object-curly-spacing": ["error", "always"], 110 | "one-var-declaration-per-line": "error", 111 | "operator-linebreak": ["error", "after"], 112 | "quote-props": ["error", "as-needed"], 113 | "quotes": ["error", "single"], 114 | "semi": ["error", "never"], 115 | "space-before-blocks": ["error", "always"], 116 | "space-before-function-paren": ["error", { 117 | "anonymous": "never", 118 | "named": "never", 119 | "asyncArrow": "always" 120 | }], 121 | "space-in-parens": "error", 122 | "space-infix-ops": "error", 123 | "space-unary-ops": ["error", { 124 | "words": true, 125 | "nonwords": false 126 | }], 127 | "spaced-comment": "error", 128 | "template-tag-spacing": "error", 129 | "unicode-bom": "error", 130 | "arrow-parens": "error", 131 | "arrow-spacing": "error", 132 | "generator-star-spacing": ["error", "after"], 133 | "no-class-assign": "error", 134 | "no-confusing-arrow": "error", 135 | "no-duplicate-imports": "error", 136 | "no-new-symbol": "error", 137 | "no-useless-computed-key": "error", 138 | "no-useless-constructor": "error", 139 | "no-var": "error", 140 | "prefer-arrow-callback": ["error", { 141 | "allowNamedFunctions": true 142 | }], 143 | "prefer-rest-params": "error", 144 | "prefer-template": "error", 145 | "rest-spread-spacing": "error", 146 | "symbol-description": "error", 147 | "template-curly-spacing": "error", 148 | "yield-star-spacing": "error", 149 | "import/newline-after-import": ["error", { 150 | "count": 2 151 | }], 152 | "import/order": ["error"] 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | wip 2 | node_modules 3 | .DS_Store 4 | Thumbs.db 5 | .idea 6 | .vs 7 | .vscode 8 | *.log 9 | .npmrc 10 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:alpine 2 | 3 | WORKDIR /var/stremio_addon 4 | # The exact files included are controlled by .dockerignore 5 | COPY . . 6 | RUN npm install --only=prod --no-package-lock 7 | 8 | CMD node dist/index.js 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 |

Porn Addon for Stremio

5 |

6 | Time to unsheathe your sword! 7 |

8 | 9 | This is a [Stremio](https://www.stremio.com/) addon that provides porn content from various websites: 10 | 11 | - __Videos__ _(Movies)_: PornHub, RedTube, YouPorn, SpankWire and Porn.com 12 | - __Webcam streams__ _(TV Channels)_: Chaturbate 13 | 14 | 15 | ## Features 16 | 17 | - Adds a dedicated tab in Discover for each website 18 | - Works in Stremio v4 and v3.6 19 | - Supports Docker out of the box 20 | - Caches results in memory or Redis 21 | - Limits the number of concurrent requests to avoid overloading the sites 22 | - Supports HTTPS proxy 23 | - Configurable via environment variables 24 | - Prints a nicely formatted status message when run 25 | - The logo is dope 🗡💖 26 | 27 | 28 | ## Running 29 | 30 | The addon is a web server that fetches video streams from the porn sites in response to requests from Stremio clients. It uses environment variables for configuration and includes a handful of npm scripts to run with or without Docker. 31 | 32 | To install and quickly start the addon, do: 33 | 34 | ```bash 35 | git clone https://github.com/naughty-doge/stremio-porn 36 | cd stremio-porn 37 | yarn # or `npm install` 38 | yarn start # or `npm start` 39 | ``` 40 | 41 | By default the server starts on `localhost:80` in development mode and doesn't announce itself to the Stremio addon tracker. To add the addon to Stremio, open its endpoint in the browser and click the Install button, or enter the URL in the app's Addons section. 42 | 43 | In order for the addon to work publicly, the following environment variables must be set: 44 | - `NODE_ENV` to `production` 45 | - `STREMIO_PORN_ENDPOINT` to a public URL of the server 46 | - `STREMIO_PORN_ID` to a non-default value 47 | 48 | Note: since this addon scrapes pages, it is recommended to run it behind a proxy and use Redis caching. 49 | 50 | 51 | ## Development 52 | 53 | The code is written in ES7 and then transpiled with Babel. It is covered by a suite of Jest tests, and the staged files are automatically linted with ESLint. The transpiled files are included in the repository: this makes for quicker start and eases deployment to different environments such as Docker and Heroku. 54 | 55 | 56 | ## npm scripts 57 | 58 | Each of these scripts can be used with `yarn
10 | -------------------------------------------------------------------------------- /tests/adapters/PornHub/PornHub.test.js: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'fs' 2 | import testAdapter from '../testAdapter' 3 | import PornHub from '../../../src/adapters/PornHub' 4 | 5 | 6 | const EMBED_PAGE = readFileSync(`${__dirname}/embeddedMoviePage.html`, 'utf8') 7 | 8 | const ITEMS = [{ 9 | id: 'ph598cafd0ca22e', 10 | type: 'movie', 11 | streams: ['201708/10/128079091'], 12 | match: { 13 | name: 'Amateur Girl Fucked With Cum On Face / Fucked After Facial', 14 | }, 15 | }, { 16 | id: 'ph5a94a343e79fe', 17 | type: 'movie', 18 | streams: ['201802/27/156159022'], 19 | match: { 20 | name: 'POV edging blowjob tongue part 2', 21 | }, 22 | }] 23 | 24 | 25 | describe('PornHub', () => { 26 | testAdapter(PornHub, ITEMS) 27 | 28 | describe('#_extractStreamsFromEmbed()', () => { 29 | test('retrieves a stream from a sample embedded movie page', () => { 30 | let adapter = new PornHub() 31 | let result = adapter._extractStreamsFromEmbed(EMBED_PAGE) 32 | 33 | expect(result).toEqual([{ 34 | url: 'https://de.phncdn.com/videos/201503/28/46795732/vl_480_493k_46795732.mp4?ttl=1522227092&ri=1228800&rs=696&hash=268b5f4d76927209ef554ac9e93c6c85', 35 | }]) 36 | }) 37 | }) 38 | }) 39 | -------------------------------------------------------------------------------- /tests/adapters/RedTube/RedTube.test.js: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'fs' 2 | import testAdapter from '../testAdapter' 3 | import RedTube from '../../../src/adapters/RedTube' 4 | 5 | 6 | const EMBED_PAGE = readFileSync(`${__dirname}/embeddedVideoPage.html`, 'utf8') 7 | 8 | const ITEMS = [{ 9 | id: 1, 10 | type: 'movie', 11 | streams: ['201208/31/1/480p_600k_1.mp4'], 12 | match: { 13 | name: 'Heather taking it deep again', 14 | }, 15 | }, { 16 | id: 4848071, 17 | type: 'movie', 18 | streams: ['201803/08/4848071/480P_600K_4848071.mp4'], 19 | match: { 20 | name: 'Brother Caught Redhead Step-Sister Masturbate and Fuck Anal', 21 | }, 22 | }] 23 | 24 | 25 | describe('RedTube', () => { 26 | testAdapter(RedTube, ITEMS) 27 | 28 | describe('#_extractStreamsFromEmbed()', () => { 29 | test('retrieves a stream from a sample embedded video page', () => { 30 | let adapter = new RedTube() 31 | let result = adapter._extractStreamsFromEmbed(EMBED_PAGE) 32 | 33 | expect(result).toEqual([{ 34 | quality: '480p', 35 | url: 'https://ce.rdtcdn.com/media/videos/201803/08/4848071/480P_600K_4848071.mp4?a5dcae8e1adc0bdaed975f0d66fb5e0568d9f5b553250a40db6040349e33a09c6fe9df21d2172658c3212e4a12a1aa10d7abea9c9e32593783053be05a3d5ee05e116588562463e0e6234de008e847b568c7c15d714814801dc24012fb8cf118017f49853398246c7335d1d54773a963ab867f31244ca5ba17067b7bae', 36 | }]) 37 | }) 38 | }) 39 | }) 40 | -------------------------------------------------------------------------------- /tests/adapters/SpankWire/SpankWire.test.js: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'fs' 2 | import testAdapter from '../testAdapter' 3 | import SpankWire from '../../../src/adapters/SpankWire' 4 | 5 | 6 | const EMBED_PAGE = readFileSync(`${__dirname}/embeddedMoviePage.html`, 'utf8') 7 | 8 | const ITEMS = [{ 9 | id: '16838912', 10 | type: 'movie', 11 | streams: true, 12 | match: { 13 | name: 'Pervert hd first time Did you ever wonder what happens when a red-hot', 14 | }, 15 | }, { 16 | id: '9423892', 17 | type: 'movie', 18 | streams: true, 19 | match: { 20 | name: 'BDSM and Bondage teen slave fucked by master domination', 21 | }, 22 | }] 23 | 24 | 25 | describe('SpankWire', () => { 26 | testAdapter(SpankWire, ITEMS) 27 | 28 | describe('#_extractStreamsFromEmbed()', () => { 29 | test('retrieves a stream from a sample embedded movie page', () => { 30 | let adapter = new SpankWire() 31 | let results = adapter._extractStreamsFromEmbed(EMBED_PAGE) 32 | 33 | expect(results).toEqual([{ 34 | quality: 'Normal', 35 | url: 'https://cdn1-embed-extremetube.spankcdn.net/media//201804/29/24260991/mp4_normal_24260991.mp4?validfrom=1524996113&validto=1525003313&rate=34k&burst=2000k&hash=d3lzXN0Tx0e9%2BId7wp%2Bf1T8Momo%3D', 36 | }, { 37 | quality: 'High', 38 | url: 'https://cdn1-embed-extremetube.spankcdn.net/media//201804/29/24260991/mp4_high_24260991.mp4?validfrom=1524996113&validto=1525003313&rate=43k&burst=2000k&hash=%2FjtfI%2FozorkRmIENVbnsoN2m29c%3D', 39 | }, { 40 | quality: 'Ultra', 41 | url: 'https://cdn1-embed-extremetube.spankcdn.net/media//201804/29/24260991/mp4_ultra_24260991.mp4?validfrom=1524996113&validto=1525003313&rate=65k&burst=2000k&hash=qmx%2BLwYFc3pQRjYmeCKSlNP5ho4%3D', 42 | }, { 43 | quality: '720p', 44 | url: 'https://cdn1-embed-extremetube.spankcdn.net/media//201804/29/24260991/mp4_720p_24260991.mp4?validfrom=1524996113&validto=1525003313&rate=141k&burst=2000k&hash=8lag09lM%2BHc%2F%2Frgi4Kcc6gObcr4%3D', 45 | }]) 46 | }) 47 | }) 48 | }) 49 | -------------------------------------------------------------------------------- /tests/adapters/SpankWire/embeddedMoviePage.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Spankwire Embed Player 6 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 |
30 |
31 | 41 |
42 | 43 | 51 |
52 | 53 | 188 | 189 | 200 | 201 | -------------------------------------------------------------------------------- /tests/adapters/YouPorn/YouPorn.test.js: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'fs' 2 | import testAdapter from '../testAdapter' 3 | import YouPorn from '../../../src/adapters/YouPorn' 4 | 5 | 6 | const EMBED_PAGE = readFileSync(`${__dirname}/embeddedMoviePage.html`, 'utf8') 7 | 8 | const ITEMS = [{ 9 | id: '11822513', 10 | type: 'movie', 11 | streams: true, 12 | match: { 13 | name: 'Hot brunette gang bang party. Having fun with Kylie i met via DATES25.COM', 14 | }, 15 | }, { 16 | id: '13745019', 17 | type: 'movie', 18 | streams: true, 19 | match: { 20 | name: 'Teenage Shoplifter Sucks Cock To Avoid Arrest Outrageous Footage', 21 | }, 22 | }] 23 | 24 | 25 | describe('YouPorn', () => { 26 | testAdapter(YouPorn, ITEMS) 27 | 28 | describe('#_extractStreamsFromEmbed()', () => { 29 | test('retrieves a stream from a sample embedded movie page', () => { 30 | let adapter = new YouPorn() 31 | let results = adapter._extractStreamsFromEmbed(EMBED_PAGE) 32 | 33 | expect(results).toEqual([{ 34 | quality: '720p', 35 | url: 'https://ee.ypncdn.com/201709/01/14062051/720p_1500k_14062051/YouPorn_-_mia-khalifa-big-tits-arab-pornstar-takes-a-fan-s-virginity.mp4?rate=193k&burst=1400k&validfrom=1524765800&validto=1524780200&hash=EGRxkAOZwod648gfnITHeyb%2Fzi8%3D', 36 | }, { 37 | quality: '480p', 38 | url: 'https://ee.ypncdn.com/201709/01/14062051/480p_750k_14062051/YouPorn_-_mia-khalifa-big-tits-arab-pornstar-takes-a-fan-s-virginity.mp4?rate=118k&burst=1400k&validfrom=1524765800&validto=1524780200&hash=BPhhTG9iIKFHlZHVJWQtGUuyk9I%3D', 39 | }, { 40 | quality: '240p', 41 | url: 'https://ee.ypncdn.com/201709/01/14062051/240p_240k_14062051/YouPorn_-_mia-khalifa-big-tits-arab-pornstar-takes-a-fan-s-virginity.mp4?rate=59k&burst=1400k&validfrom=1524765800&validto=1524780200&hash=gAETeLQVKHf3uNn3%2FzgV0qv%2BcI0%3D', 42 | }]) 43 | }) 44 | }) 45 | }) 46 | -------------------------------------------------------------------------------- /tests/adapters/YouPorn/embeddedMoviePage.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | MIA KHALIFA - Big Tits Arab Pornstar Takes A Fan's Virginity 7 | 8 | 9 | 13 | 14 | 29 | 30 | 31 | 32 | 44 | 53 | 54 | 55 | 56 |
57 | 60 |
61 |
MIA KHALIFA - Big Tits Arab Pornstar Takes A Fan's Virginity 62 | 210 | 211 | 212 | 213 | -------------------------------------------------------------------------------- /tests/adapters/testAdapter.js: -------------------------------------------------------------------------------- 1 | import HttpClient from '../../src/HttpClient' 2 | 3 | 4 | function testAdapter(AdapterClass, items = []) { 5 | describe('@integration', () => { 6 | jest.setTimeout(20000) 7 | 8 | let adapter 9 | 10 | beforeEach(() => { 11 | let httpClient = new HttpClient({ 12 | proxy: process.env.STREMIO_PORN_PROXY, 13 | }) 14 | adapter = new AdapterClass(httpClient) 15 | }) 16 | 17 | describe('#find()', () => { 18 | test('when no request query is provided, returns trending items', async () => { 19 | let type = AdapterClass.SUPPORTED_TYPES[0] 20 | let results = await adapter.find({ 21 | query: { type }, 22 | }) 23 | 24 | expect(results.length).toBeGreaterThan(0) 25 | results.forEach((result) => { 26 | expect(result.id).toBeTruthy() 27 | }) 28 | }) 29 | 30 | test('when a search string is provided, returns matching items', async () => { 31 | let search = 'deep' 32 | let limit = 3 33 | let type = AdapterClass.SUPPORTED_TYPES[0] 34 | let results = await adapter.find({ 35 | query: { search, type }, 36 | limit, 37 | }) 38 | 39 | expect(results.length).toBeLessThanOrEqual(limit) 40 | results.forEach((result) => { 41 | expect(result.id).toBeTruthy() 42 | }) 43 | }) 44 | }) 45 | 46 | describe('#getItem()', () => { 47 | items 48 | .filter((item) => item.match) 49 | .forEach(({ id, type, match }) => { 50 | test(`retrieves ${type} ${id}`, async () => { 51 | let query = { type, id } 52 | let [result] = await adapter.getItem({ query }) 53 | 54 | expect(result).toMatchObject(match) 55 | }) 56 | }) 57 | }) 58 | 59 | describe('#getStreams()', () => { 60 | items 61 | .filter((item) => item.streams === true) 62 | .forEach(({ id, type }) => { 63 | test(`doesn't throw for ${type} ${id}`, async () => { 64 | let query = { type, id } 65 | return adapter.getStreams({ query }) 66 | }) 67 | }) 68 | 69 | items 70 | .filter((item) => Array.isArray(item.streams)) 71 | .forEach(({ id, type, streams }) => { 72 | test(`retrieves streams for ${type} ${id}`, async () => { 73 | let query = { type, id } 74 | let results = await adapter.getStreams({ query }) 75 | 76 | expect(results).toHaveLength(streams.length) 77 | streams.forEach((stream) => { 78 | let includesStream = Boolean(results.find((result) => { 79 | return result.url.includes(stream) 80 | })) 81 | expect(includesStream).toBe(true) 82 | }) 83 | }) 84 | }) 85 | }) 86 | }) 87 | } 88 | 89 | export default testAdapter 90 | -------------------------------------------------------------------------------- /tests/index.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | import { get } from 'http' 4 | import { Client as AddonClient } from 'stremio-addons' 5 | 6 | 7 | jest.mock('../src/PornClient') 8 | 9 | // Prevent the addon from printing 10 | // eslint-disable-next-line no-unused-vars 11 | let log = console.log 12 | console.log = () => {} 13 | console.error = () => {} 14 | 15 | function reset() { 16 | jest.resetModules() 17 | 18 | delete process.env.STREMIO_PORN_ID 19 | delete process.env.STREMIO_PORN_ENDPOINT 20 | delete process.env.STREMIO_PORN_PORT 21 | delete process.env.STREMIO_PORN_PROXY 22 | delete process.env.STREMIO_PORN_CACHE 23 | delete process.env.STREMIO_PORN_EMAIL 24 | delete process.env.NODE_ENV 25 | } 26 | 27 | function initAddon() { 28 | return { 29 | start() { 30 | // eslint-disable-next-line global-require 31 | this.server = require('../src/index').default 32 | 33 | // In case an error occurs before the server starts (e.g. port is in use), 34 | // it silently fails and the tests stall 35 | return new Promise((resolve, reject) => { 36 | this.server.once('listening', () => resolve(this)) 37 | this.server.once('error', (err) => { 38 | reject(err) 39 | this.stop() 40 | }) 41 | }) 42 | }, 43 | 44 | stop() { 45 | if (!this.server) { 46 | return Promise.resolve(this) 47 | } 48 | 49 | let stopPromise = new Promise((resolve) => { 50 | this.server.once('close', () => resolve(this)) 51 | }) 52 | this.server.close() 53 | return stopPromise 54 | }, 55 | } 56 | } 57 | 58 | describe('Addon @integration', () => { 59 | let addonClient 60 | let addon 61 | 62 | beforeAll(() => { 63 | addonClient = new AddonClient() 64 | addonClient.add('http://localhost') 65 | }) 66 | 67 | beforeEach(() => { 68 | reset() 69 | addon = initAddon() 70 | }) 71 | 72 | afterEach(() => { 73 | return addon.stop() 74 | }) 75 | 76 | test('When a port is not specified, starts a web server on port 80', async () => { 77 | await addon.start() 78 | expect(addon.server.address().port).toBe(80) 79 | }) 80 | 81 | test('When a port is specified, starts a web server on it', async () => { 82 | process.env.STREMIO_PORN_PORT = '9028' 83 | await addon.start() 84 | expect(addon.server.address().port).toBe(9028) 85 | }) 86 | 87 | test('meta.get is implemented', async (done) => { 88 | await addon.start() 89 | 90 | addonClient.meta.get({}, (err) => { 91 | err ? done.fail(err) : done() 92 | }) 93 | }) 94 | 95 | test('meta.find is implemented', async (done) => { 96 | await addon.start() 97 | 98 | addonClient.meta.find({}, (err) => { 99 | err ? done.fail(err) : done() 100 | }) 101 | }) 102 | 103 | test('meta.search is implemented', async (done) => { 104 | await addon.start() 105 | 106 | addonClient.meta.search({}, (err) => { 107 | err ? done.fail(err) : done() 108 | }) 109 | }) 110 | 111 | test('stream.find is implemented', async (done) => { 112 | await addon.start() 113 | 114 | addonClient.stream.find({}, (err) => { 115 | err ? done.fail(err) : done() 116 | }) 117 | }) 118 | 119 | test('The main page is accessible', async () => { 120 | await addon.start() 121 | let res = await new Promise((resolve) => { 122 | get('http://localhost', resolve) 123 | }) 124 | expect(res.statusCode).toBe(200) 125 | }) 126 | 127 | test('The static files are accessible', async () => { 128 | await addon.start() 129 | let staticFiles = [ 130 | 'logo.png', 131 | 'screenshot_discover.jpg', 132 | 'bg.jpg', 133 | ] 134 | let promises = staticFiles.map((file) => { 135 | return new Promise((resolve) => { 136 | get(`http://localhost/${file}`, resolve) 137 | }) 138 | }) 139 | let responses = await Promise.all(promises) 140 | 141 | responses.forEach((res) => { 142 | // Requests to non-existent files return the landing page, 143 | // so we check that the response is not HTML 144 | let contentType = res.headers['content-type'].split(';')[0] 145 | expect(contentType).not.toBe('text/html') 146 | expect(res.statusCode).toBe(200) 147 | }) 148 | }) 149 | }) 150 | --------------------------------------------------------------------------------