Skip to main content

Build a source addon

A source addon exposes logical media to StreamShare without writing into the application database. The host asks the addon what it supports, requests bounded pages of media and resolves a selected logical item only when playback is needed.

Lifecycle

One source invocation follows this sequence:

  1. StreamShare creates an isolated addon instance and calls onInit(api).
  2. getSourceInfo() returns stable identity, availability and capabilities.
  3. The host calls search(), browse() or getRecent() according to those capabilities.
  4. When the user selects playable media, resolvePlayback(sourceItemId) returns the resource.

An addon must not perform expensive synchronization in getSourceInfo(). Return isAvailable: false when required configuration or local state is missing.

SourceCapabilities controls which operations and filters the host may present. A true flag is a behavioral promise: the corresponding criteria must be applied before pagination.

For example, a source that filters types and years but has no metadata-provider lookup should report:

capabilities: {
supportsSearch: true,
supportsBrowse: true,
supportsTypeFilter: true,
supportsGenreFilter: false,
supportsYearFilter: true,
supportsTmdbLookup: false,
supportedSorts: ['relevance', 'date', 'rating', 'title'],
supportedTypes: ['movie', 'series', 'season', 'episode', 'folder', 'video'],
}

Keep logical IDs stable

Every SourceMediaItem needs an ID that remains stable across searches, pages and application restarts. StreamShare passes that ID back to the addon for browsing and playback. Do not use a page position, translated title or temporary URL as the logical ID.

Set isContainer: true for folders, series and seasons that should open another browse level. Return playable movies and episodes with isContainer: false. Use video with isContainer: false for generic playable content that has no movie or episodic TMDB identity, such as a documentary feed or television clip. Its stable id follows the same resolvePlayback() lifecycle as other playable items. Advertising video in supportedTypes lets the host include the source in generic-video searches.

Filter, sort, then paginate

Apply every supported criterion and the requested sort before slicing a page. Treat pageToken as opaque at the API boundary: your addon may use an offset, cursor or signed provider token, while the host passes it back unchanged.

Normalize rating to the inclusive zero-to-ten scale used by the source contract. For example, multiply a five-star provider score by two. Omit the field when the provider has no reliable rating instead of substituting zero.

const hasMore = offset + limit < matchingItems.length;

return {
items: matchingItems.slice(offset, offset + limit),
hasMore,
nextPageToken: hasMore ? String(offset + limit) : undefined,
};

Keep pages bounded even when the host omits limit. Facets must describe the matching result set, not only the current page.

Separate discovery from playback

Search and browse should return logical metadata. Resolve the final URL and any required HTTP headers in resolvePlayback() only after the user selects an item. Reject unknown IDs and containers instead of returning a guessed resource.

When source selection uses a text-query fallback, verify the returned title, year and episodic context before attaching variants to the selected media. Never assume that the first remote search result is the requested item. Prefer the addon's own targetSourceItemId, and keep every returned ID independently resolvable after an addon reload rather than relying on a previous in-memory search result.

Return media request headers through SourcePlaybackInfo.headers. StreamShare forwards them to playback targets that support custom headers, but external targets may differ; test each platform the addon claims to support. Resolve signed or short-lived media URLs at playback time instead of storing them as stable IDs.

Network permissions cover requests performed by addon APIs such as api.http. They do not replace the developer's responsibility to distribute only media and credentials they are authorized to expose.

Describe playback quality

Playable SourceMediaItem records can provide the metadata rendered in source selection:

FieldExampleMeaning
resolution1080p, 2160pVideo resolution or quality tier
sourceWEB-DL, BluRayDistribution or provider origin
encodingH.264, HEVC, AV1Video codec or encoding
languageMULTI, VO, VOSTFRAudio or content-language label
qualityLabel1080p WEB-DL HEVCComposite fallback for legacy or unstructured data
tags[{label: 'Host A', color: '#38BDF8'}]Short addon-defined badges for provider-specific metadata

Prefer the structured fields whenever they are known. StreamShare renders them as separate badges and uses qualityLabel only when resolution, source and encoding are absent.

{
id: 'provider:movie:42',
type: 'movie',
title: 'Example movie',
isContainer: false,
resolution: '2160p',
source: 'WEB-DL',
encoding: 'HEVC',
language: 'MULTI',
tags: [{label: 'Host A', color: '#38BDF8'}],
}

Keep source for the distribution origin rather than the hosting provider. Use tags when a provider, edition or other source-specific value needs its own badge. Tag colors are optional six-digit hexadecimal values. Hosts validate colors, limit the number and length of tags, and choose readable foreground text automatically.

For several playable variants of the same media, keep one logical record in normal search and browse results. Advertise supportsPlaybackVariants: true, then return one item per variant when search() receives isSourceSelection: true. Each variant needs a stable distinct id; StreamShare passes the selected variant ID to resolvePlayback().

When the selected media already came from the receiving source, targetSourceItemId contains that source's previous opaque item ID. Prefer it over query to locate the logical record: host enrichment can localize or replace the displayed title before source selection. The value is source-specific and must not be interpreted by another source. A returned variant can copy targetTmdbId when the source ID confirms that it belongs to that requested media. Do not copy the requested metadata ID onto an unverified text-search result merely to force a match.

For addons backed by api.sourceCatalog, translate targetSourceItemId to the catalog query's exact mediaId; do not combine that exact lookup with enriched title or year filters.

Do not advertise playback variants when every logical media item has only one playable representation. Missing structured metadata is displayed as an unknown quality rather than being guessed by the interface.

For rich titles, artwork and episode metadata, follow TMDB metadata and source addons. Returning a TMDB identifier enables host enrichment; it does not require embedding TMDB credentials in the addon.

Start from the reference implementation

The Example Catalog Source demonstrates the complete lifecycle with deterministic in-memory data, two-level pagination and no external dependency. Its contract tests are suitable as a starting point for a real provider adapter.

When the provider is queried on demand instead of synchronized into a local catalog, continue with Adapt a remote catalog for variant loading, hierarchy, facets, configuration and HTTP limitations.