Kosmos is a light-weight source provider package framework for Syncler. Kosmos lets developers/users write source provider packages for Syncler in javascript with complete freedom.
Syncler makes it really easy for developers to verify that the source request is indeed coming from the Syncler app itself. This can also be used as an authentication tool to authenticate syncler users to your server.
Obviously you can use your own authentication, but this is just a completely optional, quick out of the box solution for devs to prevent spam requests to their resources.
This is done by verifying JWT token (with public key) supplied to the package at runtime. More documentation can be found below. The public key for the app can be found here
https://syncler.net/packages/rsa
Developing tooling for debugging is beyond the scope of this app. If you have developed any tooling and would like to share, please send us your github repository and we will link to it here.
Good news is you can easily debug your package in a browser window (make sure to disable CORS in your browser) or a nodeJS enviroment.
Here is a sample Kosmos package with documentation in the comments.
/*
{
"name": "Sample Package",
"id": "an.universally.unique.id.to.identify.your.package",
"version": 5,
"classPath": "jsExtensionSample.SampleProviderPackage",
"permaUrl": "http://bit.ly/...",
"cacheServer": "cache-server.example.com:443"
}
*/
/**
* This is a sample SourceProviderPackage implementation.
* This also includes documentation.
* Read the whole document to understand the available objects to work with.
*/
/**
* Your package js file must contain a manifest declaration at the very beginning of the file.
* This declaration must be pure json and must be inside a comment as exactly shown above at the top of this file.
*
* Manifest properties
* name: Name of your package.
*
* id: An unique id to identify your package universally. Colliding this id with other packages
* will override your package with some other package with the same id.
*
* version: Version of your package. Must be an int and bumped every new release.
*
* classPath: A fully qualified path to your SourceProviderPackage class that implements createBundle().
* Without this your package cannot be initialized.
*
* [Optional]
* permaUrl: A permanent url to your package. This url will be pinged to check for updates.
* It's highly recommended that you provide this.
*
* Your package will run inside a browser.
* Your package must follow the UMD (https://github.com/umdjs/umd) module standard as seen below.
* Your package must not use any external dependencies that cannot run on a browser.
*/
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
typeof define === 'function' && define.amd ? define(['exports'], factory) :
(factory((global.jsExtensionSample = {})));
}(this, (function (exports) { 'use strict';
/**
*
* Each provider package must implement the two following methods
* createBundle()
* createSourceProvider()
* as documented below.
* Note: These 2 methods are called in different threads in different
* environments. Any variable you store in one method will not be available in other.
*/
class SampleProviderPackage {
/**
* Must have a parameter-less constructor
*/
constructor() {
this.sourceProviders = [new SampleSourceProvider()];
}
/**
* Called at the very beginning of source search.
* Provider package must return a promise of a bundle.
* The package will have less than 5 seconds to resolve this promise.
* Package MUST not search for sources here. This is where package should return cached results if it has any.
* @param env Explained later.
* @param request Explained later.
*/
createBundle(env, request) {
/**
* A promise
*/
return new Promise((resolve, reject) => {
/**
* A new bundle object
*/
let bundle = {
/**
* Cached sources that are already available without search.
*/
sources: [],
/**
* Metadata of providers which will need to search for sources.
* This metadata will be later passed on to the createSourceProvider() method
* to initialize a SourceProvider and search on different threads for better performance
*/
sourceProviderMetadatas: this.sourceProviders.map(i => i.metadata)
};
resolve(bundle);
});
}
/**
* Create an instance of a SourceProvider that implements search(env,searchRequest);
* @param env Explained later.
* @param metadata The metadata returned in createBundle() call.
*/
createSourceProvider(env, metadata) {
/**
* Here we are just matching the name of the provider supplied in metadata object
* and returning that instance from our already initialized list of source providers
*/
return this.sourceProviders.filter(x => x.metadata.name == metadata.name)[0];
}
}
/**
* Each SourceProvider must implement a
* search() method as defined below
*/
class SampleSourceProvider {
constructor() {
/**
* Metadata about the source provider
*/
this.metadata = {
name: 'SampleSourceProvider',
/**
* All sources produced by this provider will appear as premium
* Default is true
*/
premium: true,
/**
* Defines whether torrent resolvers should be executed after
* this provider returns sources.
*/
containsTorrents: true,
/**
* [Optional] true/false
* Supply this flag when possible so that search can be optimized at
* runtime depending on debrid service is available or not
*/
requiresDebrid: true,
/**
* [Optional] json object
* Extra data space for encoding any data you might later need to initialize a source provider.
*/
data: {
/**
* For example here we storage an api key
* This data will be available to you in createSourceProvider(...) method
*/
"apiKey": "XXXX"
}
};
}
/**
* Returns a promise that is resolved/rejected to find sources
* @param env
* @param request
*/
search(env, request) {
return new Promise((resolve, reject) => {
/**
* The env object gives you access to various capabilities
* such as
* HttpClient: Make http requests to the internet
* App: Information about the app
* User: Information about user
*
* [Pending implementation]
* PersistentStorage: Store persistent data, such as cookies
*
* [Pending implementation]
* SessionStorage [Not implemented yet]: Store temporary data that is gone after an app reboot
*/
/**
* User object contains data about the user
*/
var user = env.user;
//string[]: Array of preferred languages
console.log(user.languages); //['en','fr']
//bool: if user is a premium member
console.log(user.premium); //true
/**
* Contains information about the app
*/
var app = env.app;
/**
* string: A jwt token [algorithm: RS256] created by the app for your package which
* can be used as an authentication tool.
*
* You can verify the authenticity of this token
* by verifying the signature of this token
* with the apps public key for source provider packages.
* This can be found on the official developer documentation site.
*
*/
console.log(app.jwtToken);
/**
* object: The request object contains
* data about the current source request.
*/
console.log(request);
//object: Movie to search source for.
console.log(request.movie);
//object: Episode to search source for.
console.log(request.episode);
var item;
if (request.episode) {
//int
console.log(request.episode.seasonNumber); //1
//int
console.log(request.episode.episodeNumber); //1
//object
console.log(request.episode.show);
item = request.episode.show;
}
else {
item = request.movie;
}
//string
console.log(item.titles.main.title); //Sample movie name
//string
console.log(item.titles.main.language); //en
if (item.titles.original) {
console.log(item.titles.original.title); //Original title
console.log(item.titles.original.language); //Original title language
}
//title[]: Alternate titles
if (item.titles.alternate) {
item.titles.alternate.forEach(title => {
//string
console.log(title.title); //Sample original title
//string
console.log(title.language); //en
});
}
// string[]
if (item.cast) {
item.cast.forEach(cast => {
console.log(cast); //string
});
}
// string[]
if (item.crew) {
item.crew.forEach(crew => {
console.log(crew); //string
});
}
//int: unix timestamp in seconds
console.log(item.release);
/**
* Ids
*/
console.log(item.ids.imdb); //string
console.log(item.ids.tmdb); //string
console.log(item.ids.trakt); //string
console.log(item.ids.tvdb); //string
console.log(item.ids.mal); //string
/**
* bool: True for all standard content that are retrieved from
* trakt/tmdb.
*/
console.log(item.standard);
/**
* bool: True for all standard content that are retrieved from
* mal/anilist.
*/
console.log(item.anime);
/**
* bool: true for all 3rd party content that are available on a 3rd party website
* You are advised to check the url of this object to see the source in this case
* to properly identify the content
*/
console.log(item.thirdParty);
//string: Url of the item where it was retrieved from
console.log(item.url);
/**
* [Pending implementation]
* Example of getting data (cookies in this case) from persistent storage
* Session storage can also be used the same way
*/
var persistentStorage = env.persistentStorage;
var sessionStorage = env.sessionStorage;
/**
* Returns data (can be undefined/null)
* where
* key1=cookies
* and key2=example.com
* and key3 = /path-to-this-cookie
*/
var cookies = persistentStorage.getValue({
key1: 'cookies',
key2: 'example.com',
key3: '/path-to-this-cookie'
});
/**
* Creating an axios instance to make http requests.
* Documentation for axios can be found here
* https://github.com/axios/axios
*
* All http requests must use an axios instance created
* by httpClientFactory as shown below.
*
* Everything else such as
* fetch, XmlHttpRequest will fail.
*/
var axios = env.httpClientFactory.createNewInstance();
/**
* Example of making a http request with cookie
*/
axios.request({
url: 'http://example.com',
headers: {
cookie: cookies
}
}).then(response => {
/**
* An example of reading headers
* And storing cookies in persistent storage
*/
if (response.headers && response.headers['set-cookie']) {
persistentStorage.setValue({
key1: 'cookies',
key2: 'example.com',
key3: '/path-to-this-cookie'
}, response.headers['set-cookie']);
}
var html = response.data;
/**
* An example of html parsing
*/
var doc = new DOMParser().parseFromString(html, 'text/html');
var sourceUrl = document.getElementsByTagName('a')[0].attributes[0].value;
});
//Create a new array of sources
let sources = [];
sources.push({
url: 'https://example.com',
/**
* [Optional]
* Http headers to be sent while resolving/streaming this source
*/
headers: {
'sample-header': 'sample-header-value'
},
/**
*
* Required only when returning cached links inside
* createBundle() method.
* In other cases this field is automatically
* replaced by the actual source provider name.
*/
providerName: "SampleSourceProvider",
/**
* [Optional]
* File/torrent name.
*/
name: 'name-of-the-file.mkv',
/**
* [Optional]
* Size of the file/torrent
*/
sizeInBytes: 500000,
/**
* This source will appear as premium
* Default is true
*/
premium: true,
/**
* [Optional]
* If this is a direct link that does not need resolving
* Setting this to true, automatically sets premium=true
*/
resolved: true,
/**
* [Optional]
* Name of the host of this source (only used when resolved = true)
*/
host: 'SampleHost',
/**
* [Optional]
* If this source is subbed
*/
subbed: false,
/**
* [Optional]
* If this source is dubbed
*/
dubbed: false,
//[Optional]
subtitles: [
{
url: 'https://example.com',
//[Optional]
lang: 'en'
}
],
/**
* [Optional]
* or hd,720p,1080p,4k, cam or any standard quality names
*/
quality: 'hd',
//[Optional]
//Height of the video
height: 1080,
//[Optional]
//Width of the video
width: 1920,
//[Optional]
//Only for torrents
seeders: 50,
//[Optional]
//Only for torrents
peers: 50,
/**
* [Optional]
* Android app (identified by this package id) to launch
*/
androidPackageName: 'your.app.package.id',
/**
* [Optional]
* Uri to be sent to the app
* Uri will be launched by androidPackageName (when provided)
* or passed to system to pick correct app to launch
*/
androidPackageData: 'https://example.com'
});
//Resolve this promise with sources found
resolve(sources);
});
}
}
exports.SampleProviderPackage = SampleProviderPackage;
Object.defineProperty(exports, '__esModule', { value: true });
})));