stremio-addon-sdk

Advanced Usage

Understanding Catalogs

The catalog resource in Stremio addons can be used to:

Let’s first look at how catalog is declared in the manifest:

{
  "resources": ["catalog"],
  "catalogs": [
    {
      "id": "testcatalog",
      "type": "movie"      
    }
  ]
}

This is normally all you’d need to make a standard catalog, but it won’t support filtering, paginating and it won’t be searchable.

Searching in Catalogs

To state that your catalog supports searching, you’d need to set it in the extra property:

catalogs: [
  {
    "id": "testcatalog",
    "type": "movie",
    "extra": [
      {
        "name": "search",
        "isRequired": false
      }
    ]
  }
]

But then, what if you want your catalog to support only search (as in, not show in the Board or Discover pages at all)?

Then you’d need to state that your catalog supports only searching, and you can do that with:

catalogs: [
  {
    "id": "testcatalog",
    "type": "movie",
    "extra": [
      {
        "name": "search",
        "isRequired": true
      }
    ]
  }
]

Once you’ve set search in extra, your catalog handler will receive args.extra.search as the search query (if it is a search request), so here’s an example of a search response:

const meta = {
  id: 'tt1254207',
  name: 'Big Buck Bunny',
  releaseInfo: '2008',
  poster: 'https://image.tmdb.org/t/p/w600_and_h900_bestv2/uVEFQvFMMsg4e6yb03xOfVsDz4o.jpg',
  posterShape: 'poster',
  banner: 'https://image.tmdb.org/t/p/original/aHLST0g8sOE1ixCxRDgM35SKwwp.jpg',
  type: 'movie'
}
builder.defineCatalogHandler(function(args) {
  return new Promise(function(resolve, reject) {
    if (args.id == 'testcatalog') {
      // this is a request to our catalog id
      if (args.extra.search) {
        // this is a search request
        if (args.extra.search == 'big buck bunny') {
          // if someone searched for "big buck bunny" (exact match)
          // respond with our meta item
          resolve({ metas: [meta] })
        } else {
          reject(new Error('No search results found'))
        }
      } else {
        // this is a standard catalog request
        // just respond with our meta item
        resolve({ metas: [meta] })
      }
    } else {
      reject(new Error('Unknown catalog request'))
    }
  })
})

Filtering in Catalogs

Maybe you would like your catalog to be filtered by genre, in this case, we’ll set that in the catalog definition:

catalogs: [
  {
    "id": "testcatalog",
    "type": "movie",
    "extra": [
      {
        "name": "genre",
        "options": [ "Drama", "Action" ],
        "isRequired": false
      }
    ]
  }
]

Now we’ll receive genre in our catalog handler:

const meta = {
  id: 'tt1254207',
  name: 'Big Buck Bunny',
  releaseInfo: '2008',
  poster: 'https://image.tmdb.org/t/p/w600_and_h900_bestv2/uVEFQvFMMsg4e6yb03xOfVsDz4o.jpg',
  posterShape: 'poster',
  banner: 'https://image.tmdb.org/t/p/original/aHLST0g8sOE1ixCxRDgM35SKwwp.jpg',
  type: 'movie'
}
builder.defineCatalogHandler(function(args) {
  return new Promise(function(resolve, reject) {
    if (args.id == 'testcatalog') {
      // this is a request to our catalog id
      if (args.extra.genre) {
        // this is a filter request
        if (args.extra.genre == "Action") {
          // in this example we'll only respon with our
          // meta item if the genre is "Action"
          resolve({ metas: [meta] })
        } else {
          // otherwise with no meta item
          resolve({ metas: [] })
        }
      } else {
        // this is a standard catalog request
        // just respond with our meta item
        resolve({ metas: [meta] })
      }
    } else {
      reject(new Error('Unknown catalog request'))
    }
  })
})

Pagination in Catalogs

If we want our catalogs to be paginated, we can use skip as follows:

catalogs: [
  {
    "id": "testcatalog",
    "type": "movie",
    "extra": [
      {
        "name": "skip",
        "isRequired": false
      }
    ]
  }
]

Optionally, we can also set the steps in which the catalog will request the next items, for example:

catalogs: [
  {
    "id": "testcatalog",
    "type": "movie",
    "extra": [
      {
        "name": "skip",
        "options": ["0", "100", "200"],
        "isRequired": false
      }
    ]
  }
]

This is not a requirement though, as if we don’t set options Stremio will request skip once it comes to the end of your catalog.

Here’s an example of using skip:

// we only have one meta item
const meta = {
  id: 'tt1254207',
  name: 'Big Buck Bunny',
  releaseInfo: '2008',
  poster: 'https://image.tmdb.org/t/p/w600_and_h900_bestv2/uVEFQvFMMsg4e6yb03xOfVsDz4o.jpg',
  posterShape: 'poster',
  banner: 'https://image.tmdb.org/t/p/original/aHLST0g8sOE1ixCxRDgM35SKwwp.jpg',
  type: 'movie'
}

const metaList = []

// but we'll make an array that includes our meta 60 times
for (let i = 0; i++; i < 60) {
  metaList.push(meta)
}

builder.defineCatalogHandler(function(args) {
  return new Promise(function(resolve, reject) {
    if (args.id == 'testcatalog') {
      // we'll slice our meta list using
      // skip as the starting point
      const skip = args.extra.skip || 0
      resolve({ metas: metaList.slice(skip, skip + 20) })
    } else {
      reject(new Error('Unknown catalog request'))
    }
  })
})

Understanding Cinemeta

Cinemeta is the primary addon that Stremio uses to show Movie, Series and Anime items. Other addons can choose to create their own catalogs of items or respond with streams to the Cinemeta items.

Cinemeta uses IMDB IDs for their metadata, to understand it’s pattern:

Adding Stream Results to Cinemeta Items

To add only stream results to Cinemeta items, you will first need to state that your addons id prefix is tt (as for IMDB IDs).

Add these to your manifest:

Now here is an example of returning stream responses for Cinemeta items:

builder.defineStreamHandler(function(args) {
  return new Promise(function(resolve, reject) {
    if (args.type === 'movie' && args.id === 'tt1254207') {
      // serve one stream for big buck bunny
      const stream = { url: 'http://distribution.bbb3d.renderfarming.net/video/mp4/bbb_sunflower_1080p_30fps_normal.mp4' }
      resolve({ streams: [stream] })
    } else if (args.type === 'series' && args.id === 'tt8633518:1:1') {
      // return one stream for the series Weird City, Season 1 Episode 1
      // (free Youtube Originals series)
      const stream = { id: 'yt_id::fMnq5v8yZp4' }
      resolve({ streams: [stream] })
    } else {
      reject(new Error('No streams available for: ' + args.id))
    }
  })
})

Getting Metadata from Cinemeta

There might be cases where you would need metadata based on IMDB ID. To do this, you will need both IMDB ID and the type of the item (either movie or series).

Because Cinemeta is also an addon, you can request the metadata from it.

Here is an example using needle to do a HTTP request to Cinemeta for metadata:

var needle = require('needle')

// we will get metadata for the movie: Big Buck Bunny

var itemType = 'movie'
var itemImdbId = 'tt1254207'

needle.get('https://v3-cinemeta.strem.io/meta/' + itemType + '/' + itemImdbId + '.json', function(err, resp, body) {

  if (body && body.meta) {

    // log Big Buck Bunny's metadata
    console.log(body.meta)

  }

})

Resolving Movie / Series names to IMDB ID

What if you have a movie or series name, but you need it’s IMDB ID?

We recommend using name-to-imdb in this case, and it’s really easy to use:

var nameToImdb = require("name-to-imdb");

nameToImdb({ name: "south park" }, function(err, res, inf) { 
  console.log(res); // prints "tt0121955"
  console.log(inf); // inf contains info on where we matched that name - e.g. metadata, or on imdb
})

Also setting the type and year in the request helps on ensuring that the IMDB ID that is returned is correct.

Using User Data in Addons

The Addon SDK now supports user data, this part of the docs will remain here as it is valid for use with the Express module.

This example does not use the Stremio Addon SDK, it uses Node.js and Express to serve replies.

User data is passed in the Addon Repository URL, so instead of users installing addons from the normal manifest url (for example: https://www.mydomain.com/manifest.json), users will also need to add the data they want to pass to the addon in the URL (for example: https://www.mydomain.com/c9y2kz0c26c3w4csaqne71eu4jqko7e1/manifest.json, where c9y2kz0c26c3w4csaqne71eu4jqko7e1 could be their API Authentication Token)

Simplistic Example:

const express = require('express')
const addon = express()

addon.get('/:someParameter/manifest.json', function (req, res) {
  res.send({
    id: 'org.parameterized.'+req.params.someParameter,
    name: 'addon for '+req.params.someParameter,
    resources: ['stream'],
    types: ['series'],
  })
})

addon.get('/:someParameter/stream/:type/:id.json', function(req, res) {
  // @TODO do something depending on req.params.someParameter
  res.send({ streams: [] })
})

addon.listen(7000, function() {
  console.log('http://127.0.0.1:7000/[someParameter]/manifest.json')
})

This is not a working example, it simply shows how data can be inserted by users in the Addon Repository URL so addons can then make use of it.

For working examples, you can check these addons:

Another use case for passing user data through the Addon Repository URL is creating proxy addons. This case presumes that the id of a different addon is sent in the Addon Repository URL, then the proxy addon connects to the addon of which the id it got, requests streams, passes the stream url to some API (for example Real Debrid, Premiumize, etc) to get a different streaming url that it then responds with for Stremio.

Creating Addon Configuration Pages

This guide extends Using User Data in Addons by explaining how to create an Addon Configuration Page.

In order to allow Addons to request user data, you will first need to create a web page on the /configure path of your addon and set manifest.behaviorHints.configurable to true. (you can also set manifest.behaviorHints.configurationRequired to true if your addon cannot be installed without user data)

The /configure web page should include a form with all required data, the form data should be set within the Addon Repository URL (as explained in the Using User Data in Addons) section.

An “Install Addon” button should be available on the page that will use the stremio:// protocol, for example, if your Addon Repository URL is https://my.addon.com/some-user-data/manifest.json, the “Install Addon” button should point to stremio://my.addon.com/some-user-data/manifest.json. (the stremio:// protocol links will open or focus the Stremio app with a prompt to install the addon)

Stremio supports deep links, such links can also be used in addons to link internally to Stremio.

First, set the stream resource in your manifest:

Here’s an example:

// this responds with one stream for the Big Buck Bunny
// movie, that if clicked, will redirect Stremio to the
// Board page
builder.defineStreamHandler(function(args) {
  return new Promise(function(resolve, reject) {
    if (args.type === 'movie' && args.id === 'tt1254207') {
      // serve one stream for big buck bunny
      const stream = { externalUrl: 'stremio:///board' }
      resolve({ streams: [stream] })
    } else {
      reject(new Error('No streams found for: ' + args.id))
    }
  })
})

Proxying Other Addons

Stremio addons use a HTTP server to handle requests and responses, this means that other addons can also request their responses.

This can be useful for many reasons, a guide on how this can be done is included in the readme of the IMDB Watchlist Addon which proxies the IMDB Lists addon to get the IMDB List for a particular IMDB user.

IMDB Watchlist only proxies the catalog from IMDB Lists, to proxy other resources you can use the same pattern as IMDB Watchlist does, and check the endpoints and patterns for other resources on the Protocol Documentation page.

Crawler / Scraping Addons

Scraping HTML pages presumes downloading the HTML source of a web page in order to get specific data from it.

A guide showing a simplistic version of doing this is in the readme of the IMDB Watchlist Addon. The addon uses needle to request the HTML source and cheerio to start a jQuery instance in order to simplify getting the desired information.

Cheerio is not the only module that can help with crawling / scraping though, other modules that can aid in this: jsdom, xpath, etc