├── .travis.yml ├── .gitignore ├── README.md ├── docs ├── tutorial │ ├── scaffolding.md │ ├── using-in-browser.md │ ├── hosting.md │ ├── publishing.md │ ├── testing.md │ ├── add.to.app.md │ └── using-cinemeta.md ├── api │ ├── meta │ │ ├── README.md │ │ ├── content.types.md │ │ ├── meta.genres.md │ │ ├── meta.request.md │ │ ├── meta.get.md │ │ ├── meta.search.md │ │ ├── meta.find.md │ │ └── meta.element.md │ ├── subtitles │ │ ├── subtitles.object.md │ │ ├── subtitles.find.md │ │ └── README.md │ ├── repositories.md │ ├── stream │ │ ├── README.md │ │ └── stream.response.md │ └── manifest.md ├── BENEFITS.md └── README.md ├── index.js ├── lib ├── transports │ ├── index.js │ ├── http.js │ ├── ipfs.js │ └── legacy.js ├── util │ └── promisify.js ├── errors.js ├── client.js └── detectFromURL.js ├── sip ├── README.md ├── protocol.md ├── TODO.md ├── p2p.md └── v3.md ├── validate.js ├── package.json ├── index-browser.js ├── rpc.js ├── README-old.md ├── test ├── v3.js ├── addon-protocol.js └── basic.js ├── addon-template.ejs ├── client.js ├── server.js └── browser ├── stremio-addons.min.js └── stremio-addons.js /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6" 4 | - "6.1" 5 | - "4.4" 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | generator-stremio/output 3 | generator-stremio/app/output 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OBSOLETE 2 | 3 | ## Move over to [stremio-addon-sdk](https://github.com/stremio/stremio-addon-sdk) 4 | 5 | For the old README, see [README-old.md](/README-old.md) 6 | -------------------------------------------------------------------------------- /docs/tutorial/scaffolding.md: -------------------------------------------------------------------------------- 1 | ### Scaffolding 2 | 3 | You can start development of your Stremio addon by executing: 4 | 5 | ``` 6 | npm -g install yo generator-stremio-addon # use sudo if you're on Linux 7 | yo stremio-addon 8 | ``` 9 | 10 | You will find generated Stremio addon source code in `output/` directory. 11 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.CENTRAL = "https://api9.strem.io"; 2 | 3 | module.exports.Client = require("./client"); 4 | module.exports.Client.RPC = require("./rpc"); // require rpc in index, so we can only require('stremio-addons/client') w/o an issue of requiring node-specific modules (http/https) in the browser 5 | module.exports.Server = require("./server"); 6 | -------------------------------------------------------------------------------- /lib/transports/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // @TODO: should this be require('stremio-addons-transport-ipfs') etc etc 3 | // For example, we might want to replace 'ipfs' with a fallback that is a wrapper of the http transport using gateway.ipfs.io 4 | ipfs: require('./ipfs-shim'), 5 | http: require('./http'), 6 | legacy: require('./legacy') 7 | } -------------------------------------------------------------------------------- /docs/api/meta/README.md: -------------------------------------------------------------------------------- 1 | The `meta` methods handle getting media feeds, searching and fetching one specific element from previous responses. 2 | 3 | There are 4 types of `meta` requests: 4 | 5 | - [Getting a feed of meta elements](meta.find.md) (`meta.find`) 6 | - [Searching for meta elements](meta.search.md) (`meta.search`) 7 | - [Requesting one meta element](meta.get.md) (`meta.get`) 8 | - [Getting all genres for a specific meta type](meta.genres.md) (`meta.genres`) 9 | -------------------------------------------------------------------------------- /sip/README.md: -------------------------------------------------------------------------------- 1 | # Stremio Improvement Proposals 2 | 3 | ### Improvement proposals for the Stremio Add-on system and the Stremio ecosystem in general 4 | 5 | 6 | ``protocol.md`` - a basic spec of the v3 addon protocol 7 | 8 | ``p2p.md`` - a working proto-spec of the peer to peer add-on system 9 | 10 | ``v3.md`` - notes taking from developing the v3 addon protocol; temporary document, but might contain design decisions, researches and etc. 11 | 12 | 13 | ``TODO.md`` - TODO list 14 | -------------------------------------------------------------------------------- /lib/util/promisify.js: -------------------------------------------------------------------------------- 1 | module.exports = function promisify(fn) 2 | { 3 | return function() 4 | { 5 | let args = Array.prototype.slice.call(arguments) 6 | const lastArg = args[args.length-1] 7 | 8 | // If a callback is passed, just call the original function 9 | // Else, construct a promise and return that 10 | 11 | if (typeof(lastArg) === 'function') 12 | return fn.apply(null, args) 13 | else 14 | return new Promise(function(resolve, reject) { 15 | args.push(function(err, res) { 16 | if (err) reject(err) 17 | else resolve(res) 18 | }) 19 | fn.apply(null, args) 20 | }) 21 | } 22 | } -------------------------------------------------------------------------------- /docs/api/meta/content.types.md: -------------------------------------------------------------------------------- 1 | ## Content types 2 | 3 | **Stremio supports the following content types as of Apr 2016:** 4 | 5 | * ``movie`` - movie type - has metadata like name, genre, description, director, actors, images, etc. 6 | * ``series`` - series type - has all the metadata a movie has, plus an array of episodes 7 | * ``channel`` - chnanel type - created to cover YouTube channels; has name, description and an array of uploaded videos 8 | * ``tv`` - tv type - has name, description, genre; streams for ``tv`` should be live (without duration) 9 | 10 | **If you think Stremio should add another streaming source, feel free to open an issue at this repo.** 11 | -------------------------------------------------------------------------------- /lib/transports/http.js: -------------------------------------------------------------------------------- 1 | const fetch = require('node-fetch') 2 | const errors = require('../errors') 3 | 4 | module.exports = function httpTransport(url) 5 | { 6 | // url should point to manifest.json 7 | 8 | this.manifest = function(cb) 9 | { 10 | req(url, cb) 11 | } 12 | 13 | this.get = function(args, cb) 14 | { 15 | const reqUrl = url.replace('/manifest.json', '/'+args.join('/')+'.json') 16 | req(reqUrl, cb) 17 | } 18 | 19 | function req(url, cb) 20 | { 21 | fetch(url) 22 | .then(function(resp) { 23 | if (resp.status === 404) 24 | return cb(errors.ERR_NOT_FOUND) 25 | 26 | return resp.json() 27 | .then(function(resp) { cb(null, resp) }) 28 | }) 29 | .catch(cb) 30 | } 31 | 32 | return this 33 | } -------------------------------------------------------------------------------- /docs/tutorial/using-in-browser.md: -------------------------------------------------------------------------------- 1 | ## Using in browser 2 | 3 | To use the client library in a browser you can just include the minified or non-minified script from the [`browser/`](https://github.com/Stremio/stremio-addons/tree/master/browser) directory of this project. 4 | 5 | To use with a browserify bundle, you can require the `index-browser.js` file. 6 | 7 | For example: 8 | 9 | ```javascript 10 | var browserify = require('browserify') 11 | 12 | var b = browserify() 13 | 14 | b.require('./index-browser.js', { expose: 'stremio-addons' }) 15 | 16 | b.bundle().pipe(require('fs').createWriteStream('stremio-addons.js')) 17 | ``` 18 | 19 | Then add it to your browser app: 20 | 21 | ``` 22 | 23 | ``` 24 | 25 | And initialize as normal: 26 | 27 | ``` 28 | var Stremio = require('stremio-addons') 29 | var client = new Stremio.Client() 30 | ``` -------------------------------------------------------------------------------- /lib/errors.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // Detection errors (AddonClient.detectFromURL) 3 | ERR_URL: { code: 0, message: 'Invalid URL' }, 4 | ERR_PROTOCOL: { code: 1, message: 'Invalid URL protocol' }, 5 | ERR_UNRECOGNIZED: { code: 2, message: 'Not recognized as an add-on or a repository' }, 6 | 7 | ERR_NO_TRANSPORT: { code: 3, message: 'No valid manifest.transport' }, 8 | ERR_BAD_HTTP: { code: 4, message: 'Invalid HTTP status code' }, 9 | ERR_RESP_UNRECOGNIZED: { code: 5, message: 'Response not recognized as an add-on or a repository' }, 10 | 11 | ERR_JSON_EXPECTED: { code: 6, message: 'Response is not JSON' }, 12 | 13 | ERR_NOT_FOUND: { code: 7, message: 'Not found' }, 14 | ERR_UNSUPPORTED_RESOURCE: { code: 8, message: 'Unsupported resource' }, 15 | 16 | ERR_MANIFEST_INVALID: { code: 9, message: 'Invalid manifest' }, 17 | 18 | ERR_MANIFEST_CALL_FIRST: { code: 10, message: '.manifest must be called first' }, 19 | 20 | } -------------------------------------------------------------------------------- /docs/tutorial/hosting.md: -------------------------------------------------------------------------------- 1 | ### Hosting 2 | 3 | Stremio add-ons require hosting in order to be published. You need a NodeJS hosting solution, as stremio add-ons are NodeJS apps. 4 | 5 | We recommend: 6 | 7 | - [Heroku](https://www.heroku.com) - [free with some restrictions](https://www.heroku.com/pricing) 8 | - [cloudno.de](https://cloudno.de) - [free for up to 150k requests/month](https://cloudno.de/pricing) 9 | - [Evennode](https://www.evennode.com) - [free for 7 days trial](https://www.evennode.com/pricing) 10 | 11 | You can also check this very comprehensive [guide by nodejs](https://github.com/nodejs/node-v0.x-archive/wiki/node-hosting). 12 | 13 | Stremio add-ons are deployed just like regular nodejs apps, so follow the nodejs instructions provided by your particular service provider. 14 | 15 | If you've built a great add-on, and need help with hosting your add-on, you are welcome to contact us at [addons@strem.io](addons@strem.io) 16 | -------------------------------------------------------------------------------- /docs/api/subtitles/subtitles.object.md: -------------------------------------------------------------------------------- 1 | ### Subtitle Object 2 | 3 | ``id`` - **required** - identifier of the subtitles object - could be any string - serves to identify the set of subtitles for a specific video; example of this is the OpenSubtitles MovieHash - if taking subtitles from there, the MovieHash can be used as an ``id`` 4 | 5 | ``itemHash`` - _optional_ - metadata item hash, which is defined as a combination of the [``Meta Element``](/docs/api/meta/meta.element.md)'s ``id`` followed by ``season`` / ``episode`` or ``video_id``, separated by a white space; example of this is ``tt0898266 9 17`` 6 | 7 | ``all`` - **required** - all of the subtitle variants for this ``id`` - array of 8 | 9 | ```javascript 10 | { 11 | id: "string identifier", 12 | url: "url to srt file", 13 | lang: "language code in ISO 639-1" 14 | } 15 | ``` 16 | 17 | 18 | ``exclusive`` - _optional_ - set to `true` if you don't want Stremio to try to find more subtitles by `subtitles.find`. Applicable when returning a Subtitle Object with your `stream.find`. 19 | -------------------------------------------------------------------------------- /docs/api/meta/meta.genres.md: -------------------------------------------------------------------------------- 1 | ## Meta Genres 2 | 3 | This is used when loading the Discover page, in order to show all genres in the left sidebar. 4 | 5 | **NOTE:** If you do not implement this call (not recommended), the genres will be gathered from all items you return in the first `meta.find` call. 6 | 7 | ```javascript 8 | var addon = new Stremio.Server({ 9 | "meta.genres": function(args, callback, user) { 10 | // callback expects an object with genres array 11 | } 12 | }); 13 | ``` 14 | 15 | ### Request 16 | 17 | _otherwise known as `args` in the above code_ 18 | 19 | ```javascript 20 | { 21 | query: { 22 | type: "movies" // the type for which to return genres 23 | }, 24 | sort: { 25 | 'popularities.basic': -1 // -1 for descending, 1 for ascending 26 | }, 27 | } 28 | ``` 29 | 30 | ### Response 31 | 32 | ```javascript 33 | { 34 | genres: [ 35 | // simple array of strings for the genres 36 | "Action", 37 | "Lifestyle", 38 | ] 39 | } 40 | ``` 41 | 42 | See [Content Types](content.types.md) for the `type` parameter. 43 | -------------------------------------------------------------------------------- /docs/api/meta/meta.request.md: -------------------------------------------------------------------------------- 1 | ### Meta Request 2 | 3 | ``query`` - MongoDB-like query object, where all objects must be matched against; should support ``$in``, ``$exists``, ``$gt``, ``$lt`` operators; on ``meta.search`` method, this is a string 4 | 5 | ``sort`` - an single-property object of the format ``{ property: -1 }``; the value can be ``-1`` for descending sort and ``1`` for ascending, and the property should be a property you already return in your [Meta Element](meta.element.md); **NOTE:** this only applies for ``meta.find`` 6 | 7 | ``projection`` - MongoDB-like projection object, also accepts string values - ``lean``, ``medium`` and ``full``; lean contains name, year, release date, cast, director; medium also includes episodes (if applicable) and the full projection also includes all images and full cast info 8 | 9 | ``complete`` - only return items with complete (+images) metadata 10 | 11 | ``limit`` - limit to N results 12 | 13 | ``skip`` - skip first N results 14 | 15 | _**TIP**: If you don't use MongoDB, you can use [sift](https://www.npmjs.com/package/sift) or [linvodb3](https://www.npmjs.com/package/linvodb3) to support to the query format._ 16 | -------------------------------------------------------------------------------- /docs/tutorial/publishing.md: -------------------------------------------------------------------------------- 1 | ### Publishing 2 | 3 | All you need to do to publish your add-on is host it publically, and have the **`endpoint`** field in your manifest to point to the HTTP URL where the add-on is accessible (e.g. `http://twitch.strem.io`). 4 | 5 | Hosting the add-on is very easy, since Stremio add-ons are simply Node.js applications. You can see - [hosting your Add-on](/docs/tutorial/hosting.md) tutorial. 6 | 7 | When you have a publically hosted add-on, and the `endpoint` field is properly set, the [`stremio-addons`](https://github.com/Stremio/stremio-addons) module will send the information about the add-on to our API, and it will automatically evaluate if your add-on is accessible and if so, it will be shown in our [Add-on catalogue](https://addons.strem.io). We do not moderate the listing on (https://addons.strem.io), the add-on should show automatically once `endpoint` is pointed to a valid endpoint. 8 | 9 | Please note that in order for the add-on to work on Android and iOS, it needs to support the `https` protocol. It's not necessary that you will use `https`. However, the mobile application will always try to access it through `https`. 10 | -------------------------------------------------------------------------------- /validate.js: -------------------------------------------------------------------------------- 1 | // simply console-log warnings in case of wrong args; aimed to aid development 2 | 3 | module.exports = function(method, res) { 4 | if (method === "meta.find" || method === "stream.find") { 5 | if (! Array.isArray(res)) warning('result from '+method+' is not an array, but should be, but is ',res) 6 | else res.forEach(function(o) { 7 | if (method === "meta.find") validateMetadata(o) 8 | if (method === "stream.find") validateStream(o) 9 | }) 10 | } 11 | 12 | if (method === "meta.get") { 13 | if (res) validateMetadata(res) 14 | } 15 | } 16 | 17 | function warning(msg) { 18 | console.log.apply(console, ['stremio-addons warning:'].concat(Array.prototype.slice.call(arguments))) 19 | } 20 | 21 | function validateMetadata(o) { 22 | if (! o) warning('empty metadata object') 23 | else if (! o.id) warning('metadata object does not contain id') 24 | else if (! o.type) warning('metadata object with id:'+o.id+' does not contain type') 25 | } 26 | 27 | function validateStream(o) { 28 | if (! o) warning('empty stream object') 29 | else if (! (o.url || o.yt_id || o.infoHash)) warning('stream object does not contain any stremable property') 30 | } -------------------------------------------------------------------------------- /docs/api/subtitles/subtitles.find.md: -------------------------------------------------------------------------------- 1 | ### Subtitles Find 2 | 3 | ``query`` - **required** - Object, query to retrieve the subtitles 4 | 5 | ``query.itemHash`` - **required** - identifies the current item based on metadata 6 | 7 | For movies, this is only the IMDB ID, e.g. ``"tt0063350"``. 8 | 9 | For series, this is the IMDB, and season/episode numbers, split with interval - e.g. ``"tt1748166 1 1"`` for season 1, episode 1 of [_Pioneer One_](https://en.wikipedia.org/wiki/Pioneer_One) 10 | 11 | For channels, this is the YouTube ID of the channel and the YouTube ID of the video, split with an interval. For example, ``"UC3gsgELlsob7AFi-mHOqNkg 9bZkp7q19f0"``. 12 | 13 | ``query.videoHash`` - _optional_ - String -highly recommended to use - this is the hash of the video, generated with the [_OpenSubtitles algorithm_](https://trac.opensubtitles.org/projects/opensubtitles/wiki/HashSourceCodes) 14 | 15 | ``query.videoSize`` - _optional_ - Number - byte size of the video 16 | 17 | ``query.videoName`` - _optional_ - filename of the original video 18 | 19 | ``supportsZip`` - _optional_ - boolean, true if your client supports ``.zip`` files for subtitles; in this case, the client should use the first ``.srt`` file inside the provided ``.zip`` file 20 | -------------------------------------------------------------------------------- /docs/BENEFITS.md: -------------------------------------------------------------------------------- 1 | # Benefits of creating an add-on for Stremio 2 | 3 | The largest benefit of creating an add-on for Stremio is distributing your content to one of the largest and easiest to use media centers. 4 | 5 | We believe that having all the content in one place is the key to good user experience. 6 | We also believe that when it comes to entertainment video, great user experience is the key to building an audience. 7 | If we make our users use/log-in to a number of video on demand services, we are never going to reach huge retention rates. 8 | This is why we created Stremio and the add-ons system - to introduce a way to put entertainment video in one place, in the best possible way. 9 | 10 | This philosophy means that Stremio is always rich with content and very attractive to end users. Having your content in Stremio means exposing it to 1.5 million users (as of middle of 2016) and counting. 11 | 12 | Furthermore, you can monetize your content through the ability to insert arbitrary HTML content in the sidebar and over the player (see [/docs/api/stream/stream.response.md#stream-response](Stream widget docs)). 13 | 14 | If you are interested in us helping you monetize your content, you can consult with us at [addons@strem.io](addons@strem.io) -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stremio-addons", 3 | "version": "2.8.14", 4 | "description": "Stremio Add-on Server / Client", 5 | "main": "index.js", 6 | "dependencies": { 7 | "async": "1.x.x", 8 | "ejs": "^2.5.7", 9 | "events": "^1.1.0", 10 | "extend": "^3.0.0", 11 | "ipfs": "^0.27.7", 12 | "ipfs-pubsub-room": "^1.1.5", 13 | "ipfs-repo": "^0.18.7", 14 | "js-ipfs": "0.0.301", 15 | "libp2p": "^0.19.0", 16 | "once": "^1.4.0", 17 | "thunky": "^1.0.2" 18 | }, 19 | "devDependencies": { 20 | "tape": "4.x.x", 21 | "browserify": "*", 22 | "uglifyjs": "*" 23 | }, 24 | "scripts": { 25 | "preversion": "browserify -r ./index-browser.js:stremio-addons > browser/stremio-addons.js ; uglifyjs browser/stremio-addons.js > browser/stremio-addons.min.js ; git commit --allow-empty browser/stremio-addons.js browser/stremio-addons.min.js -m 'stremio-addons.js update'", 26 | "test": "node test/basic.js" 27 | }, 28 | "repository": { 29 | "type": "git", 30 | "url": "http://github.com/Stremio/stremio-addons" 31 | }, 32 | "keywords": [ 33 | "stremio" 34 | ], 35 | "author": "Ivo Georgiev", 36 | "license": "MIT", 37 | "bugs": { 38 | "url": "https://github.com/Stremio/stremio-addons/issues" 39 | }, 40 | "homepage": "https://github.com/Stremio/stremio-addons" 41 | } 42 | -------------------------------------------------------------------------------- /docs/tutorial/testing.md: -------------------------------------------------------------------------------- 1 | ### Testing Environment 2 | 3 | Add-ons can be tested in two ways. 4 | 5 | **Testing with [stremio-addons-client](https://github.com/Ivshti/stremio-addons-client)** 6 | 7 | - Install & Run [stremio-addons-client](https://github.com/Ivshti/stremio-addons-client) 8 | - Run your add-on with `npm start` 9 | - Go to the `stremio-addons-client` url 10 | - Open the "Add-ons" tab 11 | - Add your add-on url (ie: `http://localhost:9005`, remember to use your add-on's port) to the "Add new add-on" form 12 | 13 | **Testing with [Stremio](http://www.strem.io/) versions 3.6 or earlier** 14 | 15 | - On Windows, run Stremio with: 16 | 17 | ``` 18 | stremio . --services=http://localhost:9005/stremio/v1/stremioget 19 | ``` 20 | 21 | - On OSX / Linux, open a terminal in your add-on's directory, and do: 22 | 23 | ``` 24 | open stremio://localhost:7000/stremio/v1 & # Load this add-on in Stremio 25 | PORT=7000 node index # Start the add-on server 26 | ``` 27 | 28 | **TIP:** in Stremio 3.6, results from local add-ons are still cached. To work around this while development, you can prepend anything (e.g. number) before `/stremio/v1`. For example, `http://localhost:9005/cache-break-2/stremio/v1/stremioget`. 29 | 30 | 31 | **Testing with [Stremio](http://www.strem.io/) versions 4.0 or later** 32 | 33 | Open your browser at `http://127.0.0.1:11470/#?addon=ADDON_URL`, where `ADDON_URL` is the url to your add-on. 34 | -------------------------------------------------------------------------------- /docs/api/repositories.md: -------------------------------------------------------------------------------- 1 | ### Repositories 2 | 3 | Repositories are JSON files accessed over HTTP/HTTPS that contain information about add-ons. 4 | 5 | Repositories are added to the Stremio catalogue, after which it starts displaying all the add-ons in this repository so the user can install them. 6 | 7 | This is an example of the official repository: [http://api9.strem.io/addonsrepo.json](http://api9.strem.io/addonsrepo.json) 8 | 9 | This is the basic format for a repository: 10 | 11 | `name` - **required** - the repository name 12 | 13 | `addons` - _optional_ - array of [``add-on meta objects``](/docs/api/repositories.md#add-on-meta-object) 14 | 15 | `endpoints` - _optional_ - array of add-on endpoints; use this if you don't know the add-on meta 16 | 17 | #### Add-on meta object 18 | 19 | `id` - **required** - add-on identifier 20 | 21 | `endpoints` - **required** - array of all endpoints (URLs) that this add-on can be accessed on 22 | 23 | `name` - **required** - add-on name 24 | 25 | `logo` - _optional_ - URL to add-on logo 26 | 27 | Example: 28 | 29 | ```json 30 | { 31 | "name": "My repo name", 32 | "addons": [{ 33 | "id": "com.linvo.cinemeta", 34 | "endpoints": [ 35 | "https://cinemeta.strem.io/stremioget/stremio/v1" 36 | ], 37 | "name": "Cinemeta", 38 | }], 39 | "endpoints": [ 40 | "https://channels.strem.io/stremioget/stremio/v1" 41 | ] 42 | } 43 | ``` 44 | 45 | **NOTE** - You can pass either `addons`, or `endpoints`, or both. Passing an add-on with meta using `addons` is preferred, but if you only know the endpoint, it's OK to pass it under `endpoints` 46 | -------------------------------------------------------------------------------- /lib/client.js: -------------------------------------------------------------------------------- 1 | const transports = require('./transports') 2 | const detectFromURL = require('./detectFromURL') 3 | 4 | // The node.js promisify does not support still using callbacks (detection whether a cb is passed) 5 | //const promisify = require('util').promisify 6 | const promisify = require('./util/promisify') 7 | 8 | module.exports.AddonClient = function AddonClient(manifest, transport) 9 | { 10 | this.manifest = manifest 11 | this.get = promisify(function() 12 | { 13 | let args = Array.prototype.slice.call(arguments) 14 | let cb = args.pop() 15 | if (typeof(cb) !== 'function') throw 'cb is not a function' 16 | if (args.length < 2) throw 'args min length is 1' 17 | transport.get(args, cb) 18 | }) 19 | 20 | this.destroy = promisify(function(cb) 21 | { 22 | if (transport.destroy) transport.destroy(cb) 23 | }) 24 | 25 | return this 26 | } 27 | 28 | module.exports.detectFromURL = promisify(detectFromURL) 29 | 30 | // This function is different in that it will return immediately, 31 | // but might update the manifest once it loads (hence the cb) 32 | module.exports.constructFromManifest = promisify(function(manifest, cb) 33 | { 34 | const Transport = transports[manifest.transport] 35 | transport = new Transport(manifest.url) 36 | 37 | let addon = new AddonClient(manifest, transport) 38 | 39 | transport.manifest(function(err, newManifest) { 40 | if (err) 41 | return cb(err) 42 | 43 | // Keep these values from the original 44 | newManifest.transport = manifest.transport 45 | newManifest.url = manifest.url 46 | 47 | addon.manifest = newManifest 48 | cb(null, addon) 49 | }) 50 | 51 | return addon 52 | }) 53 | 54 | -------------------------------------------------------------------------------- /docs/tutorial/add.to.app.md: -------------------------------------------------------------------------------- 1 | ### Add to Your App 2 | 3 | This package includes both Client and Server. The Client can be used to implement add-on support for any app. 4 | 5 | ```javascript 6 | var addons = require("stremio-addons"); 7 | var stremio = new addons.Client({ /* options; picker: function(addons) { return addons } */ }); 8 | // specify a picker function to filter / sort the addons we'll use 9 | // timeout: specify a request timeout 10 | // respTimeout: specify response timeout 11 | // disableHttps: use HTTP instead of HTTPS 12 | 13 | stremio.add(URLtoAddon, { priority: 0 }); // Priority is an integer, the larger it is, the higher the priority 14 | // OR 15 | stremio.add(URLtoAddon); 16 | // Priority determines which Add-on to pick first for an action, if several addons provide the same thing (e.g. streaming movies) 17 | 18 | stremio.meta.get(args,cb); /* OR */ stremio.call("meta.get", args, cb); 19 | 20 | // Events / hooks 21 | stremio.on("pick", function(params) { 22 | // called when picking addons 23 | // params.addons - all addons; you can modify this. e.g. params.addons = params.addons.filter(...) 24 | // params.method - the method we're picking for 25 | 26 | // this can be used instead of picker 27 | }); 28 | 29 | stremio.on("addon-ready", function(addon, url) { 30 | // addon is an internal object - single Addon 31 | // url is the URL to it 32 | }); 33 | ``` 34 | 35 | 36 | #### Usage in browser 37 | ```sh 38 | browserify -r ./node_modules/stremio-addons/index.js:stremio-addons > stremio-addons.js 39 | ``` 40 | Or use the pre-built ``browser/stremio-addons.js`` with ``window.require("stremio-addons")`` 41 | ```html 42 | 43 | 47 | ``` 48 | -------------------------------------------------------------------------------- /docs/tutorial/using-cinemeta.md: -------------------------------------------------------------------------------- 1 | ## Using Cinemeta 2 | 3 | **Cinemeta** is the first offical Stremio Add-on - it provides metadata for most movies / TV series in IMDB. 4 | 5 | It can be seen as an alternative of APIs like OMDb, TheMovieDB and TheTVDB. 6 | 7 | It also provides an indexing mechanism (through ``index.get``) method that automatically identifies an IMDB ID from a filename. 8 | 9 | Retrieving metadata and associating video files with IMDB ID are useful use cases both for Stremio itself, and for building new Add-ons. 10 | 11 | ### Initializing a client 12 | 13 | ```javascript 14 | var CINEMETA_ENDPOINT = "http://cinemeta.strem.io/stremioget/stremio/v1"; 15 | 16 | var Stremio = require("stremio-addons"); 17 | var addons = new Stremio.Client(); 18 | addons.add(CINEMETA_ENDPOINT); 19 | ``` 20 | 21 | ### Using ``meta.*`` methods 22 | 23 | ```javascript 24 | // get all the detailed data on a movie/series 25 | addons.meta.get({ query: { imdb_id: "tt0032138" } }, function(err, meta) { 26 | console.log(meta); 27 | }); 28 | 29 | // get top 50 series 30 | addons.meta.find({ query: { type: "series" }, limit: 50 }, function(err, res) { 31 | console.log(res); // array of 50 series 32 | }); 33 | 34 | // TIP: in order to get other content types, you can initialize add-ons for them 35 | // addons.add("http://channels.strem.io/stremioget/stremio/v1"); 36 | // addons.add("http://filmon.strem.io/stremioget/stremio/v1"); 37 | 38 | ``` 39 | 40 | For documentation on the ``meta.get`` or ``meta.find`` interface, see [Meta Request](../meta/meta.request.md) and [Meta Response](../meta/meta.element.md). 41 | 42 | ### Using ``index.get`` 43 | 44 | ```javascript 45 | 46 | addons.index.get({ files: [ { path: "The.Wizard.of.Oz.1939.1080p.BrRip.x264.BOKUTOX.YIFY.mp4" } ] }, function(err, res) { 47 | console.log(res); 48 | console.log(res.files[0].imdb_id); // outputs tt0032138 49 | }); 50 | ``` 51 | -------------------------------------------------------------------------------- /docs/api/meta/meta.get.md: -------------------------------------------------------------------------------- 1 | ## Meta Get 2 | 3 | This is used when opening an item's detail page (e.g. double click from Discover). The result has to contain properties like ``episodes``, ``uploads`` and ``cast``, so this is especially useful to show ``series`` and ``channel`` types. 4 | 5 | ```javascript 6 | var addon = new Stremio.Server({ 7 | "meta.get": function(args, callback, user) { 8 | // expects one meta element (requested by ID) 9 | } 10 | }); 11 | ``` 12 | 13 | ### Request 14 | 15 | _otherwise known as `args` in the above code_ 16 | 17 | ```javascript 18 | { 19 | query: { 20 | basic_id: 'opa2135' // based on what you set as "id" in the previous responses 21 | } 22 | } 23 | ``` 24 | 25 | See [Meta Request](meta.request.md) for Parameters. 26 | 27 | ### Response 28 | 29 | ```javascript 30 | { 31 | id: 'basic_id:opa2135', // unique ID for the media, will be returned as "basic_id" in the request object later 32 | name: 'basic title', // title of media 33 | poster: 'http://thetvdb.com/banners/posters/78804-52.jpg', // image link 34 | posterShape: 'regular', // can also be 'landscape' or 'square' 35 | banner: 'http://thetvdb.com/banners/graphical/78804-g44.jpg', // image link 36 | genre: ['Entertainment'], 37 | isFree: 1, // some aren't 38 | popularity: 3831, // the larger, the more popular this item is 39 | popularities: { basic: 3831 }, // same as 'popularity'; use this if you want to provide different sort orders in your manifest 40 | type: 'movie' // can also be "tv", "series", "channel" 41 | } 42 | ``` 43 | 44 | See [Meta Element](meta.element.md) for Parameters. 45 | 46 | See [Content Types](content.types.md) for the `type` parameter. 47 | -------------------------------------------------------------------------------- /docs/api/subtitles/README.md: -------------------------------------------------------------------------------- 1 | ### Subtitles 2 | 3 | **Subtitles are provided in a [Subtitle Object](subtitle.object.md) which represents all possible subtitle tracks (in any number of languages) for a video stream.** 4 | 5 | There are two ways to provide a [Subtitle Object](subtitle.object.md): 6 | 7 | 1. In the `subtitles` property of a [Stream](/docs/api/stream/stream.response.md). Recommended if you have an add-on that would provide both video streams and subtitles. 8 | 2. As a response from the `subtitles.find` method. Recommended if you are making a standalone add-on for subtitles. 9 | 10 | 11 | #### `subtitles.find` Example 12 | 13 | ```javascript 14 | var addon = new Stremio.Server({ 15 | "subtitles.find": function(args, callback, user) { 16 | // expects an array of subtitle objects 17 | } 18 | }); 19 | ``` 20 | 21 | #### `subtitles.find` Request 22 | 23 | ```javascript 24 | { 25 | query: { 26 | itemHash: "tt23155 4 5", // "4" = season, "5" = episode, usage of this param can vary, see docs 27 | videoHash: "8e245d9679d31e12", // open subtitles hash of file 28 | videoSize: 652174912, // optional, file size in bytes 29 | videoName: "The.Wizard.of.Oz.1939.mp4" // optional, filename 30 | }, 31 | supportsZip: true // can be "false" in some rare cases 32 | } 33 | ``` 34 | 35 | See [Subtitle Request](subtitles.find.md) for Parameters. 36 | 37 | #### Response 38 | 39 | See [Subtitle Object](subtitle.object.md) for response. 40 | 41 | ```javascript 42 | // Subtitle Object 43 | { 44 | id: "8e245d9679d31e12", // mandatory, any unique string to identify this response 45 | // can be the OpenSubtitles Hash of the file 46 | itemHash: "tt23155 4 5", // optional, same as the one from the request 47 | all: [ 48 | { 49 | id: "string identifier", 50 | url: "url to srt file", 51 | lang: "language code in ISO 639-1" 52 | }, 53 | ... 54 | ] 55 | } 56 | ``` 57 | -------------------------------------------------------------------------------- /index-browser.js: -------------------------------------------------------------------------------- 1 | module.CENTRAL = "http://api9.strem.io"; 2 | module.exports.Client = require("./client"); 3 | /* 4 | // Fetch-based client 5 | module.exports.Client.RPC = function(endpoint) { 6 | var self = { }; 7 | self.request = function(method, params, callback) { 8 | var body = JSON.stringify({ params: params, method: method, id: 1, jsonrpc: "2.0" }); 9 | 10 | var request = ((body.length < 8192) && endpoint.match("/stremioget")) ? 11 | window.fetch(endpoint+"/q.json?b="+btoa(body)) // GET 12 | : window.fetch(endpoint, { method: "POST", headers: { "Content-Type": "application/json" }, body: body }); // POST 13 | 14 | request.then(function(resp) { 15 | if (resp.status !== 200) return callback(new Error("response code "+resp.status)); 16 | if (resp.headers.get("content-type").indexOf("application/json") === -1) return callback(new Error("no application/json response")); 17 | 18 | resp.json().then(function(body) { 19 | setTimeout(function() { 20 | if (!body || body.error) return callback(null, (body && body.error) || new Error("empty body")); 21 | callback(null, null, body.result); 22 | }); 23 | }, callback).catch(callback); 24 | }).catch(callback); 25 | }; 26 | return self; 27 | }; 28 | */ 29 | 30 | // XMLHttpRequest-based client 31 | module.exports.Client.RPC = function (endpoint) { 32 | var self = { }; 33 | 34 | self.request = function(method, params, callback) { 35 | var body = JSON.stringify({ params: params, method: method, id: 1, jsonrpc: "2.0" }); 36 | 37 | var request = new XMLHttpRequest(); 38 | 39 | request.onreadystatechange = function() { 40 | if (request.readyState == XMLHttpRequest.DONE) { 41 | if (request.status == 200) { 42 | var res; 43 | try { 44 | res = JSON.parse(request.responseText); 45 | } catch(e) { callback(e) } 46 | 47 | callback(null, res.error, res.result); 48 | } else callback("network err "+request.status); 49 | } 50 | } 51 | 52 | request.open("GET", endpoint+"/q.json?b="+btoa(body), true); 53 | request.send(); 54 | }; 55 | return self; 56 | } 57 | -------------------------------------------------------------------------------- /docs/api/meta/meta.search.md: -------------------------------------------------------------------------------- 1 | ## Meta Search 2 | 3 | Perform a text search. Arguments are exactly the same as usual [``Meta Request``](meta.request.md), except ``query`` is a string. Returns an array of [``Meta Elements``](meta.element.md) matches. 4 | 5 | This is used for the Search functionality. 6 | 7 | Does not support pagination. 8 | 9 | ```javascript 10 | var addon = new Stremio.Server({ 11 | "meta.search": function(args, callback, user) { 12 | // expects one meta element (requested by ID) 13 | } 14 | }); 15 | ``` 16 | 17 | ### Request 18 | 19 | _otherwise known as `args` in the above code_ 20 | 21 | ```javascript 22 | { 23 | query: 'baseball season', // search query 24 | limit: 10 // limit length of the response array to "10" 25 | } 26 | ``` 27 | 28 | See [Meta Request](meta.request.md) for Parameters. 29 | 30 | ### Response 31 | 32 | ```javascript 33 | { 34 | query: 'baseball season', // return the query from the response 35 | results: [ // Array of Metadata objects 36 | { 37 | id: 'basic_id:opa2135', // unique ID for the media, will be returned as "basic_id" in the request object later 38 | name: 'basic title', // title of media 39 | poster: 'http://thetvdb.com/banners/posters/78804-52.jpg', // image link 40 | posterShape: 'regular', // can also be 'landscape' or 'square' 41 | banner: 'http://thetvdb.com/banners/graphical/78804-g44.jpg', // image link 42 | genre: ['Entertainment'], 43 | isFree: 1, // some aren't 44 | popularity: 3831, // the larger, the more popular this item is 45 | popularities: { basic: 3831 }, // same as 'popularity'; use this if you want to provide different sort orders in your manifest 46 | type: 'movie' // can also be "tv", "series", "channel" 47 | }, 48 | ... 49 | ], 50 | } 51 | ``` 52 | 53 | See [Meta Element](meta.element.md) for Parameters. 54 | 55 | See [Content Types](content.types.md) for the `type` parameter. 56 | -------------------------------------------------------------------------------- /lib/detectFromURL.js: -------------------------------------------------------------------------------- 1 | const URL = require('url') 2 | const fetch = require('node-fetch') 3 | const errors = require('./errors') 4 | const client = require('./client') 5 | const transports = require('./transports') 6 | 7 | const SUPPORTED_PROTOCOLS = [ 8 | 'ipfs:', 'ipns:', 9 | ':', 'http:', 'https:' // those all represent http 10 | ] 11 | 12 | module.exports = function detectFromURL(url, cb) 13 | { 14 | // Detects what a URL is 15 | // possible outcomes: repository or addon (addons have 3 different transports) 16 | 17 | const parsed = URL.parse(url) 18 | 19 | if (SUPPORTED_PROTOCOLS.indexOf(parsed.protocol) === -1) 20 | return cb(errors.ERR_PROTOCOL) 21 | 22 | if (parsed.protocol === 'ipfs:' || parsed.protocol === 'ipns:') { 23 | constructFromTransport(new transports.ipfs(url), cb) 24 | return 25 | } 26 | 27 | const isManifest = parsed.pathname.match(/manifest\.json$/) 28 | const isJSON = parsed.pathname.match(/\.json$/) 29 | 30 | fetch(url) 31 | .then(function(resp) { 32 | if (resp.status !== 200) 33 | return cb(errors.ERR_BAD_HTTP) 34 | 35 | const contentType = resp.headers.get('content-type') 36 | const isHeaderJSON = contentType && contentType.indexOf('application/json') !== -1 37 | 38 | const urlToManifest = resp.headers.get('x-stremio-addon') 39 | 40 | if (urlToManifest) { 41 | // Detected as an HTTP add-on 42 | constructFromTransport(new transports.http(urlToManifest), cb) 43 | return 44 | } else if (! (isHeaderJSON || isManifest || isJSON)) { 45 | // Detected as a legacy add-on 46 | constructFromTransport(new transports.legacy(url), cb) 47 | return 48 | } 49 | 50 | return resp.json().then(function(resp) { 51 | // Detected as a repository 52 | if (typeof(resp.name) === 'string' && Array.isArray(resp.addons)) { 53 | cb(null, { repository: resp }) 54 | return 55 | } 56 | 57 | // Detected as an HTTP add-on 58 | if (isManifest && resp.id) { 59 | cb(null, { addon: new client.AddonClient(resp, new transports.http(url)) }) 60 | return 61 | } 62 | 63 | return cb(errors.ERR_RESP_UNRECOGNIZED) 64 | }) 65 | }) 66 | .catch(cb) 67 | } 68 | 69 | function constructFromTransport(transport, cb) 70 | { 71 | transport.manifest(function(err, manifest) { 72 | if (err) cb(err) 73 | else cb(null, { addon: new client.AddonClient(manifest, transport) }) 74 | }) 75 | } -------------------------------------------------------------------------------- /docs/api/meta/meta.find.md: -------------------------------------------------------------------------------- 1 | ## Meta Find 2 | 3 | The meta feed is being handled by the `meta.find` method. This is used for loading full catalogue in Discover (Stremio). 4 | 5 | ```javascript 6 | var addon = new Stremio.Server({ 7 | "meta.find": function(args, callback, user) { 8 | // expects array of meta elements (primary meta feed) 9 | // it passes "limit" and "skip" for pagination 10 | } 11 | }); 12 | ``` 13 | 14 | ### Request 15 | 16 | _otherwise known as `args` in the above code_ 17 | 18 | ```javascript 19 | { 20 | query: { 21 | type: 'movie', // can also be "tv", "series", "channel" 22 | 'popularities.basic': { '$gt': 0 } 23 | }, 24 | popular: true, 25 | complete: true, 26 | sort: { 27 | 'popularities.basic': -1 // -1 for descending, 1 for ascending 28 | }, 29 | limit: 70, // limit length of the response array to "70" 30 | skip: 0 // offset, as pages change it will progress to "70", "140", ... 31 | } 32 | ``` 33 | 34 | When an array of fewer then `70` elements is returned, the feed will be considered finished and pagination will no longer be requested. 35 | 36 | See [Meta Request](meta.request.md) for Parameters. 37 | 38 | See [Content Types](content.types.md) for the `type` parameter. 39 | 40 | ### Response 41 | 42 | ```javascript 43 | [ 44 | { 45 | id: 'basic_id:opa2135', // unique ID for the media, will be returned as "basic_id" in the request object later 46 | name: 'basic title', // title of media 47 | poster: 'http://thetvdb.com/banners/posters/78804-52.jpg', // image link 48 | posterShape: 'regular', // can also be 'landscape' or 'square' 49 | banner: 'http://thetvdb.com/banners/graphical/78804-g44.jpg', // image link 50 | genre: ['Entertainment'], 51 | isFree: 1, // some aren't 52 | popularity: 3831, // the larger, the more popular this item is 53 | popularities: { basic: 3831 }, // same as 'popularity'; use this if you want to provide different sort orders in your manifest 54 | type: 'movie' // can also be "tv", "series", "channel" 55 | }, 56 | ... 57 | ] 58 | ``` 59 | 60 | See [Meta Element](meta.element.md) for Parameters. 61 | 62 | See [Content Types](content.types.md) for the `type` parameter. 63 | 64 | SEe [Manifest](/docs/api/manifest.md) for instructions on how to define custom sort orders (Discover Tabs) 65 | -------------------------------------------------------------------------------- /rpc.js: -------------------------------------------------------------------------------- 1 | var url = require("url"); 2 | var extend = require("extend"); 3 | 4 | var http = require("http"); 5 | var https = require("https"); 6 | 7 | var once = require("once"); 8 | 9 | var LENGTH_TO_FORCE_POST=8192; 10 | 11 | var receiveJSON = function(resp, callback) { 12 | callback = once(callback); 13 | 14 | if (resp.method == "GET") { 15 | var body = url.parse(resp.url, true).query.b; 16 | try { body = JSON.parse(new Buffer(body, "base64").toString()) } catch(e) { 17 | return callback(e) 18 | }; 19 | return callback(null, body); 20 | } 21 | 22 | var body = []; 23 | resp.on("data", function(b) { body.push(b) }); 24 | resp.on("error", callback); 25 | resp.on("end", function() { 26 | try { body = JSON.parse(Buffer.concat(body).toString()) } catch(e) { 27 | return callback(e) 28 | } 29 | callback(null, body); 30 | }); 31 | }; 32 | 33 | // Utility for JSON-RPC 34 | // Rationales in our own client 35 | // 1) have more control over the process, be able to implement debounced batching 36 | // 2) reduce number of dependencies 37 | function rpcClient(endpoint, options, globalOpts) 38 | { 39 | var isGet = true; 40 | 41 | var client = { }; 42 | client.request = function(method, params, callback) { 43 | callback = once(callback); 44 | 45 | params[0] = null; // OBSOLETE work around authentication (index 0) slot which was used before 46 | 47 | var body = JSON.stringify({ params: params, method: method, id: 1, jsonrpc: "2.0" }); 48 | 49 | if (body.length>=LENGTH_TO_FORCE_POST) isGet = false; 50 | 51 | var reqObj = { }; 52 | if (!isGet) extend(reqObj, url.parse(endpoint), { method: "POST", headers: { "Content-Type": "application/json", "Content-Length": body.length } }); 53 | else extend(reqObj, url.parse(endpoint+"/q.json?b="+new Buffer(body, "binary").toString("base64"))); 54 | 55 | if (globalOpts.disableHttps) reqObj.protocol = "http:"; 56 | 57 | var timedOut = false 58 | 59 | var req = ( reqObj.protocol==="https:" ? https : http).request(reqObj, function(res) { 60 | if (timedOut) return; 61 | 62 | if (options.respTimeout && res.setTimeout) res.setTimeout(options.respTimeout); 63 | 64 | receiveJSON(res, function(err, body) { 65 | if (err) return callback(err); 66 | if (!body) return callback(null, new Error("no body")); 67 | if (body.error) return callback(null, body.error); 68 | callback(null, null, body.result); 69 | }); 70 | }); 71 | 72 | if (options.timeout && req.setTimeout) req.setTimeout(options.timeout); 73 | req.on("error", function(err) { callback(err) }); 74 | req.on("timeout", function() { 75 | timedOut = true; 76 | req.removeAllListeners("data"); 77 | req.emit("close"); 78 | callback(new Error("rpc request timed out")); 79 | }); 80 | if (! isGet) req.write(body); 81 | req.end(); 82 | }; 83 | return client; 84 | }; 85 | 86 | module.exports = rpcClient; 87 | module.exports.receiveJSON = receiveJSON; 88 | -------------------------------------------------------------------------------- /docs/api/stream/README.md: -------------------------------------------------------------------------------- 1 | ### Stream Link 2 | 3 | Stream links are being handled by the `stream.find` method. 4 | 5 | [List of Supported Stream Response Parameters](stream.response.md) 6 | 7 | First thing to keep in mind here is that Stremio supports video streaming through HTTP, BitTorrent and IPFS. 8 | 9 | If you are interested in other protocols, contact us at [office@strem.io](mailto:office@strem.io). 10 | 11 | ```javascript 12 | var addon = new Stremio.Server({ 13 | "stream.find": function(args, callback, user) { 14 | // expects an array of stream links 15 | } 16 | }); 17 | ``` 18 | 19 | #### Request Examples 20 | 21 | ```javascript 22 | var client = new Stremio.Client() 23 | //client.add('url to my add-on') 24 | client.stream.find({ 25 | query: { 26 | basic_id: "opa2135" 27 | } 28 | }, function(err, resp) { 29 | }) 30 | ``` 31 | ```javascript 32 | // Request The Wizard of Oz 33 | client.stream.find({ 34 | query: { 35 | imdb_id: "tt0032138" 36 | } 37 | }, function(err, resp) { 38 | }) 39 | ``` 40 | ```javascript 41 | // Request pilot of Game of Thrones 42 | client.stream.find({ 43 | query: { 44 | imdb_id: "tt0944947", 45 | season: 1, 46 | episode: 1 47 | } 48 | }, function(err, resp) { 49 | }) 50 | ``` 51 | ```javascript 52 | // Request Gangnam Style 53 | client.stream.find({ 54 | query: { 55 | yt_id: "UCrDkAvwZum-UTjHmzDI2iIw", 56 | video_id: "9bZkp7q19f0" 57 | } 58 | }, function(err, resp) { 59 | }) 60 | ``` 61 | 62 | #### Search 63 | 64 | The add-on can also implement the `stream.search` method to perform a full text-search through all available streams 65 | 66 | The argument is simply an object with ``query`` property that is a string. Returns an array of [``Stream Object``](stream.response.md) matches. 67 | 68 | #### Search Examples 69 | 70 | ```javascript 71 | var client = new Stremio.Client() 72 | //client.add('url to my add-on') 73 | client.stream.find({ 74 | query: "gangnam style" 75 | }, function(err, resp) { 76 | }) 77 | ``` 78 | 79 | 80 | 81 | #### Response 82 | 83 | _Example of a response with a link to a media file:_ 84 | 85 | ```javascript 86 | [ 87 | { 88 | basic_id: 'opa2135', // what you set as "id" in the "meta.get" response 89 | availability: 1, // should be at least "1" if the stream works 90 | isFree: 1, // can also be "0" if it's not free 91 | url: 'http://techslides.com/demos/sample-videos/small.mp4', // any streamable url 92 | title: 'HD', // set quality here as string 93 | tag: ['hls'] 94 | } 95 | ] 96 | ``` 97 | 98 | _Example of a response for a torrent:_ 99 | 100 | ```javascript 101 | // Result from stremio.stream.find({ query: { imdb_id: "tt0032138" } }) 102 | [ 103 | { 104 | infoHash: "24c8802e2624e17d46cd555f364debd949f2c81e", // info hash of torrent 105 | mapIdx: 0, // optional, the file number (position) in the torrent 106 | tag: ["mp4", "hd", "1080p", "yifi"], 107 | availability: 2, // good to calculate this based on seeders, if we have them 108 | // 0 seeders -> 0 avail; 0-20 -> 1; 20-50 -> 2; 50+ -> 3; ... 109 | } 110 | ] 111 | // This would start streaming wizard of oz in HD in Stremio 112 | ``` 113 | 114 | See [Stream Response](stream.response.md) for Parameters. 115 | -------------------------------------------------------------------------------- /README-old.md: -------------------------------------------------------------------------------- 1 | ## Stremio Add-ons 2 | 3 | [![Join the chat at https://gitter.im/Stremio/stremio-addons](https://badges.gitter.im/Stremio/stremio-addons.svg)](https://gitter.im/Stremio/stremio-addons?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 4 | 5 | #### _All of the video content [Stremio](http://www.strem.io/) provides, it gets exclusively through this add-on system, with no content or specific provider being built into the app._ 6 | 7 | This package includes both Server and Client for a complete Add-on System. 8 | 9 | Add-ons are being hosted separately (on a server). As such, they have increased security and support their own node modules. 10 | 11 | ### What do they do? 12 | 13 | The purpose of an add-on is to gather media content (not to extend app features) and respond to requests from the Client which will expect: 14 | 15 | - a [manifest](/docs/api/manifest.md) (add-on description) 16 | - an array of [meta elements](/docs/api/meta/meta.element.md) (primary meta feed) 17 | - an array of [meta elements](/docs/api/meta/meta.element.md) (requested by search query) 18 | - one [meta element](/docs/api/meta/meta.element.md) (requested by ID) 19 | - an array of [subtitle objects](/docs/api/subtitles/subtitles.object.md) (requested by ID) 20 | - an array of [stream links](/docs/api/stream/stream.response.md) (requested by ID) 21 | 22 | ### Benefits 23 | 24 | - [Benefits of creating an add-on for Stremio](/docs/BENEFITS.md) 25 | 26 | ### Getting started 27 | 28 | To get started with Add-ons, you first need to have Node.js installed. 29 | 30 | You can scaffold an empty Stremio add-on by running: 31 | 32 | ``` 33 | npm -g install yo generator-stremio-addon # use sudo if you're on Linux 34 | yo stremio-addon 35 | ``` 36 | 37 | ### Documentation 38 | 39 | - [Getting started & Anatomy of an Add-on](/docs/README.md) 40 | - [Manifest](/docs/api/manifest.md) 41 | - [Meta Feed](/docs/api/meta/meta.find.md) 42 | - [Searching](/docs/api/meta/meta.search.md) 43 | - [Meta Element](/docs/api/meta/meta.element.md) 44 | - [Stream Link](/docs/api/stream/README.md) 45 | - [Subtitles](/docs/api/subtitles/README.md) 46 | - [Repositories](/docs/api/repositories.md) 47 | 48 | ### Tutorials 49 | 50 | - [Scaffolding an Add-on](/docs/tutorial/scaffolding.md) 51 | - [Creating an Add-on](https://github.com/Stremio/addon-helloworld) 52 | - [Hosting your Add-on](/docs/tutorial/hosting.md) 53 | - [Publishing an Add-on](/docs/tutorial/publishing.md) 54 | - [Testing Environments](/docs/tutorial/testing.md) 55 | - [Using Cinemeta (meta API)](/docs/tutorial/using-cinemeta.md) 56 | - [Using add-ons client in browser](/docs/tutorial/using-in-browser.md) 57 | - [Add to Your App](/docs/tutorial/add.to.app.md) 58 | - [Hosting multiple add-ons](https://github.com/Stremio/stremio-addons-box) 59 | 60 | ### Demo Add-ons 61 | 62 | - [Hello World](https://github.com/Stremio/addon-helloworld) - basic explanation of how to create a streaming add-on 63 | - [Twitch.tv](https://github.com/Stremio/stremio-twitch) - streams live from Twitch.tv 64 | - [Local Files](http://github.com/Stremio/stremio-local-files) - indexes files found locally and puts them in Stremio 65 | - [Filmon.tv](http://github.com/Stremio/filmon-stremio) - adds TV catalogue from Filmon.tv with streaming 66 | - [WatchHub](http://github.com/Stremio/stremio-watchhub) - redirects to official sources where you can stream movies/series 67 | - [OpenSubtitles](http://github.com/Stremio/stremio-opensubtitles) - find subtitles automatically for the played video file 68 | 69 | _brought to you by [Stremio](http://www.strem.io/)_ 70 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | ### Getting started 2 | 3 | First and foremost, you need to have Node.js installed. Everything in this documentation is valid across any operating system, as long as you have Node.js (required) and bash (optional) installed. 4 | 5 | You can scaffold an empty Stremio add-on by running: 6 | 7 | ``` 8 | npm -g install yo generator-stremio-addon # use sudo if you're on Linux 9 | yo stremio-addon 10 | ``` 11 | 12 | You will find generated Stremio addon source code in `output/` directory. 13 | 14 | 15 | ### Documentation 16 | 17 | - [Benefits - why should I create an add-on?](/docs/BENEFITS.md) 18 | 19 | - [Manifest](/docs/api/manifest.md) 20 | - [Meta Feed](/docs/api/meta/meta.find.md) 21 | - [Searching](/docs/api/meta/meta.search.md) 22 | - [Meta Element](/docs/api/meta/meta.element.md) 23 | - [Stream Link](/docs/api/stream/README.md) 24 | - [Subtitles](/docs/api/subtitles/README.md) 25 | 26 | - [Repositories](/docs/api/repositories.md) 27 | 28 | 29 | ### Tutorials 30 | 31 | - [Creating an Add-on](https://github.com/Stremio/addon-helloworld) 32 | - [Hosting your Add-on](/docs/tutorial/hosting.md) 33 | - [Publishing an Add-on](/docs/tutorial/publishing.md) 34 | - [Testing Environments](/docs/tutorial/testing.md) 35 | - [Using Cinemeta (meta API)](/docs/tutorial/using-cinemeta.md) 36 | - [Using add-ons client in browser](/docs/tutorial/using-in-browser.md) 37 | - [Add to Your App](/docs/tutorial/add.to.app.md) 38 | - [Hosting multiple add-ons](https://github.com/Stremio/stremio-addons-box) 39 | - [Scaffolding an Add-on with yeoman](/docs/tutorial/scaffolding.md) 40 | 41 | ### Anatomy of an Add-on 42 | 43 | ```javascript 44 | var Stremio = require("stremio-addons"); 45 | 46 | var manifest = { 47 | // Basic properties 48 | "id": "org.stremio.basic", // just change "basic" to a shorthand of your add-on 49 | "version": "1.0.0", 50 | 51 | // Properties that determine when Stremio picks this add-on 52 | "types": ["movie"], // can also be "tv", "series", "channel"; your add-on will be preferred for those content types 53 | "idProperty": "imdb_id", // the property to use as an ID for your add-on; your add-on will be preferred for items with that property; can be an array 54 | 55 | // Properties that determine how the add-on looks 56 | "name": "Example Addon", 57 | "description": "Sample addon providing a few public domain movies", 58 | "icon": "URL to 256x256 monochrome png icon", 59 | "background": "URL to 1366x756 png background", 60 | }; 61 | 62 | var addon = new Stremio.Server({ 63 | "stream.find": function(args, callback, user) { 64 | // callback expects array of stream objects 65 | }, 66 | "meta.find": function(args, callback, user) { 67 | // callback expects array of meta object (primary meta feed) 68 | // it passes "limit" and "skip" for pagination 69 | }, 70 | "meta.get": function(args, callback, user) { 71 | // callback expects one meta element 72 | }, 73 | "meta.search": function(args, callback, user) { 74 | // callback expects array of search results with meta objects 75 | // does not support pagination 76 | }, 77 | "meta.genres": function(args, callback, user) { 78 | // callback expects array of strings (genres) 79 | }, 80 | }, manifest); 81 | 82 | var server = require("http").createServer(function (req, res) { 83 | addon.middleware(req, res, function() { res.end() }); // wire the middleware - also compatible with connect / express 84 | }).on("listening", function() 85 | { 86 | console.log("Sample Stremio Addon listening on "+server.address().port); 87 | }).listen(process.env.PORT || 7000); // set port for add-on 88 | 89 | ``` 90 | 91 | -------------------------------------------------------------------------------- /sip/protocol.md: -------------------------------------------------------------------------------- 1 | 2 | ## Stremio Add-on Protocol 3 | 4 | **If you're creating an add-on, we recommend you build it using our [addon-sdk](), which will provide a convenient abstraction to the protocol, as well as an easy way of publishing your add-ons.** 5 | 6 | The Stremio addon protocol defines a universal interface to describe multimedia content. It can describe catalogs, detailed metadata and streams related to multimedia content. 7 | 8 | It is typically transported over HTTP or IPFS, and follows a paradigm similar to REST. 9 | 10 | This allows Stremio or other similar applications to aggregate content seamlessly from different sources, for example YouTube, Twitch, iTunes, Netflix, DTube and others. It also allows developers to build such add-ons with minimal skill. 11 | 12 | To define a minimal add-on, you only need an HTTP server/endpoint serving a `manifest.json` file and responding to resource requests at `/{resource}/{type}/{id}.json`. 13 | 14 | Currently used resources are: `catalog`, `meta`, `stream`. 15 | 16 | `/catalog/{type}/{id}.json` - catalogs of media items; `type` denotes the type, such as `movie`, `series`, `channel`, `tv`, and `id` denotes the catalog ID, which is custom and specified in your manifest - (TODO explain why catalog needs an ID) 17 | 18 | `/meta/{type}/{id}.json` - detailed metadata about a particular item; `type` again denotes the type, and `id` is the ID of the particular item, as found in the catalog 19 | 20 | `/stream/{type}/{id}.json` - list of all streams for a particular items; `type` again denotes the type, and `id` is the ID of the particular item, as found in the catalog or a video ID (a single metadata object may contain mutiple videos, for example a YouTube channel or a TV series) 21 | 22 | The JSON format of the response to these resources is described [here](). 23 | 24 | **NOTE: Your add-on may selectively provide any number of resources. It must provide at least 1 resource and a manifest.** 25 | 26 | 27 | ## Minimal example 28 | 29 | Create a directory called `example-addon` 30 | 31 | `manifest.json`: 32 | 33 | ``` 34 | { 35 | "id": "org.myexampleaddon", 36 | "version": "1.0.0", 37 | "name": "simple Big Buck Bunny example", 38 | "types": [ "movie" ], 39 | "catalogs": [ { type: "movie", id: "top" } ], 40 | "resources": [ "catalog", "stream" ], 41 | "idPrefix": "myexampleaddon" 42 | } 43 | ``` 44 | 45 | TODO: Link object defintion 46 | 47 | `/catalogs/movie/top.json`: 48 | 49 | ``` 50 | { "catalog": [ 51 | { 52 | "id": "myexampleaddon:1", 53 | "type": "movie", 54 | "name": "Big Buck Bunny", 55 | "poster": "https://upload.wikimedia.org/wikipedia/commons/thumb/c/c5/Big_buck_bunny_poster_big.jpg/220px-Big_buck_bunny_poster_big.jpg" 56 | } 57 | ] } 58 | ``` 59 | 60 | TODO: Link object definition 61 | 62 | `/stream/movie/tt1254207.json`: 63 | 64 | ``` 65 | { "streams": [ 66 | { 67 | "name": "", 68 | "url": "http://distribution.bbb3d.renderfarming.net/video/mp4/bbb_sunflower_1080p_30fps_normal.mp4" 69 | }, 70 | { 71 | "name": "", 72 | "url": "" 73 | // TODO: icon 74 | } 75 | ] } 76 | ``` 77 | 78 | TODO: Link object definition 79 | 80 | This add-on is so simple that it can actually be hosted statically on GitHub pages! 81 | 82 | [See example here: TODO]() 83 | 84 | ## Objects 85 | 86 | Metadata 87 | 88 | Stream 89 | 90 | Subtitles 91 | 92 | ## Next steps 93 | 94 | Check out the following tutorials for different languages: 95 | 96 | **If in doubt, and you know JavaScript, use the Node.js SDK** 97 | 98 | * [Creating an add-on with the NodeJS Stremio add-on SDK]() 99 | * [Creating an add-on with Python]() 100 | * [Creating an add-on with PHP]() 101 | * [Creating an add-on with NodeJS and express]() 102 | 103 | -------------------------------------------------------------------------------- /lib/transports/ipfs.js: -------------------------------------------------------------------------------- 1 | const URL = require('url') 2 | const errors = require('../errors') 3 | const IPFS = require('ipfs') 4 | const Room = require('ipfs-pubsub-room') 5 | const thunky = require('thunky') 6 | 7 | const IPFSRepo = require('ipfs-repo') 8 | // Potentially for IPFSRepo 9 | //const path = require('path') 10 | //const os = require('os') 11 | 12 | // @TODO: retry logic 13 | const TIME_TO_RETR_MISSING = 6 * 1000 14 | 15 | const setupIPFS = thunky(function(cb) { 16 | const node = new IPFS({ 17 | EXPERIMENTAL: { 18 | pubsub: true 19 | }, 20 | 21 | // takes path; the impl can take memory 22 | repo: new IPFSRepo('./.jsipfs', { lock: 'memory' }), 23 | 24 | // overload the default IPFS node config, find defaults at https://github.com/ipfs/js-ipfs/tree/master/src/core/runtime 25 | config: { 26 | Addresses: { 27 | Swarm: [ 28 | // @todo: sockets, webrtc, web browser compatible 29 | '/ip4/127.0.0.1/tcp/1338', 30 | //'/dns4/ws-star.discovery.libp2p.io/tcp/443/wss/p2p-websocket-star', 31 | ] 32 | }, 33 | Bootstrap: [ 34 | '/ip4/127.0.0.1/tcp/4001/ipfs/QmYRaTC2DqsgXaRUJzGFagLy725v1QyYwt66kvpifPosgj', 35 | ], 36 | Discovery: { 37 | MDNS: { 38 | Enabled: false 39 | } 40 | } 41 | }, 42 | }) 43 | 44 | node.once('ready', function() { 45 | cb(null, node) 46 | }) 47 | 48 | // @TODO 49 | node.on('error', (err) => { console.error(err) }) 50 | }) 51 | 52 | module.exports = function ipfsTransport(url) 53 | { 54 | const manifestUrl = url.replace('ipfs://', '/ipfs/').replace('ipns://', '/ipns/') 55 | const base = manifestUrl.replace('/manifest.json', '/') 56 | 57 | this.manifest = function(cb) 58 | { 59 | setupIPFS(function(err, node) { 60 | if (err) return cb(err) 61 | retrFile(node, manifestUrl, function(err, resp) { 62 | if (err) return cb(err) 63 | if (!resp || typeof(resp.id) !== 'string') 64 | return cb(errors.ERR_MANIFEST_INVALID) 65 | 66 | // Find the add-on creator peer 67 | // https://github.com/ipfs/js-ipfs/issues/870 68 | //node1.peerRouting.findPeer(node3.peerInfo.id, (err, peer) => { 69 | node.addonRoom = Room(node, resp.id) 70 | node.addonRoom.on('subscribed', function() { 71 | cb(null, resp) 72 | }) 73 | // @TODO TEMP 74 | //node.addonRoom.on 'peer joined' 'peer left' 'subscribed' 75 | }) 76 | }) 77 | } 78 | 79 | this.get = function(args, cb) 80 | { 81 | setupIPFS(function(err, node) { 82 | if (err) return cb(err) 83 | 84 | if (! node.addonRoom) 85 | return cb(errors.ERR_MANIFEST_CALL_FIRST) 86 | 87 | const p = args.join('/') 88 | retrFile(node, base+p+'.json', function(err, res) { 89 | if (err && err.message.match('No such file')) { 90 | node.addonRoom.broadcast(p) 91 | 92 | setTimeout(function() { 93 | retrFile(node, base+p+'.json', cb) 94 | }, TIME_TO_RETR_MISSING) 95 | return 96 | } 97 | 98 | cb(err, res) 99 | }) 100 | }) 101 | } 102 | 103 | this.destroy = function(cb) 104 | { 105 | // @XXX: if you call this without calling manifest/get before, it will create a instance and then kill it 106 | setupIPFS(function(err, node) { 107 | if (err) return cb(err) 108 | node.stop(cb) 109 | }) 110 | } 111 | 112 | function retrFile(node, p, cb) 113 | { 114 | node.files.cat(p, function(err, res) { 115 | if (err) 116 | return cb(err) 117 | 118 | try { 119 | res = JSON.parse(res.toString()) 120 | } catch(e) { 121 | return cb(err) 122 | } 123 | 124 | cb(null, res) 125 | }) 126 | } 127 | 128 | function requestMissing(url, cb) 129 | { 130 | // https://github.com/ipfs/js-ipfs/issues/870 131 | // node1.peerRouting.findPeer(node3.peerInfo.id, (err, peer) => { 132 | // https://github.com/ipfs/js-libp2p-ipfs-nodejs/tree/master/examples/echo 133 | // https://github.com/libp2p/go-libp2p/tree/master/examples/echo 134 | // https://github.com/libp2p/js-libp2p/tree/master/examples/transports 135 | } 136 | // @TODO ipns, or otherwise do not open a pubsub 137 | // @TODO: anti-spam on the pubsub, and research whether all clients have to listen 138 | 139 | return this 140 | } -------------------------------------------------------------------------------- /docs/api/stream/stream.response.md: -------------------------------------------------------------------------------- 1 | ### Stream Response 2 | 3 | **One of the following must be passed** to point to the stream itself 4 | 5 | * ``url`` - direct URL to a video stream - http, https, rtmp protocols supported 6 | * ``yt_id`` - youtube video ID, plays using the built-in YouTube player 7 | * ``infoHash`` and/or ``fileIdx`` - info hash of a torrent file, and `fileIdx` is the index of the video file within the torrent; **if fileIdx is not specified, the largest file in the torrent will be selected** 8 | * ``mapIdx`` - obsolete/legacy alias to ``fileIdx``, specifies index of file in case of BitTorrent 9 | * ``externalUrl`` - URL to the video, which should be opened in a browser (webpage), e.g. link to Netflix 10 | * ``externalUris`` - an array of objects that represent URI to the video; supports linking to iOS or Android apps (see ``externalUri`` docs below) 11 | 12 | **Additional properties to provide information / behaviour flags** 13 | 14 | ``name`` - _optional_ - name of the stream, e.g. "Netflix"; the add-on name will be used if not specified 15 | 16 | ``title`` - _optional_ - title of the stream; usually used for stream quality 17 | 18 | ``availability`` - _optional_ - 0-3 integer representing stream availability, in the context of P2P streams - 0 not available, 1 barely available, 2 OK, 3 highly available 19 | 20 | ``tag`` - _optional_ - array, optional tags of the stream; use ``"480p"``, ``"720p"``, ``"1080p"``/``"hd"`` or ``"2160p"`` to specify quality 21 | 22 | ``isFree`` - _optional_ - set this to ``true`` if the stream si free of charge 23 | 24 | ``isSubscription`` - _optional_ - set this to ``true`` if this stream requires a subscription (e.g. Netflix) 25 | 26 | ``isPeered`` - _optional_ - set this to ``true`` if this stream is peered locally and therefore delivered with a high speed; useful for areas with slow internet connections, such as India 27 | 28 | ``subtitles`` - _optional_ - [``Subtitles Objects``](/docs/api/subtitles/subtitles.object.md) representing subtitles for this stream. Use the `exclusive` flag under `subtitles` if you want Stremio_not to_ try to get subtitles from other add-ons 29 | 30 | ``live`` - _optional_ - boolean, specify if this is a live stream; this will be auto-detected if you're using HLS 31 | 32 | ``repeat`` - _optional_ - boolean, true if you want stremio to do ``stream.find`` again with the same arguments when the video ends, and play the result 33 | 34 | ``geos`` - _optional_ - use if the stream is geo-restricted - array of ISO 3166-1 alpha-2 country codes **in lowercase** in which the stream is accessible 35 | 36 | ``widgetSidebar`` - _optional_ - URL to a page that will be shown in the Player sidebar instead of usual contents; the page will be rendered in a restricted web view, appending "?item_hash=" at the end with Item Hash 37 | 38 | ``widgetPlayer`` - _optional_ - URL to a page that will replace the sit on top of the entire Player; the page will be rendered in a restricted web view, appending "?item_hash=" at the end with Item Hash; useful for things like YouTube/Vimeo embeds, as well as showing additional information/functionality when player is paused 39 | 40 | ``widgetPlayerStates`` - _optional_ - array of the states in which the ``widgetPlayer`` is shown; default is ``["buffering", "loading"]``, which means it will be shown during loading 41 | 42 | Possible states are: 43 | 44 | * ``buffering`` - while the video is buffering 45 | * ``loading`` - white the video is initially loading 46 | * ``paused`` - while the video is paused 47 | * ``postplay`` - after the video has finished playing 48 | * ``error`` - upon player error 49 | * ``device`` - when casting to a device 50 | * ``replaceplayer`` - entirely replaces the default player with the widget 51 | 52 | ``meta`` - _optional_ - object, used to specify ``{ season: X, episode: Y }`` in case you're using a [``Stream Object``](/documentation/protocol.md#stream-object) for ``videos`` for a series 53 | 54 | 55 | #### ``externalUris`` 56 | 57 | ``externalUris`` is an array of objects containing three properties: 58 | 59 | * ``platform`` - platform for which the URI is relevant - possible values are ``android`` and ``ios`` 60 | * ``uri`` - URI to the video; example: ``aiv://aiv/play?asin=B012HPO8TE`` 61 | * ``appUri`` - URI to download the app required, if any; example: ``itms-apps://itunes.apple.com/app/amazon-instant-video/id5455193`` 62 | -------------------------------------------------------------------------------- /sip/TODO.md: -------------------------------------------------------------------------------- 1 | ## DONE 2 | 3 | * draft new client lib with `legacy` support; figure out the architecture of detection and etc. 4 | * Write IPFS-based proto 5 | * see if IPFS pubsub/room has signatures - it has peerId, so it should be secure - there are NO SIGNATURES 6 | * implement built-in promisification 7 | * very simple pubsub impl 8 | * solve the js-ipfs lock issue so we can test N instances at the same time - see ipfs tests and etc 9 | * learn IPFS architecture and re-evaluate the design 10 | * consider localtunnel (lt) instead of pubsub, will be easier and more anonymous: BETTER MAKE IT TUTORIALS 11 | * how does routing work currently in js-ipfs?? why does IPNS depend on "dht being implemented"; DHT is implemented since 0.24.0 12 | * can we broadcast content requests via the DHT? can we improve pubsub? ; NOT A SMART IDEA 13 | * IPFS impl to use pubusb to get missing; also re-eval the pubsub model, perhaps sending a message to someone in the swarm is sufficient 14 | * figure out IPNS slowness and how to work around; also IPNS is not implemented in js-ipfs 15 | * Move out client to a new repo 16 | * Decide on the new set of modules - `stremio-addon-sdk`, `stremio-addon-client`, `stremio-aggregators` 17 | * consider the response formats - everything will be wrapped 18 | * Move out SDK/docs to a new repo 19 | * think about catalog pagination and Search; consider filters for Discover - maybe an internal thing for Cinemeta: search will be a new resource, pagination part of the ID 20 | * stremio-aggregators 21 | * stremio-addon-{sdk,client} and stremio-aggregators: think of code style, consider linter/flux/etc. 22 | * Docs: explain idProperty better or change the standard to make more sense 23 | * Figure out reloading/refresh - will just re-initialize an aggregator every time 24 | * stremio-aggregators: consider reloading behaviour (addoncollection), i.e. what happens when an addon is installed or removed; DECIDED: we just re-construct everything 25 | * FIGURE OUT: addon discovery 26 | * Extra args 27 | * think about publishing/discovery and claiming an ID; ipns solves that, but url-based addons do not 28 | * FIGURE OUT: user-installed add-ons: will be a separate AddonCollection 29 | * SDK: enforce to at least lint the manifest (e.g. .catalogs) 30 | * consider stremio-addon-models that also enforces types/validation; OR implement a linter; also lint/validate the manifest 31 | * basic tests for stremio-addon-sdk 32 | * basic tests for stremio-aggregators 33 | * Publish to the API: `addonPublish { transportUrl, transportName } -> { success: 1 }` 34 | * think about whether we should bring back auto landing pages and how; also this can play along with SEO if there is a page for everything; also think of landing pages + authentication such as put.io; nah, this works via in-stremio page (app.strem.io/addon?url=...) 35 | * consider cache - should be a concern of the transport, mostly the p2p one 36 | * consider how to handle JSON parse errors and 404 37 | * Write Docs 38 | 39 | ## TODO 40 | 41 | * Write Spec 42 | * SDK: Default function for publish (crawls all known resources), also validates and should be ran in testing too 43 | * `stremio-addon-client`: ability to set header for every request (for example `X-User-ID`) 44 | * Tutorials like 'Create a hosted add-on with nodejs', 'Create a hosted add-on with Python', 'Create a hosted add-on with Go', 'Create a hosted add-on with PHP', 'Create a hosted add-on with C#' 45 | * Example: publish an add-on via now.sh; also publishToCentral 46 | * Example: publish an add-on via localtunnel; consider other possibilities that are easy 47 | * Tutorial: how to make a metadata add-on with search 48 | 49 | ## TODO p2p 50 | 51 | * stremio p2p add-ons: consider a tendermint-like architecture where the replication service runs; replicationService<->add-on; this can connect directly via the stremio add-on protocol itself (over HTTP), and be a separate module "stremio-addon-p2p-replicator" or smth; that way even legacy add-ons can be replicated 52 | * stremio addons Proto SDK, with http and ws (supernode) exposed connections 53 | ipfs config with bootstrap nodes, webrtc/websocket local multiaddrs 54 | test them in an automated test 55 | in `--publish` mode, log a gateway-based ipfs url to the manifest (it references peer ID, therefore the addon can update itself with IPNS) 56 | * IPFS `requestUpdate` message: broadcast a message to the creator peer and "aggregator" peers to fetch / update an entry; use WebRTC 57 | * IPFS delegated nodes helping with routing and broadcasting/keeping track of `requestUpdate` 58 | * IPNS over js-ipfs using the delegated routing nodes 59 | * example addon based on the SDK 60 | * tutorial: 'Create and publish a peer-to-peer addon with NodeJS' 61 | * UI: figure out how to show add-ons that have not been online for a while - because they will just get increasingly outdated, but not offline -------------------------------------------------------------------------------- /docs/api/manifest.md: -------------------------------------------------------------------------------- 1 | ### Manifest format 2 | 3 | The first thing to define for your add-on is the manifest, which describes it's name, purpose and some technical details. 4 | 5 | Valid properties are: 6 | 7 | ``id`` - **required** - identifier, dot-separated, e.g. "com.stremio.filmon" 8 | 9 | ``name`` - **required** - human readable name 10 | 11 | ``description`` - **required** - human readable description 12 | 13 | ``idProperty`` - **required** - ID property of the Meta or Streams that this add-on delivers - for example ``imdb_id`` or ``filmon_id``; can be string or array of strings 14 | 15 | ``types`` - **required** - array of supported types, from all the [``Content Types``](./meta/content.types.md) 16 | 17 | **IMPORTANT** - ``types`` and ``idProperty`` will be used when Stremio selects add-ons to call for a certain request. For example, if the user wants to watch Metropolis, the query would be ``{ type: "movie", imdb_id: "tt0017136" }``, your add-on has to have ``imdb_id`` in the manifest ``idProperty`` and ``movie`` in the manifest ``types``. 18 | 19 | ``webDescription`` - _optional_ - human readable description for the auto-generated add-on HTML page ; HTML allowed 20 | 21 | ``endpoint`` - _optional_ - http endpoint to the hosted version of this add-on; should end in standard stremio URL path, such as ``/stremio/v1`` for the v1 version of the protocol; example: ``http://cinemeta.strem.io/stremioget/stremio/v1`` 22 | 23 | ``dontAnnounce`` - _optional_ - do not announce to stremio add-on tracker; this means that your add-on won't be listed on [addons.strem.io](http://addons.strem.io) even if it has a valid `endpoint` 24 | 25 | **IMPORTANT** - At every start of the add-on server, the add-on will attempt to announce itself to the Stremio central API. If you have a valid `endpoint` in place, the central API will start showing it in the [Add-on catalogue](https://addons.strem.io). 26 | 27 | ``background`` - _optional_ - background image for the add-on; URL to png/jpg, at least 1024x786 resolution 28 | 29 | ``logo`` - _optional_ - logo icon, URL to png, monochrome, 256x256 30 | 31 | ``isFree`` - _optional_ - set this to ``true`` if you want to specify that all of the content in this add-on is free of charge; this is used when auto-generating a landing page for that add-on 32 | 33 | ``contactEmail`` - **required** - contact email for add-on issues; used for the Report button in the app; also, the Stremio team may reach you on this email for anything relating your add-on 34 | 35 | ``suggested`` - _optional_ - array of IDs of other add-ons that should be suggested when installing this add-on 36 | 37 | ``sorts`` - _optional_ - additional types of sorting in catalogues; array of sort objects 38 | 39 | ```javascript 40 | [ 41 | { 42 | prop: "popularities.moviedb", 43 | name: "SORT_TRENDING", 44 | types: ["movie", "series"], 45 | noDiscoverTab: false, // hide this sort from Discover 46 | countrySpecific: false // force Stremio to send country code with the meta.find 47 | } 48 | ] 49 | ``` 50 | 51 | ***TIP* - use different sorts to provide different catalogues for your users, e.g. separate "popular movies" and "new movies". This will appear as a tab in Discover and as a row in Board** 52 | 53 | 54 | ``listedOn`` - _optional_ - array - where is this add-on listed - there are four possible values - ``web``, ``desktop`` ([addons.strem.io](http://addons.strem.io)), ``android``, ``ios``; by default, the value is set to ``["web", "desktop", "android"]``. To hide the add-on from all catalogues, just pass an empty array (``listedOn: []``) 55 | 56 | ***WARNING* - unlike the other platforms, getting the add-on listed on ``ios`` may require moderator approval** 57 | 58 | 59 | ``searchDebounce`` - _optional_ - how much to de-bounce after the user types before calling ``meta.search`` 60 | 61 | ``countrySpecific`` - _optional_ - boolean - if true, the stremio client must pass ``countryCode`` of the user along with ``meta.find``. *Example*: add-on for service where the streams are georestricted, e.g. Netflix; you can use this either directly in ``manifest``, or under one or more of the ``sorts`` 62 | 63 | ``zipSpecific`` - _optional_ - boolean - if true, the stremio client must pass ``zip`` code of the user along with ``meta.find``. *Example*: cinema showtimes guide add-on where result is specific to city 64 | 65 | ``countrySpecificStreams`` - _optional_ - boolean - if true, the stremio client must pass ``countryCode`` of the user along with ``stream.find``, so that it can return geo-specific results. Please note that returning ``geos`` in the response [``Stream objects``](./stream/stream.response.md) is preferred over returning geo-specific results from ``stream.find``, but this is allowed if you have a data limitation 66 | 67 | ***TIP* - to implement sources where streams are geo-restricted (stream.find), see [``Stream object's``](./stream/stream.response.md) `geos`** 68 | 69 | -------------------------------------------------------------------------------- /test/v3.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const AddonClient = require('../lib/client') 4 | const errors = require('../lib/errors') 5 | 6 | const tape = require('tape') 7 | 8 | tape('detectFromURL: invalid protocol', function(t) { 9 | AddonClient.detectFromURL('ftp://cinemeta.strem.io', function(err, res) { 10 | t.equals(err, errors.ERR_PROTOCOL, 'err is the right type') 11 | t.notOk(res, 'no response') 12 | t.end() 13 | }) 14 | }) 15 | 16 | tape('detectFromURL: legacy protocol', function(t) { 17 | // https://cinemeta.strem.io/stremioget/stremio/v1/q.json?b=eyJwYXJhbXMiOltdLCJtZXRob2QiOiJtZXRhIiwiaWQiOjEsImpzb25ycGMiOiIyLjAifQ== 18 | AddonClient.detectFromURL('https://cinemeta.strem.io/stremioget/stremio/v1', function(err, res) { 19 | t.error(err, 'no error from detectFromURL') 20 | t.ok(res.addon, 'addon is ok') 21 | t.ok(res.addon.manifest, 'manifest is ok') 22 | t.deepEqual(res.addon.manifest.catalogs, [{ type: 'series', id: 'top' }, { type: 'movie', id: 'top' }], 'catalogs is right') 23 | t.deepEqual(res.addon.manifest.resources, ['meta'], 'resources is right') 24 | res.addon.get('catalog', res.addon.manifest.catalogs[0].type, res.addon.manifest.catalogs[0].id, function(err, resp) { 25 | t.error(err, 'no error from catalog') 26 | t.ok(resp, 'has response') 27 | t.ok(Array.isArray(resp.metas), 'response is an array') 28 | t.ok(resp.metas.length === 70, 'response is full length') 29 | 30 | res.addon.get('meta', resp.metas[0].type, resp.metas[0].imdb_id, function(err, resp) { 31 | t.error(err, 'no error from meta') 32 | 33 | t.ok(resp.meta, 'has meta') 34 | t.ok(resp.meta.fanart, 'has fanart') 35 | 36 | t.end() 37 | }) 38 | }) 39 | }) 40 | }) 41 | 42 | tape('detectFromURL: detect and use manifest.json URL', function(t) { 43 | const ipfsURL = 'https://gateway.ipfs.io/ipfs/QmeZ431sbdzuqJppkiGMTucuZxwBH7CffQMtftkLDypBrg/manifest.json' 44 | const ipnsURL = 'https://gateway.ipfs.io/ipns/QmYRaTC2DqsgXaRUJzGFagLy725v1QyYwt66kvpifPosgj/manifest.json' 45 | 46 | let addon 47 | AddonClient.detectFromURL(ipfsURL) 48 | .then(function(res) { 49 | t.ok(res.addon, 'addon is ok') 50 | t.ok(res.addon.manifest, 'manifest is ok') 51 | t.deepEqual(res.addon.manifest.catalogs, ['top'], 'catalogs is right') 52 | t.deepEqual(res.addon.manifest.resources, ['meta', 'stream'], 'resources is right') 53 | 54 | addon = res.addon 55 | 56 | return addon.get('catalog', 'top') 57 | }) 58 | .then(function(resp) { 59 | t.ok(resp && Array.isArray(resp.metas), 'response is an array') 60 | 61 | return addon.get('meta', resp.metas[0].type, resp.metas[0].id) 62 | }) 63 | .then(function(resp) { 64 | t.ok(resp.meta.id, 'meta has id') 65 | 66 | return addon.get('stream', resp.meta.type, resp.meta.id) 67 | }) 68 | .then(function(resp) { 69 | t.ok(Array.isArray(resp.streams), 'streams is array') 70 | t.equal(resp.streams.length, 2, 'streams is right length') 71 | t.end() 72 | }) 73 | .catch(function(err) { 74 | t.error(err, 'no error') 75 | t.end() 76 | }) 77 | }) 78 | 79 | tape('detectFromURL: IPFS: detect and use manifest.json URL', function(t) { 80 | const ipfsURL = 'ipfs://QmeZ431sbdzuqJppkiGMTucuZxwBH7CffQMtftkLDypBrg/manifest.json' 81 | const ipnsURL = 'ipns://QmYRaTC2DqsgXaRUJzGFagLy725v1QyYwt66kvpifPosgj/manifest.json' 82 | 83 | let addon 84 | 85 | AddonClient.detectFromURL(ipfsURL) 86 | .then(function(res) { 87 | t.ok(res.addon, 'addon is ok') 88 | t.ok(res.addon.manifest, 'manifest is ok') 89 | t.deepEqual(res.addon.manifest.catalogs, ['top'], 'catalogs is right') 90 | t.deepEqual(res.addon.manifest.resources, ['meta', 'stream'], 'resources is right') 91 | 92 | addon = res.addon 93 | 94 | return addon.get('catalog', 'top') 95 | }) 96 | .then(function(resp) { 97 | t.ok(Array.isArray(resp.metas), 'response is an array') 98 | return addon.get('meta', resp.metas[0].type, resp.metas[0].id) 99 | }) 100 | .then(function(resp) { 101 | t.ok(resp.meta.id, 'meta has id') 102 | 103 | return addon.get('stream', 'movie', parseInt(Math.random()*1000)) 104 | }) 105 | .then(function(resp) { 106 | console.log(resp) 107 | // IPFS addons need to be destroyed in order to allow the proc to exit 108 | addon.destroy(function() { t.end() }) 109 | }) 110 | .catch(function(err) { 111 | t.error(err, 'no error') 112 | 113 | // IPFS addons need to be destroyed in order to allow the proc to exit 114 | if (addon) addon.destroy(function() { t.end() }) 115 | else t.end() 116 | }) 117 | }) 118 | 119 | 120 | 121 | // @TODO: detectFromURL: not recognized json response (ERR_RESP_UNRECOGNIZED) 122 | 123 | // @TODO: detectFromURL: linked to a landing page with x-stremio-addon 124 | 125 | // @TODO: detectFromURL: linked directly to manifest.json 126 | 127 | // @TODO: detectFromURL: .get() in http transport: 404 and etc. handled accordingly 128 | 129 | // @TODO: constructFromManifest: invalid transport 130 | 131 | // @TODO: constructFromManifest: constructs successfully 132 | -------------------------------------------------------------------------------- /docs/api/meta/meta.element.md: -------------------------------------------------------------------------------- 1 | ### Meta Element 2 | 3 | The response is an array of Metadata objects. 4 | 5 | #### Metadata object 6 | 7 | ``id`` - **required** - universal identifier, formed like "DOMAIN_id:ID", for example "yt_id:UCrDkAvwZum-UTjHmzDI2iIw". 8 | 9 | ``type`` - **required** - type of the content; e.g. `movie`, `series`, `channel`, `tv` (see [Content Types](content.types.md)) 10 | 11 | ``name`` - **required** - name of the content 12 | 13 | ``genre`` - **required** - genre/categories of the content; array of strings, e.g. ``["Thriller", "Horror"]`` 14 | 15 | ``poster`` - **required** - URL to png of poster; accepted aspect ratios: 1:0.675 (IMDb poster type) or 1:1 (square) ; you can use any resolution, as long as the file size is below 100kb; below 50kb is recommended 16 | 17 | ``posterShape`` - _optional_ - can be `square` (1:1 aspect) or `regular` (1:0.675) or `landscape` (1:1.77). If you don't pass this, `regular` is assumed 18 | 19 | ``background`` - _optional_ - the background shown on the stremio detail page ; heavily encouraged if you want your content to look good; URL to PNG, max file size 500kb 20 | 21 | ``description`` - _optional_ - a few sentances describing your content 22 | 23 | ``year`` - _optional_ - string - year the content came out ; if it's ``series`` or ``channel``, use a start and end years split by a tide - e.g. ``"2000-2014"``. If it's still running, use a format like ``"2000-"`` 24 | 25 | ``director``, ``cast`` - _optional_ - directors and cast, both arrays of names 26 | 27 | ``imdbRating`` - _optional_ - IMDb rating, a number from 0 to 10 ; use if applicable 28 | 29 | ``dvdRelease`` - _optional_ - DVD release date 30 | 31 | ``released`` - _optional_ - initial release date; for movies, this is the cinema debut 32 | 33 | ``inTheaters`` - _optional_ - used only for ``movie`` type, boolean whether this movie is still in theaters or not; if not provided, it will be decided based on ``released`` date 34 | 35 | ``videos`` - _optional_ - used for ``channel``, array of Video objects 36 | 37 | ``uploads`` - _optional_ - used for ``channel``, array of Video objects; same as ``videos`` but **OBSOLETE** 38 | 39 | ``episodes`` - _optional_ - **OBSOLETE**, replaced by ``videos`` 40 | 41 | ``certification`` - _optional_ - [MPAA rating](http://www.mpaa.org/film-ratings/) - can be "G", "PG", "PG-13", "R", "NC-17" 42 | 43 | ``runtime`` - _optional_ - human-readable expected runtime - e.g. "120m" 44 | 45 | ``language`` - _optional_ - spoken language 46 | 47 | ``country`` - _optional_ - official country of origin 48 | 49 | ``awards`` - _optional_ - human-readable string that describes all the significant awards 50 | 51 | ``website`` - _optional_ - URL to official website 52 | 53 | ``isPeered`` - _optional_ - set this property if you know whether that item can be streamed with peering by the same add-on which is serving the meta 54 | 55 | #### Video object 56 | 57 | ``id`` - **required** - ID of the video; you can skip this only if you've passed ``season`` and ``episode`` 58 | 59 | ``title`` - **required** - title of the video 60 | 61 | ``publishedAt`` - **required** - Date, publish date of the video; for episodes, this should be the initial air date 62 | 63 | ``thumbnail`` - _optional_ - URL to png of the video thumbnail, in the video's aspect ratio, max file size 5kb 64 | 65 | ``stream`` - _optional_ - In case you can return links to streams while forming meta response, **you can pass the [``Stream Object``](/docs/api/stream/stream.response.md)** to point the video to a HTTP URL, BitTorrent, YouTube or any other stremio-supported transport protocol. 66 | 67 | ``available`` - _optional_ - set to ``true`` to explicitly state that this video is available for streaming, from your add-on; no need to use this if you've passed ``stream`` 68 | 69 | ``episode`` - _optional_ - episode number, if applicable 70 | 71 | ``season`` - _optional_ - season number, if applicable 72 | 73 | ``trailer`` - _optional_ - YouTube ID (string) of the trailer video; use if this is an episode for a series 74 | 75 | ``overview`` - _optional_ - video overview/summary 76 | 77 | _**NOTE** - In case you've provided ``id``, the query to ``stream.find`` for playing that video will contain ``video_id`` property with the same value._ 78 | 79 | _In case you've provided ``season`` and ``episode`` combination, both would be contained in the query to ``stream.find``._ 80 | 81 | ##### Video object - series example 82 | 83 | ```javascript 84 | { 85 | id: "1:1", 86 | title: "Pilot", 87 | publishedAt: new Date("1994-09-22 20:00 UTC+02"), 88 | season: 1, 89 | episode: 1, 90 | overview: "Monica and the gang introduce Rachel to the real world after she leaves her fiancé at the altar." 91 | } 92 | ``` 93 | 94 | ##### Video object - YouTube video example (channels) 95 | 96 | 97 | ```javascript 98 | { 99 | id: "9bZkp7q19f0", 100 | title: "PSY - GANGNAM STYLE", 101 | publishedAt: new Date("2012-07-15 20:00 UTC+02"), 102 | thumbnail: "https://i.ytimg.com/vi/9bZkp7q19f0/hqdefault.jpg" 103 | } 104 | ``` 105 | 106 | -------------------------------------------------------------------------------- /lib/transports/legacy.js: -------------------------------------------------------------------------------- 1 | const fetch = require('node-fetch') 2 | const errors = require('../errors') 3 | 4 | // Legacy add-on adapter 5 | // Makes legacy add-ons magically work with the new API 6 | // This is very ugly but a necessary evil 7 | 8 | module.exports = function legacyTransport(url) 9 | { 10 | this.manifest = function(cb) 11 | { 12 | jsonRPCRequest('meta', [], function(err, resp) { 13 | if (err) 14 | return cb(err) 15 | 16 | let v3Manifest 17 | let error 18 | 19 | cb(null, mapManifest(resp)) 20 | }) 21 | } 22 | 23 | this.get = function(args, cb) 24 | { 25 | let resource = args[0] 26 | 27 | if (args.length !== 3) 28 | return cb(errors.ERR_UNSUPPORTED_RESOURCE) 29 | 30 | if (resource == 'catalog') { 31 | jsonRPCRequest('meta.find', [null, remapCatalog(args)], wrapResp('metas', cb)) 32 | } 33 | else if (resource == 'meta') { 34 | jsonRPCRequest('meta.get', [null, remapMeta(args)], wrapResp('meta', cb)) 35 | } 36 | else if (resource == 'stream') { 37 | jsonRPCRequest('stream.find', [null, remapStream(args)], wrapResp('streams', cb)) 38 | } 39 | /* 40 | else if (resource == 'subtitles') { 41 | jsonRPCRequest('subtitles.find', [null, remapSubs(args)], wrapResp('subtitles', cb)) 42 | }*/ 43 | else { 44 | cb(errors.ERR_UNSUPPORTED_RESOURCE) 45 | } 46 | } 47 | 48 | function jsonRPCRequest(method, params, cb) 49 | { 50 | const body = JSON.stringify({ params: params, method: method, id: 1, jsonrpc: '2.0' }) 51 | const reqUrl = url + '/q.json?b=' + new Buffer(body).toString('base64') 52 | 53 | fetch(reqUrl) 54 | .then(function(resp) { 55 | if (resp.status !== 200) return cb(errors.ERR_BAD_HTTP) 56 | else return resp.json() 57 | }) 58 | .then(function(resp) { 59 | cb(resp.error, resp.result) 60 | }) 61 | .catch(cb) 62 | } 63 | 64 | function wrapResp(name, cb) 65 | { 66 | return function(err, res) { 67 | if (err) return cb(err) 68 | 69 | var o = { } 70 | o[name] = res 71 | return cb(null, o) 72 | } 73 | } 74 | 75 | function mapManifest(resp) 76 | { 77 | const manifest = resp.manifest 78 | let v3Manifest = { 79 | id: manifest.id, 80 | 81 | name: manifest.name, 82 | description: manifest.description, 83 | contactEmail: manifest.contactEmail, 84 | 85 | logo: manifest.logo, 86 | background: manifest.background, 87 | 88 | idProperty: manifest.idProperty, 89 | types: manifest.types, 90 | 91 | resources: [], 92 | catalogs: [], 93 | 94 | url: url, 95 | transport: 'legacy' 96 | } 97 | 98 | const sorts = Array.isArray(manifest.sorts) ? manifest.sorts : [ null ] 99 | 100 | if (resp.methods.indexOf('meta.find') !== -1) { 101 | sorts.forEach(function(sort) { 102 | ((sort && sort.types) || manifest.types).forEach(function(type) { 103 | if (! type) return 104 | 105 | let key = type 106 | if (sort) { 107 | key += ':' + sort.prop 108 | if (sort.countryCode) key += ':COUNTRY' 109 | } 110 | 111 | v3Manifest.catalogs.push({ type: key, id: 'top' }) 112 | }) 113 | }) 114 | } 115 | 116 | if (resp.methods.indexOf('meta.get') !== -1) 117 | v3Manifest.resources.push('meta') 118 | 119 | if (resp.methods.indexOf('stream.find') !== -1) 120 | v3Manifest.resources.push('stream') 121 | 122 | return v3Manifest 123 | } 124 | 125 | function remapCatalog(args) 126 | { 127 | let spl = args[1].split(':') 128 | let req = { query: { type: spl[0] }, limit: 70 } 129 | 130 | if (spl[1]) { 131 | // Just follows the convention set out by stremboard 132 | // L287 cffb94e4a9c57f5872e768eff25164b53f004a2b 133 | req.query.sort = { } 134 | req.query.sort[spl[1]] = -1 135 | req.query.sort['popularity'] = -1 136 | } 137 | if (spl[2]) req.countryCode = spl[2].toLowerCase() 138 | 139 | return req 140 | } 141 | 142 | function remapMeta(args) 143 | { 144 | let req = { query: { } } 145 | 146 | // type is not used 147 | const id = args[2].split(':') 148 | if (id[0].match('^tt')) req.query.imdb_id = id[0] 149 | else req.query[id[0]] = id[1] 150 | 151 | return req 152 | } 153 | 154 | function remapStream(args) 155 | { 156 | let req = { query: { } } 157 | 158 | req.query.type = args[1] 159 | 160 | let id = args[2].split(':') 161 | if (id[0].match('^tt')) { 162 | req.query.imdb_id = id[0] 163 | id = id.slice(1) 164 | } else { 165 | req.query[id[0]] = id[1] 166 | id = id.slice(2) 167 | } 168 | 169 | if (id.length == 2) { 170 | req.query.season = parseInt(id[0]) 171 | req.query.episode = parseInt(id[1]) 172 | } 173 | if (id.length == 1) { 174 | req.query.video_id = id[0] 175 | } 176 | 177 | return req 178 | } 179 | 180 | /* 181 | function remapSubs(args) 182 | { 183 | // @TODO 184 | return req 185 | }*/ 186 | 187 | // Examples 188 | //console.log(remapStream(['stream', 'channel', 'yt_id:UCaFoZFhV1LgbFIB3-6zdWVg:OLT7x6mpBq4'])) 189 | //console.log(remapStream(['stream', 'series', 'tt0386676:1:1'])) 190 | 191 | return this 192 | } 193 | -------------------------------------------------------------------------------- /addon-template.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <%= addon.manifest.name %> - Stremio Addon 5 | 57 | 58 |
59 | <% if (addon.manifest.logo) { %> 60 | 61 | <% } else if (addon.manifest.icon) { %> 62 |
63 | <% } else { %> 64 |

<%= addon.manifest.name %>

65 | <% } %> 66 | 67 |

<%= addon.manifest.version %>

68 |

<%- addon.manifest.webDescription || addon.manifest.description %>

69 | 70 | <% if (stats.statsLabel) { %> 71 |

<%= (stats.statsLabel || "") + stats.statsNum %>

72 | <% } %> 73 | 74 | <% var mapType = function(t) { return ({ movie: "Movies", series: "Series", tv: "TV Channels", channel: "Channels" })[t] || t }; %> 75 | 76 |

This add-on has:

77 | 86 | 87 |
88 |

Best from this add-on:

89 |
90 | <% top.filter(function(x) { return x.poster }).slice(0,5).forEach(function(item) { %> 91 | <%= item.name+' ('+item.year+')' %> 92 | <% }); %> 93 |
94 |
95 | 96 |

<%- addon.manifest.webDescription || addon.manifest.description %>

97 | 98 | <% if (endpoint) { %> 99 | 100 | <% } else { %> 101 |
No valid end-point for this add-on
102 | <% } %> 103 | 104 |

To contact add-on creator: <%- addon.manifest.email %>

105 | 106 |
107 | 108 | <% if (endpoint) { %> 109 | 118 | <% } %> 119 | 120 | 121 | -------------------------------------------------------------------------------- /test/addon-protocol.js: -------------------------------------------------------------------------------- 1 | var stremio = require("../"); 2 | var tape = require("tape"); 3 | var _ = require("underscore"); 4 | var async = require("async"); 5 | 6 | var NETWORK_TIMEOUT = 35*1000; 7 | 8 | var TEST_SECRET = "51af8b26c364cb44d6e8b7b517ce06e39caf036a"; 9 | 10 | var addons = process.argv.filter(function(x) { return x.match("^http") }); 11 | if (! addons.length) throw "No add-ons specified"; 12 | 13 | var slackPush, slackChannel, slackMessage, noStats, shortLog; 14 | process.argv.forEach(function(x) { 15 | if (x.match("--slack-push")) slackPush = x.split("=")[1]; 16 | if (x.match("--slack-channel")) slackChannel = x.split("=")[1]; 17 | if (x.match("--slack-message")) slackMessage = x.split("=")[1]; 18 | if (x.match("--no-stats")) noStats = true; 19 | if (x.match("--short-log")) shortLog = true; 20 | }); 21 | 22 | var topitems = [], hasErr; // global, so we can test meta and stream add-ons at once 23 | 24 | async.eachSeries(addons, function(url, ready) { 25 | var test = tape.createHarness(); 26 | 27 | /* Send errors to Slack webhook 28 | */ 29 | var errors = 0, output = []; 30 | // WARNING: we can't do createStream more than once, because it bugs the first test 31 | test.createStream({ /* objectMode: true */ }).on("data", function(x) { 32 | if (x.match("^not ok")) { errors++; hasErr = true } 33 | output.push(x); 34 | process.stdout.write(x); 35 | }).on("end", function() { 36 | if (errors < 2) return ready(); 37 | if (!slackPush) return ready(); 38 | 39 | var body = require("querystring").stringify({ payload: JSON.stringify({ 40 | channel: slackChannel || "#mon-stremio", username: "webhookbot", 41 | text: "*WARNING: "+url+" failing "+(slackMessage || "")+" with "+errors+" errors *\n", 42 | icon_emoji: ":bug:" 43 | }) }); 44 | 45 | console.log("Sending errors to slack"); 46 | var req = require("https").request(_.extend(require("url").parse(slackPush), { 47 | headers: { "Content-Type": "application/x-www-form-urlencoded", "Content-Length": body.length }, 48 | method: "POST" 49 | }), function() { 50 | console.log("Sent errors to slack"); 51 | ready(); 52 | }); 53 | req.write(body); 54 | req.end(); 55 | }); 56 | 57 | 58 | var s = new stremio.Client({ timeout: NETWORK_TIMEOUT }); 59 | s.setAuth(null, TEST_SECRET); 60 | 61 | var LID; 62 | test("is available - fires addon-ready", function(t) { 63 | s.add(url, { priority: 1 }); 64 | 65 | var timeout = setTimeout(function() { t.error("timed out"); t.end() }, 10000) 66 | 67 | s.once("addon-ready", function(addon) { 68 | clearTimeout(timeout); 69 | t.ok(addon && addon.url == url, "has proper url"); 70 | if (addon.manifest.stremio_LID) LID = addon.manifest.stremio_LID; 71 | t.end(); 72 | }); 73 | }); 74 | 75 | 76 | test("stats.get responds", function(t) { 77 | if (noStats) { t.skip("--no-stats"); t.end(); return } 78 | s.call("stats.get", { }, function(err, stats) { 79 | t.error(err, "has error"); 80 | t.ok(stats, "has results"); 81 | t.ok(stats && stats.statsNum, "has statsNum"); 82 | if (stats && stats.stats) stats.stats.forEach(function(s) { 83 | t.notEqual(s.color || s.colour, "red", "square "+s.name+" is not red"); 84 | }); 85 | t.end(); 86 | }); 87 | }); 88 | 89 | // Test if an add-on implements the Stremio protocol OK and responds 90 | test("meta.find - get top 70 items", function(t) { 91 | if (!s.get("meta.find").length) { t.skip("no meta.find in this add-on"); return t.end(); } 92 | 93 | s.meta.find({ query: { type: "movie" }, limit: 70, sort: { popularity: -1 } }, function(err, meta) { 94 | t.error(err); 95 | t.ok(meta, "has results"); 96 | t.ok(meta && meta.length == 70, "70 items"); 97 | topitems = meta ? meta.filter(function(x) { 98 | return LID ? (x.popularities && x.popularities[LID]) : x.popularity 99 | }).slice(0, 15) : topitems; 100 | t.ok(topitems.length, "has popular items"); 101 | t.end(); 102 | }); 103 | }); 104 | 105 | test("meta.find - collect genres", function(t) { 106 | if (!s.get("meta.find").length) { t.skip("no meta.find in this add-on"); return t.end(); } 107 | 108 | s.meta.find({ query: { }, limit: 500, projection: { genre: 1 }, sort: { popularity: -1 } }, function(err, meta) { 109 | t.error(err); 110 | t.ok(meta, "has results"); 111 | var genres = { }; 112 | if (meta) meta.forEach(function(x) { x.genre && x.genre.forEach(function(g) { genres[g] = 1 }) }); 113 | t.ok(Object.keys(genres).length > 3, "more than 3 genres"); 114 | t.end(); 115 | }); 116 | }); 117 | 118 | test("stream.find for top items of meta.find", function(t) { 119 | if (!s.get("stream.find").length) { t.skip("no stream.find in this add-on"); return t.end(); } 120 | if (! (topitems && topitems.length)) { t.skip("no topitems"); return t.end(); } 121 | 122 | async.eachSeries(topitems, function(item, next) { 123 | t.comment("trying "+item.name); 124 | s.stream.find({ query: _.pick(item, "imdb_id", "yt_id", "filmon_id", "type") }, function(err, streams) { 125 | t.error(err); 126 | t.ok(streams && streams.length, "has streams"); 127 | var stream = streams && streams[0]; 128 | 129 | if (! stream) return next(new Error("no stream")); 130 | 131 | t.ok(stream.hasOwnProperty("availability"), "has availability"); 132 | t.ok(stream.availability > 0, "availability > 0"); 133 | t.ok(stream.url || stream.yt_id || (stream.infoHash && stream.hasOwnProperty("fileIdx")), "has an HTTP / YouTube / BitTorrent stream"); 134 | next(); 135 | }); 136 | 137 | }, function() { t.end() }); 138 | }); 139 | 140 | 141 | test("subtitles.get for top items of meta.find", function(t) { 142 | if (!s.get("subtitles.get").length) { t.skip("no subtitles.get in this add-on"); return t.end(); } 143 | if (! (topitems && topitems.length)) { t.skip("no topitems"); return t.end(); } 144 | 145 | async.eachSeries(topitems, function(item, next) { 146 | t.comment("trying "+item.name); 147 | s.subtitles.get({ 148 | hash: item.type == "movie" ? item.imdb_id : item.imdb_id+" 1 1", 149 | meta: item.type=="series" ? 150 | { imdb_id: item.imdb_id, season: item.state.season, episode: item.state.episode } : 151 | { imdb_id: item.imdb_id } 152 | }, function(err, resp) { 153 | t.error(err); 154 | t.ok(resp && resp.subtitles, "has subtitles"); 155 | t.ok(resp && resp.hash, "has item hash"); 156 | next(); 157 | }); 158 | }, function() { t.end() }); 159 | }); 160 | 161 | 162 | //test("meta.find - particular genre") 163 | //test("meta.find - returns valid results") // copy from filmon addon 164 | 165 | }, function() { 166 | process.exit(hasErr ? 1 : 0); 167 | }); 168 | -------------------------------------------------------------------------------- /sip/p2p.md: -------------------------------------------------------------------------------- 1 | # Stremio P2P addons (v3) 2 | 3 | This document describes a new specification for Stremio add-ons, which allow much more freedom in how an add-on is created and hosted. 4 | 5 | ## Revised spec 6 | 7 | ( @TODO: move this to a separate doc ) 8 | 9 | Let us describe a v3 add-on as having: 10 | 11 | 1. A manifest 12 | 2. One or more metadata catalogs (listed in the manifest) 13 | 3. Supported type(s) and supported idPrefix(es) - used to determine whether to perform a `/stream/` or `/meta/` 14 | 15 | The new add-on spec should have a much friendlier way of requesting things: access resources in following REST-like format: 16 | 17 | ``` 18 | /{resource}/{type}/{id}.json 19 | ``` 20 | 21 | Or alternatively 22 | 23 | ``` 24 | /{resource}/{type}/{id}/{extraArgs}.json 25 | ``` 26 | 27 | Where extraArgs is a querystring-encoded object 28 | 29 | For example: ```/stream/series/tt14.json``` 30 | 31 | Transports allowed should be HTTP and IPFS 32 | 33 | In the case of IPFS in case the object does not exist, a `requestUpdate` message will be sent to the add-on creator peer (referenced by ID/pubkey via `peerRouting.findPeer`), and other delegated nodes (which might forward to the creator peer) - and we will wait a certain time to see if the content will be updated by the add-on publishing an updated version of itself. It's possible that a message of a new `addonDescriptorSigned` (see Addon Discovery) will be broadcasted back through the delegated nodes, telling the client to grab the content from this newer version. 34 | 35 | This message would also be sent if we consider the object outdated. 36 | 37 | 38 | 39 | ## Design goals 40 | 41 | * The NodeJS-based SDK to make add-ons should have a simple API, and be able to publish an add-on *without needing a server/hosting infrastructure* 42 | 43 | * The protocol should be simple enough to implement a hosted add-on with a basic HTTP library in any other language (Python, C#, etc.) 44 | 45 | * The protocol should be simple enough to allow 'static' add-ons: directories of files, uploaded to GitHub pages or IPFS 46 | 47 | * Simplify the entire client-side stack (stremio-addons, stremio-addons-user) 48 | 49 | 50 | ## Publishing 51 | 52 | `publish` mode: e.g. `./myAddon --publish`; this would start an IPFS node, upload the initial files (manifest and possibly catalogs) and then respond to all stream/details requests later on (`requestUpdate`), and upload the response in IPFS 53 | 54 | You have to keep this running in order for your add-on to update it's own content 55 | 56 | Additionally this mode will publish the result of stream/detail for the most popular pieces of content, without them being requested by peers first. 57 | 58 | Alternatively this would be able to publish to a directory, but of course in that mode you'd be limited to what is initially published rather than receiving messages to request what's missing 59 | 60 | 61 | ## Bootstrapping / dev friendliness 62 | 63 | * after one npm command, you should be able to initialize sample add-on 64 | * after starting the add-on, it should open in stremio or the open-source client; it's really important that opening the addon in a linter and a client is very easy 65 | * basically distinct starting a local addon server in two modes: ``--publish`` and ``--dev`` (default); where the ``--dev`` mode would prompt an existing stremio to open WITH the addon or warn the dev when there is no open stremio and provide `app.strem.io` fallback 66 | * detailed logging that shows what's happening and what's being asked from stremio 67 | * stremio-addons renamed to stremio-addons-sdk 68 | 69 | ## Cache 70 | 71 | Every response should have a certain caching policy. 72 | 73 | We should return the policy in the JSON response itself, so as to make it transport agnostic. 74 | In the case of IPFS, we can implement an "always available" policy where even if something is expired, it would be served from an old version if a new response is found in N seconds. 75 | 76 | For HTTP, the cache policy may be return in the form of HTTP headers as well. 77 | 78 | ## Add-on discovery 79 | 80 | Peer to peer add-on discovery can be implemented via IPFS, using the routing (DHT). You would publish an add-on to the DHT, by publishing it's addonDescriptorSigned, which is a signed message of hash({ transportUrl, transportName, manifest }) (AddonDescriptorHash) 81 | 82 | ## User identification / authentication 83 | 84 | HTTP-based transport may support user identification and possibly authentication. 85 | 86 | Simplest form of that is just to send a unique, anonymous UID as an HTTP header when fetching content. 87 | 88 | This can be achieved by an universal API in `stremio-addons-client` to set header for every request (for example `X-User-ID`) 89 | 90 | ## Bridge from BitTorrent/HTTP 91 | 92 | Consider an easy way of allowing files to be replicated over IPFS from sources like HTTP and BitTorrent *dynamically*. Dynamically means we wouldn't need the full file at once in order to upload it to IPFS. 93 | 94 | This can be done by gradually uploading chunks from the underlying source (HTTP or BitTorrent), when we have them, as IPFS blocks. Those blocks will also be IPFS objects, which means they follow markedag protobufs spec, and are of the `blob` type, meaning they only contain data (rather than references). 95 | 96 | The IPFS object hashes will be broadcasted on an IPFS pubsub channel, along with some metadata on what range they represent, as a signed message from the relayer. 97 | 98 | Once the whole underlying file has been retrieved, we can create an IPFS object that is a `list`, which means it will reference all the other IPFS objects of the `blob` to make one complete file. 99 | 100 | Once this is done, it will broadcast the final IPFS object. 101 | 102 | This can be used to aid and magically p2p-ify video distribution from HTTP to IPFS. 103 | 104 | 105 | ## Supernodes (delegated nodes) 106 | 107 | To aid using the decentralized system in resource limited environments and the browser, we can introduce a concept of a "supernode". Delegated nodes (supernodes) would be *all add-on creator nodes*, and some pre-set nodes (similar to DHT bootstrap nodes and [IPFS bootstrappers](https://github.com/libp2p/js-libp2p/blob/b871bb0a1ab400d76aa6808ed26fc905f64bc515/examples/libp2p-in-the-browser/1/src/browser-bundle.js#L13)). 108 | 109 | We don't really need to 'discover' the delegated nodes: we will have a hardcoded list of multiaddrs (see [IPFS browser config](https://github.com/ipfs/js-ipfs/blob/master/src/core/runtime/config-browser.json)) and the stremio-addon-sdk would include node ID and WebSocket multiaddr address(es) in the manifest by default. Also, WebRTC peers do not need to be discovered, instead they can be found directly with a multiaddr derived from the ID via the webrtc-star signalling mechanism - see https://github.com/libp2p/js-libp2p/tree/master/examples/libp2p-in-the-browser. Furthermore, those can be derived by taking all add-ons the central `addoncollection.json` knows about and seeing if they have IPFS nodes (all `transportUrl`'s that are to IPFS). 110 | 111 | Such a node could be doing delegated routing (see https://github.com/ipfs/notes/issues/162), relaying `requestUpdate` messages, and caching content so as to make it more available (and over more transports). 112 | 113 | Those nodes should expose WebSockets and WebRTC transports - both are compatible with the browser and have complementing strengths (e.g. WS is less resource intense and can go behind CloudFlare) 114 | 115 | The user would send `requestUpdate` message to all their delegated nodes, and use them for resolving names. This improves performance, and also ensures all add-ons receive the `requestUpdate` message. 116 | 117 | The add-on SDK can ensure add-on creators run supernodes by either shipping the go binaries for ipfs or by using `js-ipfs`. The go version might be needed because we need full DHT support. -------------------------------------------------------------------------------- /client.js: -------------------------------------------------------------------------------- 1 | var emitter = require("events").EventEmitter; 2 | var extend = require("extend"); 3 | 4 | var MAX_RETRIES = 4; 5 | var SERVICE_RETRY_TIMEOUT = 30*1000; 6 | 7 | function bindDefaults(call) { 8 | return { 9 | meta: { 10 | get: call.bind(null, "meta.get"), 11 | find: call.bind(null, "meta.find"), 12 | search: call.bind(null, "meta.search") 13 | }, 14 | index: { 15 | get: call.bind(null, "index.get") 16 | }, 17 | stream: { 18 | find: call.bind(null, "stream.find") 19 | }, 20 | subtitles: { 21 | get: call.bind(null, "subtitles.get"), 22 | find: call.bind(null, "subtitles.find") 23 | } 24 | } 25 | }; 26 | 27 | // Check priority based on arguments - e.g. on type and idProperty 28 | function checkArgs(args, manifest) 29 | { 30 | var score = 0; 31 | if (! args.query) return score; 32 | if ((manifest.types || []).indexOf(args.query.type)!=-1) score++; 33 | if (!manifest.idProperty && manifest.filter) Object.keys(manifest.filter).some(function(k) { 34 | if (k.match("_id$")) return manifest.idProperty = k.split(".").pop() 35 | }); 36 | (Array.isArray(manifest.idProperty) ? manifest.idProperty : [manifest.idProperty]).forEach(function(prop) { if (args.query.hasOwnProperty(prop)) score++ }); 37 | if (args.query.id && args.query.id.toString().indexOf(manifest.idProperty) === 0) score++; 38 | return score; 39 | }; 40 | 41 | 42 | function Addon(url, options, stremio, ready) 43 | { 44 | var self = this; 45 | 46 | var client = options.client || Stremio.RPC; 47 | 48 | if (typeof(url) == "string") { 49 | this.client = client(url, { 50 | timeout: options.timeout || stremio.options.timeout || 10000, 51 | respTimeout: options.respTimeout || stremio.options.respTimeout //|| 10000, 52 | }, stremio.options); 53 | this.url = url; 54 | } else { 55 | // Locally required add-on, emulate .client 56 | this.client = { request: function(method, args, cb) { 57 | url.request(method, args, function(err, res) { cb(null, err, res) }) 58 | } }; 59 | this.url = url.toString(); 60 | } 61 | 62 | if (ready) stremio.once("addon-meta:"+self.url, function() { ready(null, self) }); 63 | 64 | this.priority = options.priority || 0; 65 | this.initialized = false; 66 | this.initializing = false; 67 | this.manifest = { }; 68 | this.methods = []; 69 | this.retries = 0; 70 | this.idx = stremio.count++; 71 | 72 | initialize(); 73 | 74 | function initialize() { 75 | self.initializing = true; 76 | self.client.request("meta", [], function(err, error, res) { 77 | self.initializing = false; 78 | self.networkErr = err; 79 | if (err) { stremio.emit("network-error", err, self, self.url) } // network error. just ignore 80 | 81 | // Re-try if the add-on responds with error on meta; this is usually due to a temporarily failing add-on 82 | if (error) { 83 | console.error(error); 84 | if (self.retries++ < MAX_RETRIES) setTimeout(function() { self.initialized = false }, SERVICE_RETRY_TIMEOUT); 85 | } // service error. mark initialized, can re-try after 30 sec 86 | self.initialized = true; 87 | self.retries = 0; // return retries back to 0 88 | if (res && res.methods) self.methods = [].concat(res.methods); 89 | if (res && res.manifest) self.manifest = res.manifest; 90 | if (res) stremio.emit("addon-ready", self, self.url); 91 | stremio.emit("addon-meta:"+self.url, self, err, res); 92 | stremio.emit("addon-meta", self.url, self, err, res); 93 | }); 94 | } 95 | 96 | this.call = function(method, args, cb) 97 | { 98 | // wait for initialization 99 | if (! self.initialized) { 100 | if (! self.initializing) initialize(); 101 | stremio.once("addon-meta:"+self.url, function() { self.call(method, args, cb) }); 102 | return; 103 | } 104 | 105 | // Validate arguments - we should do this via some sort of model system 106 | if (self.methods.indexOf(method) == -1) return cb(1); 107 | self.client.request(method, args, function(err, error, res) { 108 | cb(0, err, error, res) 109 | }); 110 | }; 111 | 112 | this.identifier = function() { 113 | return (self.manifest && self.manifest.id) || self.url 114 | }; 115 | 116 | this.isInitializing = function() { 117 | return !this.initialized && !q.idle(); 118 | }; 119 | 120 | this.reinitialize = function() { 121 | //this.initialized = false; 122 | this.retries = 0; 123 | initialize(); 124 | }; 125 | }; 126 | 127 | function Stremio(options) 128 | { 129 | var self = this; 130 | emitter.call(this); 131 | 132 | //self.setMaxListeners(200); // something reasonable 133 | 134 | Object.defineProperty(self, "supportedTypes", { enumerable: true, get: function() { 135 | var types = {}; 136 | self.get("meta.find").forEach(function(service) { 137 | if (service.manifest.types) service.manifest.types.forEach(function(t) { types[t] = true }); 138 | }); 139 | return types; 140 | } }); 141 | 142 | options = self.options = options || {}; 143 | 144 | var services = { }; 145 | 146 | // counter 147 | this.count = 0; 148 | 149 | // Adding services 150 | // add(string url, object opts, function) 151 | // add(string url, function callback) 152 | this.add = function(url, opts, cb) { 153 | if (typeof(opts) === "function") { cb = opts; opts = { } }; 154 | cb = (typeof(cb) === "function") ? cb : function() { }; 155 | if (services[url]) { 156 | cb(null, services[url]); 157 | return services[url]; 158 | } 159 | services[url] = new Addon(url, extend({}, options, opts || {}), self, cb); 160 | return services[url]; 161 | }; 162 | 163 | // Removing 164 | this.remove = function(url) { 165 | delete services[url]; 166 | }; 167 | this.removeAll = function() { 168 | services = { }; 169 | }; 170 | 171 | // Listing 172 | this.get = function(forMethod, forArgs, noPicker) { 173 | var res = Object.keys(services).map(function(k) { return services[k] }); 174 | if (forMethod) res = res.filter(function(x) { return x.initialized ? x.methods.indexOf(forMethod) != -1 : true }); // if it's not initialized, assume it supports the method 175 | if (forMethod && !noPicker) res = picker(res, forMethod); // apply the picker for a method 176 | 177 | var cmp = function(a, b, fn) { return fn(a) - fn(b) }; 178 | return res.sort(function(a, b) { 179 | return cmp(b, a, function(x) { return x.initialized && !x.networkErr }) // sort by whether it's usable 180 | || cmp(b, a, function(x) { return forArgs ? checkArgs(forArgs, x.manifest) : 0 }) // sort by relevance to arguments 181 | || cmp(b, a, function(x) { return x.priority }) // compare prio // sort by priority 182 | || a.idx - b.idx // index in the entire array 183 | }); 184 | }; 185 | 186 | function fallthrough(s, method, args, cb) { 187 | var networkErr; // save last network error to return it potentially 188 | 189 | function next() { 190 | var service = s.shift(); 191 | if (! service) return cb(networkErr || new Error("no addon supplies this method / arguments")); 192 | 193 | service.call(method, [null, args], function(skip, err, error, res) { 194 | networkErr = err; 195 | // err, error are respectively HTTP error / JSON-RPC error; we need to implement fallback based on that (do a skip) 196 | if (skip || err) return next(); // Go to the next service 197 | 198 | cb(error, res, service); 199 | }); 200 | }; 201 | next(); 202 | }; 203 | 204 | function call(method, args, cb) { 205 | return fallthrough(self.get(method, args), method, args, cb); 206 | }; 207 | 208 | function picker(s, method) { 209 | var params = { addons: s, method: method }; 210 | if (options.picker) params.addons = options.picker(params.addons, params.method); 211 | self.emit("pick", params); 212 | return [].concat(params.addons); 213 | } 214 | 215 | 216 | this.fallthrough = fallthrough; 217 | this.call = call; 218 | this.checkArgs = checkArgs; 219 | extend(this, bindDefaults(call)); 220 | 221 | }; 222 | 223 | // Inherit the emitter 224 | Stremio.super_ = emitter; 225 | Stremio.prototype = new emitter(); 226 | Stremio.prototype.constructor = Stremio; 227 | 228 | module.exports = Stremio; 229 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | var url = require("url"); 2 | var rpc = require("./rpc"); 3 | var validate = require("./validate"); // simply console-log warnings in case of wrong args; aimed to aid development 4 | var extend = require("extend"); 5 | var async = require("async"); 6 | 7 | var template; 8 | 9 | var SESSION_LIVE = 10*60*60*1000; // 10 hrs 10 | var CACHE_TTL = 2.5 * 60 * 60; // seconds to live for the cache 11 | 12 | var CENTRAL = "https://api9.strem.io"; 13 | 14 | var IS_DEVEL = process.env.NODE_ENV !== "production"; 15 | 16 | function Server(methods, options, manifest) 17 | { 18 | var self = this; 19 | 20 | if (options && typeof(manifest) === "undefined") { 21 | manifest = options; 22 | options = null; 23 | } 24 | 25 | options = extend({ 26 | allow: [ CENTRAL ], // default stremio central 27 | secret: "8417fe936f0374fbd16a699668e8f3c4aa405d9f" // default secret for testing add-ons 28 | }, options || { }); 29 | 30 | this.methods = methods; 31 | this.manifest = manifest; 32 | this.options = options; 33 | 34 | Object.keys(methods).forEach(function(key) { 35 | if (typeof(methods[key]) != "function") throw Error(key+" should be a function"); 36 | }); 37 | 38 | // Announce to central 39 | self.announced = false; 40 | function announce() { 41 | self.announced = true; 42 | var body = JSON.stringify({ id: manifest.id, manifest: manifest }); 43 | var parsed = url.parse(CENTRAL+"/stremio/announce/"+options.secret); 44 | var req = (parsed.protocol.match("https") ? require("https") : require("http")).request(extend(parsed, { 45 | method: "POST", headers: { "Content-Type": "application/json", "Content-Length": body.length } 46 | }), function(res) { if (res.statusCode !== 200) console.error("Announce error for "+manifest.id+", statusCode: "+res.statusCode); }); 47 | req.on("error", function(err) { console.error("Announce error for "+manifest.id, err) }); 48 | req.end(body); 49 | } 50 | 51 | // Introspect the addon 52 | function meta(cb) { 53 | cb(null, { 54 | methods: Object.keys(methods), 55 | manifest: extend({ methods: Object.keys(methods) }, manifest || {}) 56 | }); 57 | }; 58 | 59 | // In case we use this in place of endpoint URL 60 | this.toString = function() { 61 | return self.manifest.id; 62 | }; 63 | 64 | // Direct interface 65 | this.request = function(method, params, cb) { 66 | if (method == "meta") return meta(cb); 67 | if (! methods[method]) return cb({ message: "method not supported", code: -32601 }, null); 68 | 69 | var auth = params[0], // AUTH is obsolete 70 | args = params[1] || { }; 71 | 72 | return methods[method](args, function(err, res) { 73 | if (err) return cb(err); 74 | if (IS_DEVEL) validate(method, res); // This would simply console-log warnings in case of wrong args; aimed to aid development 75 | cb(null, res); 76 | }, { stremioget: true }); // everything is allowed without auth in stremioget mode 77 | }; 78 | 79 | // HTTP middleware 80 | this.middleware = function(req, res, next) { 81 | if (!self.announced && !manifest.dontAnnounce) announce(); 82 | 83 | var start = Date.now(), finished = false; 84 | req._statsNotes = []; 85 | var getInfo = function() { return [req.url].concat(req._statsNotes).filter(function(x) { return x }) }; 86 | if (process.env.STREMIO_LOGGING) { 87 | res.on("finish", function() { 88 | finished = true; 89 | console.log("\x1b[34m["+(new Date()).toISOString()+"]\x1b[0m -> \x1b[32m["+(Date.now()-start)+"ms]\x1b[0m "+getInfo().join(", ")+" / "+res.statusCode) 90 | }); 91 | setTimeout(function() { if (!finished) console.log("-> \x1b[31m[WARNING]\x1b[0m "+getInfo().join(", ")+" taking more than 3000ms to run") }, 3000); 92 | } 93 | 94 | var parsed = url.parse(req.url); 95 | 96 | req._statsNotes.push(req.method); // HTTP method 97 | 98 | if (req.method === "OPTIONS") { 99 | var headers = {}; 100 | headers["Access-Control-Allow-Origin"] = "*"; 101 | headers["Access-Control-Allow-Methods"] = "POST, GET, PUT, DELETE, OPTIONS"; 102 | headers["Access-Control-Allow-Credentials"] = false; 103 | headers["Access-Control-Max-Age"] = "86400"; // 24 hours 104 | headers["Access-Control-Allow-Headers"] = "X-Requested-With, X-HTTP-Method-Override, Content-Type, Accept"; 105 | res.writeHead(200, headers); 106 | res.end(); 107 | return; 108 | }; 109 | 110 | if (req.method == "POST" || ( req.method == "GET" && parsed.pathname.match("q.json$") ) ) return serveRPC(req, res, function(method, params, cb) { 111 | req._statsNotes.push(method); // stremio method 112 | self.request(method, params, cb); 113 | }); else if (req.method == "GET") { // unsupported by JSON-RPC, it uses post 114 | return landingPage(req, res); 115 | } 116 | 117 | res.writeHead(405); // method not allowed 118 | res.end(); 119 | }; 120 | 121 | function serveRPC(req, res, handle) { 122 | var isGet = req.url.match("q.json"); 123 | var isJson = req.headers["content-type"] && req.headers["content-type"].match("^application/json"); 124 | if (!(isGet || isJson)) return res.writeHead(415); // unsupported media type 125 | res.setHeader("Access-Control-Allow-Origin", "*"); 126 | 127 | function formatResp(id, err, body) { 128 | var respBody = { jsonrpc: "2.0", id: id }; 129 | if (err) respBody.error = { message: err.message, code: err.code || -32603 }; 130 | else respBody.result = body; 131 | return respBody; 132 | }; 133 | function send(respBody, ttl) { 134 | respBody = JSON.stringify(respBody); 135 | res.setHeader("Content-Type", "application/json"); 136 | res.setHeader("Content-Length", Buffer.byteLength(respBody, "utf8")); 137 | if (! (req.headers.host && req.headers.host.match(/localhost|127.0.0.1/))) { 138 | res.setHeader("Cache-Control", "public, max-age="+(ttl || CACHE_TTL) ); // around 2 hours default 139 | } 140 | res.end(respBody); 141 | }; 142 | 143 | rpc.receiveJSON(req, function(err, body) { 144 | if (err) return send({ code: -32700, message: "parse error" }); // TODO: jsonrpc, id prop 145 | 146 | var ttl = CACHE_TTL; 147 | if (!isNaN(options.cacheTTL)) ttl = options.cacheTTL; 148 | if (options.cacheTTL && options.cacheTTL[body.method]) ttl = options.cacheTTL[body.method]; 149 | 150 | if (Array.isArray(body)) { 151 | async.map(body, function(b, cb) { 152 | // WARNING: same logic as --> 153 | if (!b || !b.id || !b.method) return cb(null, formatResp(null, { code: -32700, message: "parse error" })); 154 | handle(b.method, b.params, function(err, bb) { 155 | cb(null, formatResp(b.id, err, bb)) 156 | }); 157 | }, function(err, bodies) { send(bodies, ttl) }); 158 | } else { 159 | // --> THIS 160 | if (!body || !body.id || !body.method) return send(formatResp(null, { code: -32700, message: "parse error" })); 161 | handle(body.method, body.params, function(err, b) { 162 | send(formatResp(body.id, err, b), ttl) 163 | }); 164 | } 165 | }); 166 | }; 167 | 168 | function landingPage(req, res) { 169 | var endpoint = manifest.endpoint || "http://"+req.headers.host+req.url; 170 | var stats = { }, top = []; 171 | 172 | // TODO: cache at least stats.get for some time 173 | if (! self.methods['stats.get']) return respond(); 174 | 175 | self.request("stats.get", [{ stremioget: true }], function(err, s) { 176 | if (err) console.log(err); 177 | if (s) stats = s; 178 | if (! self.methods['meta.find']) return respond(); 179 | self.request("meta.find", [{stremioget: true}, { query: {}, limit: 10 }], function(err, t) { 180 | if (err) return error(err); 181 | if (t) top = t; 182 | respond(); 183 | }); 184 | }); 185 | 186 | function error(e) { 187 | console.error("LANDING PAGE ERROR",e); 188 | res.writeHead(500); res.end(); 189 | } 190 | 191 | function respond() { 192 | try { 193 | if (! template) template = require("ejs").compile(require("fs").readFileSync(__dirname+"/addon-template.ejs").toString(), { }); 194 | var body = template({ 195 | addon: { manifest: manifest, methods: methods }, 196 | endpoint: endpoint, 197 | stats: stats, top: top 198 | }); 199 | res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" }); 200 | res.end(body); 201 | } catch(e) { error(e) } 202 | } 203 | } 204 | }; 205 | 206 | module.exports = Server; 207 | -------------------------------------------------------------------------------- /sip/v3.md: -------------------------------------------------------------------------------- 1 | ## stremio-addons v3 2 | 3 | ## WIP DOCUMENT used during research for designing the new spec and the P2P extension 4 | 5 | ## DESIGN DOC 6 | ## NOT A SPEC 7 | 8 | ### Preliminary notes 9 | 10 | Some things in the current iteration of stremio-addons are obsolete, such as: 11 | 12 | * `.add`-ing addon server objects directly 13 | * `validate` 14 | 15 | 16 | Other desing considerations: 17 | 18 | * must be very transport agnostic, in order to support HTTP/HTTPS, mitigate mixed content policy, use `fetch`; additionally it should be compatible with IPFS 19 | * eliminate selection policy (and priorities), and instead shift this responsibility to the client 20 | * consider if `fallthrough` can be eliminated and the significance of `checkArgs` reduced to mere type/idPrefix filter 21 | * SHOULD WE and HOW should we do custom functions / messages; considering the entirely declarative nature of addons, custom methods would only be something that stremio clients themselves have to decide on providing. The best decision seems to be to keep this out of the spec, and stick with the declarative nature of the protocol 22 | * Custom RESOURCES will be allowed 23 | * The Discover and Board mechanics can be unified on a models level with a `Metadata.getAllCatalogs()` that just aggregaates all catalogs from all add-ons 24 | * Consider moving the Search to an in-house stremio service, maybe ElasticSearch; that would simplify the role of Cinemeta; DISCARDED because we still have add-ons like YouTube where you ABSOLUTELY have to rely on their own search 25 | 26 | 27 | ### Analysis 28 | 29 | Let us describe a v3 add-on as having: 30 | 31 | 1. A manifest 32 | 2. One or more metadata catalogs (listed in the manifest) 33 | 3. Supported type(s) and supported idPrefix(es) - used to determine whether to perform a /stream/ or /meta/ 34 | 35 | Furthermore `stremboard` can be replaced by something like `Metadata.getAllCatalogs()` which gets / keeps track of all catalogs collected. 36 | 37 | Start: 38 | 39 | ``` 40 | src/dialogs/localmedia/localmedia.js: stremio.fallthrough([addon], "meta.find", { query: { } }, function(err, result) { 41 | src/pages/discover/discover.js: stremio.fallthrough([].concat(addons), "meta.find", q, receiveRes); 42 | src/pages/search/search.js: if (imdbMatch) return stremio.meta.get({ query: { imdb_id: imdbMatch[0] } }, function(err, resp) { 43 | node_modules/stremio-models/metadata.js: stremio.meta.get({ query: query }, function(err, resp) { 44 | node_modules/stremio-models/metadata.js: stremio.meta.find({ query: q, projection: "lean", limit: idsRetr.length }, function(err, res) { 45 | node_modules/stremio-models/metadata.js: stremio.meta.find({ 46 | node_modules/stremio-models/stream.js: stremio.fallthrough(group, "stream.find", args, count(function(err, resp, addon) { 47 | node_modules/stremio-models/subtitles.js: stremio.fallthrough(getSubsAddons("subtitles.hash"), "subtitles.hash", { url: url }, cb); 48 | node_modules/stremio-models/subtitles.js: stremio.fallthrough(getSubsAddons("subtitles.find"), "subtitles.find", args, function(err, resp) { 49 | node_modules/stremio-models/subtitles.js: stremio.fallthrough(getSubsAddons("subtitles.tracks"), "subtitles.tracks", { url: url }, function(err, tracks) { 50 | node_modules/stremboard/index.js: else stremio.meta.find({ query: notif.getMetaQuery() }, function(err, res) { 51 | node_modules/stremboard/index.js: stremio.meta.find({ 52 | node_modules/stremboard/index.js: stremio.fallthrough([addon], "meta.find", args, function(err, res) { 53 | ``` 54 | 55 | With comments: 56 | 57 | ``` 58 | 59 | // getting a catalog from local media (local add-on can just have one catalog defined) - can just use `Metadata.getAllCatalogs()` 60 | src/dialogs/localmedia/localmedia.js: stremio.fallthrough([addon], "meta.find", { query: { } }, function(err, result) { 61 | 62 | // should use `Metadata.getAllCatalogs()` 63 | src/pages/discover/discover.js: stremio.fallthrough([].concat(addons), "meta.find", q, receiveRes); 64 | 65 | // individual meta get - should use /meta/{type}/{id} 66 | src/pages/search/search.js: if (imdbMatch) return stremio.meta.get({ query: { imdb_id: imdbMatch[0] } }, function(err, resp) { 67 | 68 | // individual meta get - should use /meta/{type}/{id} 69 | node_modules/stremio-models/metadata.js: stremio.meta.get({ query: query }, function(err, resp) { 70 | 71 | // retrieveManyById - TRICKY 72 | // should be dropped in discover; it's only used for featured ATM; this should be migrated to the catalogs spec 73 | node_modules/stremio-models/metadata.js: stremio.meta.find({ query: q, projection: "lean", limit: idsRetr.length }, function(err, res) { 74 | 75 | // gets similar - TRICKY 76 | // proposal: we should just make /meta/{type}/{id} return details about similar, and this would be DROPPED 77 | node_modules/stremio-models/metadata.js: stremio.meta.find({ 78 | 79 | // should use /stream/{type}/{id} 80 | node_modules/stremio-models/stream.js: stremio.fallthrough(group, "stream.find", args, count(function(err, resp, addon) { 81 | 82 | // will NOT use add-on: localServer/subHash?url=... 83 | node_modules/stremio-models/subtitles.js: stremio.fallthrough(getSubsAddons("subtitles.hash"), "subtitles.hash", { url: url }, cb); 84 | 85 | // should use /subtitles/{type}/{id} 86 | node_modules/stremio-models/subtitles.js: stremio.fallthrough(getSubsAddons("subtitles.find"), "subtitles.find", args, function(err, resp) 87 | 88 | // will NOT use add-on: localServer/subtitles.json?url=... 89 | node_modules/stremio-models/subtitles.js: stremio.fallthrough(getSubsAddons("subtitles.tracks"), "subtitles.tracks", { url: url }, function(err, tracks) { 90 | 91 | // gets details for a notifItem w/o meta: should be OBSOLETE 92 | node_modules/stremboard/index.js: else stremio.meta.find({ query: notif.getMetaQuery() }, function(err, res) { 93 | 94 | // getting recommendations - TRICKY 95 | // SOLUTION: cinemeta add-on should allow referencing movies like /movie/{type}/wiki:{wikiURL} 96 | node_modules/stremboard/index.js: stremio.meta.find({ 97 | 98 | // should use `Metadata.getAllCatalogs()` 99 | node_modules/stremboard/index.js: stremio.fallthrough([addon], "meta.find", args, function(err, res) { 100 | 101 | 102 | also name-to-imdb 103 | also stremio-server 104 | 105 | ``` 106 | 107 | 108 | ### Add-on 109 | 110 | ``` 111 | const addon = new Addon(manifest) 112 | 113 | addon.defineCatalogHandler(function(type, id, cb) { 114 | cb(null, [{ 115 | .... 116 | }]) 117 | }) 118 | 119 | addon.defineMetaHandler(function(type, id, cb) { 120 | 121 | }) 122 | 123 | addon.defineStreamHandler(function(type, id, cb) { 124 | 125 | }) 126 | 127 | // alternative 128 | addon.defineCustomResource('resource', function(type, id, cb) { 129 | 130 | }) 131 | 132 | // --publish flag would immediately publish /manifest.json and all catalogs defined in the manifest (or only `/catalog/movie/top.json`) 133 | 134 | ``` 135 | 136 | 137 | ### Tester tool / UI 138 | 139 | stremio-open-ui: bootstrap + angular based UI 140 | stremio-addon-lint: ... 141 | 142 | 143 | 144 | ### ElasticSearch test 145 | 146 | ``` 147 | docker run -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" -e http.cors.allow-origin="http://localhost:3030" -e http.cors.enabled=true -e http.cors.allow-headers=X-Requested-With,X-Auth-Token,Content-Type,Content-Length,Authorization -e http.cors.allow-credentials=true -d docker.elastic.co/elasticsearch/elasticsearch:6.2.2 148 | ``` 149 | 150 | ``` 151 | docker run -p 1358:1358 -d appbaseio/dejavu 152 | ``` 153 | 154 | 155 | ### Design decisions 156 | 157 | * use `node-fetch` because it has a nice browser fallback to browser natives 158 | * HTTP addons should use `X-Stremio-Addon` header on landing pages to point to the `manifest.json` 159 | * To solve the legacy exception with IMDB IDs (starting with `"tt"`), we just consider `"tt"` a valid ID prefix and no longer require ID prefixes to be split with a colon (`":"`) - this means that new add-ons will have to handle IMDB IDs directly, and to signal you support it you just have to provide `idPrefixes: ["tt"]` in your manifest 160 | 161 | 162 | ## NAT traversal/hole punching 163 | 164 | Useful information about that: 165 | 166 | https://stackoverflow.com/questions/38786438/libutp-%C2%B5tp-and-nat-traversal-udp-hole-punching?utm_medium=organic&utm_source=google_rich_qa&utm_campaign=google_rich_qa 167 | 168 | https://github.com/arvidn/libtorrent/blob/c1ade2b75f8f7771509a19d427954c8c851c4931/src/bt_peer_connection.cpp#L1421 169 | 170 | https://github.com/mafintosh/utp-native 171 | 172 | https://github.com/mafintosh/hyperdht#dhtholepunchpeer-node-callback 173 | 174 | https://webrtchacks.com/an-intro-to-webrtcs-natfirewall-problem/ 175 | 176 | https://www.html5rocks.com/en/tutorials/webrtc/infrastructure/ 177 | 178 | https://github.com/nicojanssens/1tp 179 | 180 | https://github.com/libp2p/js-libp2p-webrtc-star 181 | 182 | https://github.com/libp2p/js-libp2p-webrtc-direct 183 | 184 | https://github.com/libp2p/js-libp2p/tree/master/examples/discovery-mechanisms 185 | 186 | https://webrtc.github.io/samples/src/content/peerconnection/trickle-ice/ 187 | 188 | ## IPNS 189 | 190 | Currently, `js-ipfs` does not provide an IPNS implementation. Implementing it seems straightforward, see: 191 | 192 | https://github.com/ipfs/go-ipfs/blob/master/namesys/routing.go#L114 193 | 194 | https://github.com/ipfs/go-ipfs/blob/master/namesys/routing.go#L141 195 | 196 | https://github.com/ipfs/go-ipfs/blob/master/namesys/routing.go#L157 197 | 198 | We are relying on routing though, but this should be easy with supernodes / delegated nodes 199 | 200 | IPNS would NOT be needed if we just use our own update mechanism, where: an `addonDescriptor` is an object `{transportUrl, transportName, manifest}`, an `addonDescriptorSigned` is a signed hash of that (with a seq number), and addons are published via submitting the `addonDescriptorSigned` to the central API and/or a DHT. Then, the latest `addonDescriptorSigned` for an ID (`manifest.id`) is the latest version of that add-on. -------------------------------------------------------------------------------- /browser/stremio-addons.min.js: -------------------------------------------------------------------------------- 1 | require=function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o0&&this._events[type].length>m){this._events[type].warned=true;console.error("(node) warning: possible EventEmitter memory "+"leak detected. %d listeners added. "+"Use emitter.setMaxListeners() to increase limit.",this._events[type].length);if(typeof console.trace==="function"){console.trace()}}}return this};EventEmitter.prototype.on=EventEmitter.prototype.addListener;EventEmitter.prototype.once=function(type,listener){if(!isFunction(listener))throw TypeError("listener must be a function");var fired=false;function g(){this.removeListener(type,g);if(!fired){fired=true;listener.apply(this,arguments)}}g.listener=listener;this.on(type,g);return this};EventEmitter.prototype.removeListener=function(type,listener){var list,position,length,i;if(!isFunction(listener))throw TypeError("listener must be a function");if(!this._events||!this._events[type])return this;list=this._events[type];length=list.length;position=-1;if(list===listener||isFunction(list.listener)&&list.listener===listener){delete this._events[type];if(this._events.removeListener)this.emit("removeListener",type,listener)}else if(isObject(list)){for(i=length;i-- >0;){if(list[i]===listener||list[i].listener&&list[i].listener===listener){position=i;break}}if(position<0)return this;if(list.length===1){list.length=0;delete this._events[type]}else{list.splice(position,1)}if(this._events.removeListener)this.emit("removeListener",type,listener)}return this};EventEmitter.prototype.removeAllListeners=function(type){var key,listeners;if(!this._events)return this;if(!this._events.removeListener){if(arguments.length===0)this._events={};else if(this._events[type])delete this._events[type];return this}if(arguments.length===0){for(key in this._events){if(key==="removeListener")continue;this.removeAllListeners(key)}this.removeAllListeners("removeListener");this._events={};return this}listeners=this._events[type];if(isFunction(listeners)){this.removeListener(type,listeners)}else if(listeners){while(listeners.length)this.removeListener(type,listeners[listeners.length-1])}delete this._events[type];return this};EventEmitter.prototype.listeners=function(type){var ret;if(!this._events||!this._events[type])ret=[];else if(isFunction(this._events[type]))ret=[this._events[type]];else ret=this._events[type].slice();return ret};EventEmitter.prototype.listenerCount=function(type){if(this._events){var evlistener=this._events[type];if(isFunction(evlistener))return 1;else if(evlistener)return evlistener.length}return 0};EventEmitter.listenerCount=function(emitter,type){return emitter.listenerCount(type)};function isFunction(arg){return typeof arg==="function"}function isNumber(arg){return typeof arg==="number"}function isObject(arg){return typeof arg==="object"&&arg!==null}function isUndefined(arg){return arg===void 0}},{}],3:[function(require,module,exports){"use strict";var hasOwn=Object.prototype.hasOwnProperty;var toStr=Object.prototype.toString;var isArray=function isArray(arr){if(typeof Array.isArray==="function"){return Array.isArray(arr)}return toStr.call(arr)==="[object Array]"};var isPlainObject=function isPlainObject(obj){if(!obj||toStr.call(obj)!=="[object Object]"){return false}var hasOwnConstructor=hasOwn.call(obj,"constructor");var hasIsPrototypeOf=obj.constructor&&obj.constructor.prototype&&hasOwn.call(obj.constructor.prototype,"isPrototypeOf");if(obj.constructor&&!hasOwnConstructor&&!hasIsPrototypeOf){return false}var key;for(key in obj){}return typeof key==="undefined"||hasOwn.call(obj,key)};module.exports=function extend(){var options,name,src,copy,copyIsArray,clone;var target=arguments[0];var i=1;var length=arguments.length;var deep=false;if(typeof target==="boolean"){deep=target;target=arguments[1]||{};i=2}if(target==null||typeof target!=="object"&&typeof target!=="function"){target={}}for(;i 0 && this._events[type].length > m) { 368 | this._events[type].warned = true; 369 | console.error('(node) warning: possible EventEmitter memory ' + 370 | 'leak detected. %d listeners added. ' + 371 | 'Use emitter.setMaxListeners() to increase limit.', 372 | this._events[type].length); 373 | if (typeof console.trace === 'function') { 374 | // not supported in IE 10 375 | console.trace(); 376 | } 377 | } 378 | } 379 | 380 | return this; 381 | }; 382 | 383 | EventEmitter.prototype.on = EventEmitter.prototype.addListener; 384 | 385 | EventEmitter.prototype.once = function(type, listener) { 386 | if (!isFunction(listener)) 387 | throw TypeError('listener must be a function'); 388 | 389 | var fired = false; 390 | 391 | function g() { 392 | this.removeListener(type, g); 393 | 394 | if (!fired) { 395 | fired = true; 396 | listener.apply(this, arguments); 397 | } 398 | } 399 | 400 | g.listener = listener; 401 | this.on(type, g); 402 | 403 | return this; 404 | }; 405 | 406 | // emits a 'removeListener' event iff the listener was removed 407 | EventEmitter.prototype.removeListener = function(type, listener) { 408 | var list, position, length, i; 409 | 410 | if (!isFunction(listener)) 411 | throw TypeError('listener must be a function'); 412 | 413 | if (!this._events || !this._events[type]) 414 | return this; 415 | 416 | list = this._events[type]; 417 | length = list.length; 418 | position = -1; 419 | 420 | if (list === listener || 421 | (isFunction(list.listener) && list.listener === listener)) { 422 | delete this._events[type]; 423 | if (this._events.removeListener) 424 | this.emit('removeListener', type, listener); 425 | 426 | } else if (isObject(list)) { 427 | for (i = length; i-- > 0;) { 428 | if (list[i] === listener || 429 | (list[i].listener && list[i].listener === listener)) { 430 | position = i; 431 | break; 432 | } 433 | } 434 | 435 | if (position < 0) 436 | return this; 437 | 438 | if (list.length === 1) { 439 | list.length = 0; 440 | delete this._events[type]; 441 | } else { 442 | list.splice(position, 1); 443 | } 444 | 445 | if (this._events.removeListener) 446 | this.emit('removeListener', type, listener); 447 | } 448 | 449 | return this; 450 | }; 451 | 452 | EventEmitter.prototype.removeAllListeners = function(type) { 453 | var key, listeners; 454 | 455 | if (!this._events) 456 | return this; 457 | 458 | // not listening for removeListener, no need to emit 459 | if (!this._events.removeListener) { 460 | if (arguments.length === 0) 461 | this._events = {}; 462 | else if (this._events[type]) 463 | delete this._events[type]; 464 | return this; 465 | } 466 | 467 | // emit removeListener for all listeners on all events 468 | if (arguments.length === 0) { 469 | for (key in this._events) { 470 | if (key === 'removeListener') continue; 471 | this.removeAllListeners(key); 472 | } 473 | this.removeAllListeners('removeListener'); 474 | this._events = {}; 475 | return this; 476 | } 477 | 478 | listeners = this._events[type]; 479 | 480 | if (isFunction(listeners)) { 481 | this.removeListener(type, listeners); 482 | } else if (listeners) { 483 | // LIFO order 484 | while (listeners.length) 485 | this.removeListener(type, listeners[listeners.length - 1]); 486 | } 487 | delete this._events[type]; 488 | 489 | return this; 490 | }; 491 | 492 | EventEmitter.prototype.listeners = function(type) { 493 | var ret; 494 | if (!this._events || !this._events[type]) 495 | ret = []; 496 | else if (isFunction(this._events[type])) 497 | ret = [this._events[type]]; 498 | else 499 | ret = this._events[type].slice(); 500 | return ret; 501 | }; 502 | 503 | EventEmitter.prototype.listenerCount = function(type) { 504 | if (this._events) { 505 | var evlistener = this._events[type]; 506 | 507 | if (isFunction(evlistener)) 508 | return 1; 509 | else if (evlistener) 510 | return evlistener.length; 511 | } 512 | return 0; 513 | }; 514 | 515 | EventEmitter.listenerCount = function(emitter, type) { 516 | return emitter.listenerCount(type); 517 | }; 518 | 519 | function isFunction(arg) { 520 | return typeof arg === 'function'; 521 | } 522 | 523 | function isNumber(arg) { 524 | return typeof arg === 'number'; 525 | } 526 | 527 | function isObject(arg) { 528 | return typeof arg === 'object' && arg !== null; 529 | } 530 | 531 | function isUndefined(arg) { 532 | return arg === void 0; 533 | } 534 | 535 | },{}],3:[function(require,module,exports){ 536 | 'use strict'; 537 | 538 | var hasOwn = Object.prototype.hasOwnProperty; 539 | var toStr = Object.prototype.toString; 540 | 541 | var isArray = function isArray(arr) { 542 | if (typeof Array.isArray === 'function') { 543 | return Array.isArray(arr); 544 | } 545 | 546 | return toStr.call(arr) === '[object Array]'; 547 | }; 548 | 549 | var isPlainObject = function isPlainObject(obj) { 550 | if (!obj || toStr.call(obj) !== '[object Object]') { 551 | return false; 552 | } 553 | 554 | var hasOwnConstructor = hasOwn.call(obj, 'constructor'); 555 | var hasIsPrototypeOf = obj.constructor && obj.constructor.prototype && hasOwn.call(obj.constructor.prototype, 'isPrototypeOf'); 556 | // Not own constructor property must be Object 557 | if (obj.constructor && !hasOwnConstructor && !hasIsPrototypeOf) { 558 | return false; 559 | } 560 | 561 | // Own properties are enumerated firstly, so to speed up, 562 | // if last one is own, then all properties are own. 563 | var key; 564 | for (key in obj) { /**/ } 565 | 566 | return typeof key === 'undefined' || hasOwn.call(obj, key); 567 | }; 568 | 569 | module.exports = function extend() { 570 | var options, name, src, copy, copyIsArray, clone; 571 | var target = arguments[0]; 572 | var i = 1; 573 | var length = arguments.length; 574 | var deep = false; 575 | 576 | // Handle a deep copy situation 577 | if (typeof target === 'boolean') { 578 | deep = target; 579 | target = arguments[1] || {}; 580 | // skip the boolean and the target 581 | i = 2; 582 | } 583 | if (target == null || (typeof target !== 'object' && typeof target !== 'function')) { 584 | target = {}; 585 | } 586 | 587 | for (; i < length; ++i) { 588 | options = arguments[i]; 589 | // Only deal with non-null/undefined values 590 | if (options != null) { 591 | // Extend the base object 592 | for (name in options) { 593 | src = target[name]; 594 | copy = options[name]; 595 | 596 | // Prevent never-ending loop 597 | if (target !== copy) { 598 | // Recurse if we're merging plain objects or arrays 599 | if (deep && copy && (isPlainObject(copy) || (copyIsArray = isArray(copy)))) { 600 | if (copyIsArray) { 601 | copyIsArray = false; 602 | clone = src && isArray(src) ? src : []; 603 | } else { 604 | clone = src && isPlainObject(src) ? src : {}; 605 | } 606 | 607 | // Never move original objects, clone them 608 | target[name] = extend(deep, clone, copy); 609 | 610 | // Don't bring in undefined values 611 | } else if (typeof copy !== 'undefined') { 612 | target[name] = copy; 613 | } 614 | } 615 | } 616 | } 617 | } 618 | 619 | // Return the modified object 620 | return target; 621 | }; 622 | 623 | },{}],"stremio-addons":[function(require,module,exports){ 624 | module.CENTRAL = "http://api9.strem.io"; 625 | module.exports.Client = require("./client"); 626 | /* 627 | // Fetch-based client 628 | module.exports.Client.RPC = function(endpoint) { 629 | var self = { }; 630 | self.request = function(method, params, callback) { 631 | var body = JSON.stringify({ params: params, method: method, id: 1, jsonrpc: "2.0" }); 632 | 633 | var request = ((body.length < 8192) && endpoint.match("/stremioget")) ? 634 | window.fetch(endpoint+"/q.json?b="+btoa(body)) // GET 635 | : window.fetch(endpoint, { method: "POST", headers: { "Content-Type": "application/json" }, body: body }); // POST 636 | 637 | request.then(function(resp) { 638 | if (resp.status !== 200) return callback(new Error("response code "+resp.status)); 639 | if (resp.headers.get("content-type").indexOf("application/json") === -1) return callback(new Error("no application/json response")); 640 | 641 | resp.json().then(function(body) { 642 | setTimeout(function() { 643 | if (!body || body.error) return callback(null, (body && body.error) || new Error("empty body")); 644 | callback(null, null, body.result); 645 | }); 646 | }, callback).catch(callback); 647 | }).catch(callback); 648 | }; 649 | return self; 650 | }; 651 | */ 652 | 653 | // XMLHttpRequest-based client 654 | module.exports.Client.RPC = function (endpoint) { 655 | var self = { }; 656 | 657 | self.request = function(method, params, callback) { 658 | var body = JSON.stringify({ params: params, method: method, id: 1, jsonrpc: "2.0" }); 659 | 660 | var request = new XMLHttpRequest(); 661 | 662 | request.onreadystatechange = function() { 663 | if (request.readyState == XMLHttpRequest.DONE) { 664 | if (request.status == 200) { 665 | var res; 666 | try { 667 | res = JSON.parse(request.responseText); 668 | } catch(e) { callback(e) } 669 | 670 | callback(null, res.error, res.result); 671 | } else callback("network err "+request.status); 672 | } 673 | } 674 | 675 | request.open("GET", endpoint+"/q.json?b="+btoa(body), true); 676 | request.send(); 677 | }; 678 | return self; 679 | } 680 | 681 | },{"./client":1}]},{},[]); 682 | --------------------------------------------------------------------------------