stremio

package module
v0.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 11, 2020 License: AGPL-3.0 Imports: 22 Imported by: 4

README

go-stremio

Stremio addon SDK for Go

Contents

  1. Introduction
  2. About this SDK
  3. Features
  4. Example
  5. Advantages
  6. Related projects

Introduction

Stremio is a modern media center that's a one-stop solution for your video entertainment. You discover, watch and organize video content from easy to install addons.

These addons run remotely as a web service, so they can't do any harm to your computer. This is different from how addons work for Kodi for example, where they run locally on your computer and have dozens of third party dependencies. There have been several security incidents with Kodi addons like this one and even the Kodi developers themselves warn of the dangers of third party Kodi addons.

About this SDK

When developing a Stremio addon, you're essentially developing a web service. But there are some defined routes, expected behavior, JSON structure etc., so instead of having to figure all of this out on your own before you've got even a basic addon running, using an SDK can get you up to speed much faster, as it takes care of all of this automatically.

But the official Stremio addon SDK is for Node.js only.

This SDK is for Go!

It provides the most important parts of the Node.js SDK and depending on the requirements of you, the libary users, it will be extended to provide more in the future.

Features

  • All required types for building catalog and stream addons
  • Web server with graceful shutdown
  • CORS middleware to allow requests from Stremio
  • Health check endpoint
  • Optional profiling endpoints (for go pprof)
  • Optional request logging
  • Optional cache control and ETag handling
  • Optional custom middleware
  • Optional custom endpoints
  • Custom user data (users can have settings for your addon!)
    • With optional URL-safe Base64 decoding and JSON unmarshalling
  • Addon installation callback (manifest endpoint)

Current non-features, as they're usually part of a reverse proxy deployed in front of the service:

  • TLS termination (for using HTTPS)
  • Rate limiting (against DoS attacks)
  • Compression (like gzip)

Example

Full examples can be found in examples. Here's a part of the one for a stream addon:

package main

import (
    "github.com/deflix-tv/go-stremio"
)

var (
    manifest = stremio.Manifest{
        ID:          "com.example.blender-streams",
        Name:        "Blender movie streams",
        Description: "Stream addon for free movies that were made with Blender",
        // ...
    }
)

func main() {
	// Let the movieHandler handle the "movie" type
    streamHandlers := map[string]stremio.StreamHandler{"movie": movieHandler}

    addon, err := stremio.NewAddon(manifest, nil, streamHandlers, stremio.DefaultOptions)
    if err != nil {
        panic(err)
    }

    addon.Run()
}

func movieHandler(id string, userData interface{}) ([]stremio.StreamItem, error) {
    // We only serve Big Buck Bunny and Sintel
    if id == "tt1254207" {
        return []stremio.StreamItem{
            // Torrent stream
            {
                InfoHash: "dd8255ecdc7ca55fb0bbf81323d87062db1f6d1c",
                // Stremio recommends to set the quality as title, as the streams
                // are shown for a specific movie so the user knows the title.
                Title:     "1080p (torrent)",
                FileIndex: 1,
            },
            // HTTP stream
            {
                URL:   "http://distribution.bbb3d.renderfarming.net/video/mp4/bbb_sunflower_1080p_30fps_normal.mp4",
                Title: "1080p (HTTP stream)",
            },
        }, nil
    } else if id == "tt1727587" {
        // ...
    }
    return nil, stremio.NotFound
}

Advantages

Some reasons why you might want to consider developing an addon in Go with this SDK:

Criterium Node.js addon Go addon
Direct SDK dependencies 9 5
Transitive SDK dependencies 85 8
Size of a runnable addon 27 MB¹ 15 MB
Number of artifacts to deploy depends² 1
Runtime dependencies Node.js -
Concurrency Single-threaded Multi-threaded

¹) du -h --max-depth=0 node_modules
²) All your JavaScript files and the package.json if you can install the depencencies with npm on the server, otherwise (like in a Docker container) you also need all the node_modules, which are hundreds to thousands of files.

Looking at the performance it depends a lot on what your addon does. Due to the single-threaded nature of Node.js, the more CPU-bound tasks your addon does, the bigger the performance difference will be (in favor of Go). Here we compare the simplest possible addon to be able to compare just the SDKs and not any additional overhead (like DB access):

Criterium Node.js addon Go addon
Startup time to 1st request¹ 150-230ms 5-20ms
Max rps² @ 1000 connections Local³: 6,000
Remote⁴: 3,000
Local³: 59,000
Remote⁴: 58,000
Memory usage @ 1000 connections Idle: 35 MB
Load⁵: 70 MB
Idle: 10 MB
Load⁵: 45 MB

¹) Measured using ttfok and the code in benchmark. This metric is relevant in case you want to use a "serverless functions" service (like AWS Lambda or Vercel (former ZEIT Now)) that doesn't keep your service running between requests.
²) Max number of requests per second where the p99 latency is still < 100ms
³) The load testing tool ran on a different server, but in the same datacenter and the requests were sent within a private network
⁴) The load testing tool ran on a different server in a different datacenter of another cloud provider in another city for more real world-like circumstances
⁵) At a request rate half of what we measured as maximum

The load tests were run under the following circumstances:

  • We used the addon code, load testing tool and setup described in benchmark
  • We ran the service on a DigitalOcean "Droplet" with 2 cores and 2 GB RAM, which costs $15/month
  • The load tests ran for 60s, with previous warmup
  • Virtualized servers of cloud providers vary in performance throughout the week and day, even when using the exact same machines, because the CPU cores of the virtualization host are shared between multiple VPS. We conducted the Node.js and Go service tests at the same time so their performance difference doesn't come from the performance variation due to running at different times.
  • The client server used to run the load testing tool was high-powered (4-8 dedicated cores, 8-32 GB RAM)

Additional observations:

  • The Go service's response times were generally lower across all request rates
  • The Go service's response times had a much lower deviation, i.e. they were more stable. With less than 60s of time for the load test the Node.js service fared even worse, because outliers lead to a higher p99 latency.
  • We also tested on a lower-powered server by a cheap cloud provider (also 2 core, 2 GB RAM, but the CPU was generally worse). In this case the difference between the Node.js and the Go service was even higher. The Go service is perfectly fitted for scaling out with multiple cheap servers.
  • We also tested with different amounts of connections. With more connections the difference between the Node.js and the Go service was also higher. In a production deployment you want to be able to serve as many users as possible, so this goes in favor of the Go service as well.

Note:

  • This Go SDK is at its very beginning. Some features will be added in the future that might decrease its performance, while others will increase it.
  • The Node.js addon was run as a single instance. You can do more complex deployments with a load balancer like HAProxy and multiple instances of the same Node.js service on a single machine to take advantage of multiple CPU cores. But then you should also activate preforking in the Go addon for using several OS processes in parallel, which we didn't do.

Documentation

Index

Constants

This section is empty.

Variables

View Source
var DefaultOptions = Options{
	BindAddr:     "localhost",
	Port:         8080,
	LoggingLevel: "info",
}

DefaultOptions is an Options object with default values. For fields that aren't set here the zero value is the default value.

View Source
var (
	// NotFound signals that the catalog/meta/stream was not found.
	// It leads to a "404 Not Found" response.
	NotFound = errors.New("Not found")
)

Functions

func NewLogger added in v0.3.0

func NewLogger(level string) (*zap.Logger, error)

NewLogger creates a new logger with sane defaults and the passed level. Supported levels are: debug, info, warn, error. Only logs with that level and above are then logged (e.g. with "info" no debug logs will be logged). It makes sense to get this logger as early as possible and use it in your ManifestCallback, CatalogHandler and StreamHandler, so that all logs behave and are formatted the same way. You should then also set this logger in the options for `NewAddon()`, so that not two loggers are created. Alternatively you can create your own custom *zap.Logger and set it in the options when creating a new addon, leading to the addon using that custom logger.

Types

type Addon

type Addon struct {
	// contains filtered or unexported fields
}

Addon represents a remote addon. You can create one with NewAddon() and then run it with Run().

func NewAddon

func NewAddon(manifest Manifest, catalogHandlers map[string]CatalogHandler, streamHandlers map[string]StreamHandler, opts Options) (*Addon, error)

NewAddon creates a new Addon object that can be started with Run(). A proper manifest must be supplied, but manifestCallback and all but one handler can be nil in case you only want to handle specific requests and opts can be the zero value of Options.

func (*Addon) AddEndpoint added in v0.3.0

func (a *Addon) AddEndpoint(method, path string, handler func(*fiber.Ctx))

AddEndpoint adds a custom endpoint (a route and its handler). If you want to be able to access custom user data, you can use a path like this: "/:userData/foo" and then either deal with the data yourself by using `c.Params("userData", "")` in the handler, or use the convenience method `DecodeUserData("userData", c)`.

func (*Addon) AddMiddleware added in v0.3.0

func (a *Addon) AddMiddleware(path string, mw func(*fiber.Ctx))

AddMiddleware appends a custom middleware to the chain of existing middlewares. Set path to an empty string or "/" to let the middleware apply to all routes. Don't forget to call c.Next() on the Fiber context!

func (*Addon) DecodeUserData added in v0.3.0

func (a *Addon) DecodeUserData(param string, c *fiber.Ctx) (interface{}, error)

DecodeUserData decodes the request's user data and returns the result. It's useful when you add custom endpoints to the addon that don't have a userData parameter like the ManifestCallback, CatalogHandler and StreamHandler have. The param value must match the URL parameter you used when creating the custom endpoint, for example when using `AddEndpoint("GET", "/:userData/ping", customEndpoint)` you must pass "userData".

func (*Addon) RegisterUserData added in v0.3.0

func (a *Addon) RegisterUserData(userDataObject interface{})

RegisterUserData registers the type of userData, so the addon can automatically unmarshal user data into an object of this type and pass the object into the manifest callback or catalog and stream handlers.

func (*Addon) Run

func (a *Addon) Run()

Run starts the remote addon. It sets up an HTTP server that handles requests to "/manifest.json" etc. and gracefully handles shutdowns.

func (*Addon) SetManifestCallback added in v0.3.0

func (a *Addon) SetManifestCallback(callback ManifestCallback)

SetManifestCallback sets the manifest callback

type BehaviorHints

type BehaviorHints struct {
	// Note: Must include `omitempty`, otherwise it will be included if this struct is used in another one, even if the field of the containing struct is marked as `omitempty`
	Adult bool `json:"adult,omitempty"`
	P2P   bool `json:"p2p,omitempty"`
}

type CatalogHandler

type CatalogHandler func(id string, userData interface{}) ([]MetaPreviewItem, error)

CatalogHandler is the callback for catalog requests for a specific type (like "movie"). The id parameter is the catalog ID that you specified yourself in the CatalogItem objects in the Manifest. The userData parameter depends on whether you called `RegisterUserData()` before: If not, a simple string will be passed. It's empty if the user didn't provide user data. If yes, a pointer to an object you registered will be passed. It's nil if the user didn't provide user data.

type CatalogItem

type CatalogItem struct {
	Type string `json:"type"`
	ID   string `json:"id"`
	Name string `json:"name"`

	// Optional
	Extra []ExtraItem `json:"extra,omitempty"`
}

CatalogItem represents a catalog.

type ExtraItem

type ExtraItem struct {
	Name string `json:"name"`

	// Optional
	IsRequired   bool     `json:"isRequired,omitempty"`
	Options      []string `json:"options,omitempty"`
	OptionsLimit int      `json:"optionsLimit,omitempty"`
}

type Manifest

type Manifest struct {
	ID          string `json:"id"`
	Name        string `json:"name"`
	Description string `json:"description"`
	Version     string `json:"version"`

	// One of the following is required
	// Note: Can only have one in code because of how Go (de-)serialization works
	//Resources     []string       `json:"resources,omitempty"`
	ResourceItems []ResourceItem `json:"resources,omitempty"`

	Types    []string      `json:"types"`
	Catalogs []CatalogItem `json:"catalogs"`

	// Optional
	IDprefixes    []string      `json:"idPrefixes,omitempty"`
	Background    string        `json:"background,omitempty"` // URL
	ContactEmail  string        `json:"contactEmail,omitempty"`
	BehaviorHints BehaviorHints `json:"behaviorHints,omitempty"`
}

Manifest describes the capabilities of the addon. See https://github.com/Stremio/stremio-addon-sdk/blob/f6f1f2a8b627b9d4f2c62b003b251d98adadbebe/docs/api/responses/manifest.md

type ManifestCallback added in v0.3.0

type ManifestCallback func(userData interface{}) int

ManifestCallback is the callback for manifest requests, so mostly addon installations. You can use the callback to *prevent* users from installing your addon. The userData parameter depends on whether you called `RegisterUserData()` before: If not, a simple string will be passed. It's empty if the user didn't provide user data. If yes, a pointer to an object you registered will be passed. It's nil if the user didn't provide user data. Return an HTTP status code >= 400 to stop further processing and let the addon return that exact status code. Any status code < 400 will lead to the manifest being returned with a 200 OK status code in the response.

type MetaItem

type MetaItem struct {
	ID   string `json:"id"`
	Type string `json:"type"`
	Name string `json:"name"`

	// Optional
	Genres      []string       `json:"genres,omitempty"`   // Will be replaced by Links at some point
	Director    []string       `json:"director,omitempty"` // Will be replaced by Links at some point
	Cast        []string       `json:"cast,omitempty"`     // Will be replaced by Links at some point
	Links       []MetaLinkItem `json:"links,omitempty"`    // For genres, director, cast and potentially more. Not fully supported by Stremio yet!
	Poster      string         `json:"poster,omitempty"`   // URL
	PosterShape string         `json:"posterShape,omitempty"`
	Background  string         `json:"background,omitempty"` // URL
	Description string         `json:"description,omitempty"`
	ReleaseInfo string         `json:"releaseInfo,omitempty"` // E.g. "2000" for movies and "2000-2014" or "2000-" for TV shows
	IMDbRating  string         `json:"imdbRating,omitempty"`
	Released    string         `json:"released,omitempty"` // Must be ISO 8601, e.g. "2010-12-06T05:00:00.000Z"
	Videos      []VideoItem    `json:"videos,omitempty"`
	Runtime     string         `json:"runtime,omitempty"`
	Language    string         `json:"language,omitempty"`
	Country     string         `json:"country,omitempty"`
	Awards      string         `json:"awards,omitempty"`
	Website     string         `json:"website,omitempty"` // URL

}

MetaItem represents a meta item and is meant to be used when info for a specific item was requested. See https://github.com/Stremio/stremio-addon-sdk/blob/f6f1f2a8b627b9d4f2c62b003b251d98adadbebe/docs/api/responses/meta.md

type MetaLinkItem

type MetaLinkItem struct {
	Name     string `json:"name"`
	Category string `json:"category"`
	URL      string `json:"url"` //  // URL. Can be "Meta Links" (see https://github.com/Stremio/stremio-addon-sdk/blob/f6f1f2a8b627b9d4f2c62b003b251d98adadbebe/docs/api/responses/meta.links.md)
}

MetaLinkItem links to a page within Stremio. It will at some point replace the usage of `genres`, `director` and `cast`. Note: It's not fully supported by Stremio yet (not fully on PC and not at all on Android)!

type MetaPreviewItem

type MetaPreviewItem struct {
	ID     string `json:"id"`
	Type   string `json:"type"`
	Name   string `json:"name"`
	Poster string `json:"poster"` // URL

	// Optional
	PosterShape string `json:"posterShape,omitempty"`

	// Optional, used for the "Discover" page sidebar
	Genres      []string       `json:"genres,omitempty"`   // Will be replaced by Links at some point
	Director    []string       `json:"director,omitempty"` // Will be replaced by Links at some point
	Cast        []string       `json:"cast,omitempty"`     // Will be replaced by Links at some point
	Links       []MetaLinkItem `json:"links,omitempty"`    // For genres, director, cast and potentially more. Not fully supported by Stremio yet!
	IMDbRating  string         `json:"imdbRating,omitempty"`
	ReleaseInfo string         `json:"releaseInfo,omitempty"` // E.g. "2000" for movies and "2000-2014" or "2000-" for TV shows
	Description string         `json:"description,omitempty"`
}

MetaPreviewItem represents a meta preview item and is meant to be used within catalog responses. See https://github.com/Stremio/stremio-addon-sdk/blob/f6f1f2a8b627b9d4f2c62b003b251d98adadbebe/docs/api/responses/meta.md#meta-preview-object

type Options

type Options struct {
	// The interface to bind to.
	// "0.0.0.0" to bind to all interfaces. "localhost" to *exclude* requests from other machines.
	// Default "localhost".
	BindAddr string
	// The port to listen on.
	// Default 8080.
	Port int
	// You can set a custom logger, or leave this empty to create a new one
	// with sane defaults and the LoggingLevel in these options.
	// If you already called `NewLogger()`, you should set that logger here.
	// Default nil.
	Logger *zap.Logger
	// The logging level.
	// Only logs with the same or a higher log level will be shown.
	// For example when you set it to "info", info, warn and error logs will be shown, but no debug logs.
	// Accepts "debug", "info", "warn" and "error".
	// Default "info".
	LoggingLevel string
	// Flag for indicating whether requests should be logged.
	// Default false (meaning requests will be logged by default).
	DisableRequestLogging bool
	// Flag for indicating whether IP addresses should be logged.
	// Default false.
	LogIPs bool
	// Flag for indicating whether the user agent header should be logged.
	// Default false.
	LogUserAgent bool
	// URL to redirect to when someone requests the root of the handler instead of the manifest, catalog, stream etc.
	// When no value is set, it will lead to a "404 Not Found" response.
	// Default "".
	RedirectURL string
	// Flag for indicating whether you want to expose URL handlers for the Go profiler.
	// The URLs are be the standard ones: "/debug/pprof/...".
	// Default false.
	Profiling bool
	// Duration of client/proxy-side cache for responses from the catalog endpoint.
	// Helps reducing number of requsts and transferred data volume to/from the server.
	// The result is not cached by the SDK on the server side, so if two *separate* users make a reqeust,
	// and no proxy cached the response, your CatalogHandler will be called twice.
	// Default 0.
	CacheAgeCatalogs time.Duration
	// Same as CacheAgeCatalogs, but for streams.
	CacheAgeStreams time.Duration
	// Flag for indicating to proxies whether they are allowed to cache responses from the catalog endpoint.
	// Default false.
	CachePublicCatalogs bool
	// Same as CachePublicCatalogs, but for streams.
	CachePublicStreams bool
	// Flag for indicating whether the "ETag" header should be set and the "If-None-Match" header checked.
	// Helps reducing the transferred data volume from the server even further.
	// Only makes sense when setting a non-zero CacheAgeCatalogs.
	// Leads to a slight computational overhead due to every CatalogHandler result being hashed.
	// Default false.
	HandleEtagCatalogs bool
	// Same as HandleEtagCatalogs, but for streams.
	HandleEtagStreams bool
	// Flag for indicating whether user data should is Base64-encoded.
	// As the user data is in the URL it needs to be the URL-safe Base64 encoding described in RFC 4648.
	// When true, go-stremio first decodes the value before passing or unmarshalling it.
	// Default false.
	UserDataIsBase64 bool
}

Options are the options that can be used to configure the addon.

type ResourceItem

type ResourceItem struct {
	Name  string   `json:"name"`
	Types []string `json:"types"`

	// Optional
	IDprefixes []string `json:"idPrefixes,omitempty"`
}

type StreamHandler

type StreamHandler func(id string, userData interface{}) ([]StreamItem, error)

StreamHandler is the callback for stream requests for a specific type (like "movie"). The id parameter can be for example an IMDb ID if your addon handles the "movie" type. The userData parameter depends on whether you called `RegisterUserData()` before: If not, a simple string will be passed. It's empty if the user didn't provide user data. If yes, a pointer to an object you registered will be passed. It's nil if the user didn't provide user data.

type StreamItem

type StreamItem struct {
	// One of the following is required
	URL         string `json:"url,omitempty"` // URL
	YoutubeID   string `json:"ytId,omitempty"`
	InfoHash    string `json:"infoHash,omitempty"`
	ExternalURL string `json:"externalUrl,omitempty"` // URL

	// Optional
	Title     string `json:"title,omitempty"`   // Usually used for stream quality
	FileIndex uint8  `json:"fileIdx,omitempty"` // Only when using InfoHash

}

StreamItem represents a stream for a MetaItem. See https://github.com/Stremio/stremio-addon-sdk/blob/f6f1f2a8b627b9d4f2c62b003b251d98adadbebe/docs/api/responses/stream.md

type VideoItem

type VideoItem struct {
	ID       string `json:"id"`
	Title    string `json:"title"`
	Released string `json:"released"` // Must be ISO 8601, e.g. "2010-12-06T05:00:00.000Z"

	// Optional
	Thumbnail string       `json:"thumbnail,omitempty"` // URL
	Streams   []StreamItem `json:"streams,omitempty"`
	Available bool         `json:"available,omitempty"`
	Episode   string       `json:"episode,omitempty"`
	Season    string       `json:"season,omitempty"`
	Trailer   string       `json:"trailer,omitempty"` // Youtube ID
	Overview  string       `json:"overview,omitempty"`
}

Directories

Path Synopsis
examples

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL