feat: drop stremio-addon-sdk
Instead of using the inflexible SDK, we create manifest, routes and the configure page ourselves using express. This also makes the plugin re-configurable. Additionally, a `CONFIGURATION_DESCRIPTION` env var was created to be able to customize the description on the configuration page.
This commit is contained in:
parent
34fc1f7b43
commit
4c7ad874ff
12 changed files with 589 additions and 1046 deletions
|
|
@ -6,8 +6,7 @@ const config: Config = {
|
|||
'./src/**/*.ts',
|
||||
'!./src/**/index.ts',
|
||||
'!./src/**/types.ts',
|
||||
'!./src/addon.ts',
|
||||
'!./src/server.ts',
|
||||
'!./src/landingTemplate.ts',
|
||||
],
|
||||
coverageDirectory: 'coverage',
|
||||
coverageProvider: 'babel',
|
||||
|
|
|
|||
1032
package-lock.json
generated
1032
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -6,10 +6,10 @@
|
|||
"scripts": {
|
||||
"analyse": "tsc --noEmit",
|
||||
"build": "rm -rf ./dist && tsc",
|
||||
"dev": "nodemon src/server.ts",
|
||||
"dev": "nodemon src/index.ts",
|
||||
"lint": "eslint",
|
||||
"lint:fix": "eslint --fix",
|
||||
"start": "node dist/server.js",
|
||||
"start": "node dist/index.js",
|
||||
"test": "jest",
|
||||
"ci": "npm run lint && npm run analyse && npm run test"
|
||||
},
|
||||
|
|
@ -31,14 +31,15 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"cheerio": "^1.0.0",
|
||||
"express": "^5.1.0",
|
||||
"make-fetch-happen": "^14.0.3",
|
||||
"slugify": "^1.6.6",
|
||||
"stremio-addon-sdk": "^1.6.10",
|
||||
"unpacker": "^1.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.26.0",
|
||||
"@stylistic/eslint-plugin": "^4.2.0",
|
||||
"@types/express": "^5.0.1",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/make-fetch-happen": "^10.0.4",
|
||||
"@types/node": "^22.15.3",
|
||||
|
|
|
|||
|
|
@ -1,13 +0,0 @@
|
|||
import addon from './addon';
|
||||
|
||||
describe('addon', () => {
|
||||
test('manifest can be retrieved with defaults', () => {
|
||||
expect(addon.manifest).toStrictEqual(
|
||||
expect.objectContaining({
|
||||
id: 'webstreamr',
|
||||
name: 'WebStreamr',
|
||||
description: 'Provides HTTP URLs from streaming websites.',
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
90
src/addon.ts
90
src/addon.ts
|
|
@ -1,90 +0,0 @@
|
|||
import { ContentType, Manifest, addonBuilder } from 'stremio-addon-sdk';
|
||||
import { Handler, HandlerStream, KinoKiste, MeineCloud } from './handler';
|
||||
import { fulfillAllPromises, iso2ToFlag, logInfo } from './utils';
|
||||
|
||||
const handlers: Handler[] = [
|
||||
new KinoKiste(),
|
||||
new MeineCloud(),
|
||||
];
|
||||
|
||||
const manifest: Manifest = {
|
||||
id: process.env['MANIFEST_ID'] || 'webstreamr',
|
||||
version: '0.3.0', // x-release-please-version
|
||||
name: process.env['MANIFEST_NAME'] || 'WebStreamr',
|
||||
description: process.env['MANIFEST_DESCRIPTION'] || 'Provides HTTP URLs from streaming websites.',
|
||||
resources: [
|
||||
'stream',
|
||||
],
|
||||
types: [
|
||||
'movie',
|
||||
'series',
|
||||
],
|
||||
catalogs: [],
|
||||
idPrefixes: ['tt'],
|
||||
behaviorHints: {
|
||||
p2p: false,
|
||||
configurable: true,
|
||||
configurationRequired: true,
|
||||
},
|
||||
config: [],
|
||||
};
|
||||
|
||||
handlers.forEach((handler) => {
|
||||
manifest.config?.push({
|
||||
key: handler.id,
|
||||
type: 'checkbox',
|
||||
title: `${handler.languages.map(language => iso2ToFlag(language) + ' ' + language.toUpperCase()).join(', ')} | ${handler.label}`,
|
||||
});
|
||||
});
|
||||
|
||||
const builder = new addonBuilder(manifest);
|
||||
|
||||
type RequestConfig = Record<string, boolean | string | number>;
|
||||
|
||||
const sortStreams = (streams: HandlerStream[]): void => {
|
||||
streams.sort((a, b) => {
|
||||
const resolutionComparison = parseInt(b.resolution ?? '0') - parseInt(a.resolution ?? '0');
|
||||
if (resolutionComparison !== 0) {
|
||||
return resolutionComparison;
|
||||
}
|
||||
|
||||
return parseFloat(b.size ?? '0') - parseFloat(a.size ?? '0');
|
||||
});
|
||||
};
|
||||
|
||||
builder.defineStreamHandler(async (args: { type: ContentType; id: string; config?: RequestConfig }) => {
|
||||
logInfo(`Search stream for type "${args.type}" and id "${args.id}"`);
|
||||
|
||||
const selectedHandlers = handlers.filter(handler => handler.id in (args.config || {}));
|
||||
if (selectedHandlers.length === 0) {
|
||||
logInfo('No handlers configured, bail out');
|
||||
|
||||
return {
|
||||
streams: [{
|
||||
name: 'WebStreamr',
|
||||
title: '⚠️ No handlers configured. Please re-configure the plugin.',
|
||||
ytId: 'E4WlUXrJgy4',
|
||||
}],
|
||||
};
|
||||
}
|
||||
|
||||
const streams: HandlerStream[] = [];
|
||||
const handlerPromises = selectedHandlers.map(async (handler) => {
|
||||
if (!handler.contentTypes.includes(args.type)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handlerStreams = await handler.handle(args.id);
|
||||
logInfo(`${handler.id} returned ${handlerStreams.length} streams`);
|
||||
|
||||
streams.push(...handlerStreams);
|
||||
});
|
||||
await fulfillAllPromises(handlerPromises);
|
||||
|
||||
sortStreams(streams);
|
||||
|
||||
logInfo(`Return ${streams.length} streams`);
|
||||
return { streams };
|
||||
});
|
||||
|
||||
export default builder.getInterface();
|
||||
113
src/index.ts
Normal file
113
src/index.ts
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
import express, { NextFunction, Request, Response } from 'express';
|
||||
import { landingTemplate } from './landingTemplate';
|
||||
import { Handler, HandlerStream, KinoKiste, MeineCloud } from './handler';
|
||||
import { buildManifest, fulfillAllPromises, logInfo } from './utils';
|
||||
import { Config } from './types';
|
||||
|
||||
const addon = express();
|
||||
|
||||
const handlers: Handler[] = [
|
||||
new KinoKiste(),
|
||||
new MeineCloud(),
|
||||
];
|
||||
|
||||
addon.use((_req: Request, res: Response, next: NextFunction) => {
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
res.setHeader('Access-Control-Allow-Headers', '*');
|
||||
|
||||
if (process.env['NODE_ENV'] === 'production') {
|
||||
res.setHeader('Cache-Control', 'max-age=3600, public');
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
addon.get('/', (_req: Request, res: Response) => {
|
||||
res.redirect('/configure');
|
||||
});
|
||||
|
||||
addon.get('/configure', (_req: Request, res: Response) => {
|
||||
const manifest = buildManifest(handlers, {});
|
||||
|
||||
res.setHeader('content-type', 'text/html');
|
||||
res.send(landingTemplate(manifest));
|
||||
});
|
||||
|
||||
addon.get('/:config/configure', (req: Request, res: Response) => {
|
||||
const config: Config = JSON.parse(req.params['config'] || '{}');
|
||||
|
||||
const manifest = buildManifest(handlers, config);
|
||||
|
||||
res.setHeader('content-type', 'text/html');
|
||||
res.send(landingTemplate(manifest));
|
||||
});
|
||||
|
||||
addon.get('/manifest.json', (_req: Request, res: Response) => {
|
||||
const manifest = buildManifest(handlers, {});
|
||||
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.send(manifest);
|
||||
});
|
||||
|
||||
addon.get('/:config/manifest.json', (req: Request, res: Response) => {
|
||||
const config: Config = JSON.parse(req.params['config'] || '{}');
|
||||
|
||||
const manifest = buildManifest(handlers, config);
|
||||
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.send(manifest);
|
||||
});
|
||||
|
||||
addon.get('/:config/stream/:type/:id.json', async function (req: Request, res: Response) {
|
||||
const config: Config = JSON.parse(req.params['config'] || '{}');
|
||||
const type: string = req.params['type'] || '';
|
||||
const id: string = req.params['id'] || '';
|
||||
|
||||
logInfo(`Search stream for type "${type}" and id "${id}"`);
|
||||
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
|
||||
const selectedHandlers = handlers.filter(handler => handler.id in config);
|
||||
if (selectedHandlers.length === 0) {
|
||||
logInfo('No handlers configured, bail out');
|
||||
|
||||
res.send(JSON.stringify({
|
||||
streams: [{
|
||||
name: 'WebStreamr',
|
||||
title: '⚠️ No handlers found. Please re-configure the plugin.',
|
||||
ytId: 'E4WlUXrJgy4',
|
||||
}],
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
const streams: HandlerStream[] = [];
|
||||
const handlerPromises = selectedHandlers.map(async (handler) => {
|
||||
if (!handler.contentTypes.includes(type)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handlerStreams = await handler.handle(id);
|
||||
logInfo(`${handler.id} returned ${handlerStreams.length} streams`);
|
||||
|
||||
streams.push(...handlerStreams);
|
||||
});
|
||||
await fulfillAllPromises(handlerPromises);
|
||||
|
||||
streams.sort((a, b) => {
|
||||
const resolutionComparison = parseInt(b.resolution ?? '0') - parseInt(a.resolution ?? '0');
|
||||
if (resolutionComparison !== 0) {
|
||||
return resolutionComparison;
|
||||
}
|
||||
|
||||
return parseFloat(b.size ?? '0') - parseFloat(a.size ?? '0');
|
||||
});
|
||||
|
||||
logInfo(`Return ${streams.length} streams`);
|
||||
res.send(JSON.stringify({ streams }));
|
||||
});
|
||||
|
||||
const port = parseInt(process.env['PORT'] || '51546');
|
||||
addon.listen(port, () => {
|
||||
logInfo(`Add-on Repository URL: http://127.0.0.1:${port}/manifest.json`);
|
||||
});
|
||||
300
src/landingTemplate.ts
Normal file
300
src/landingTemplate.ts
Normal file
|
|
@ -0,0 +1,300 @@
|
|||
// Adapted version of https://github.com/Stremio/stremio-addon-sdk/blob/v1.6.2/src/landingTemplate.js
|
||||
import { ManifestWithConfig } from './types';
|
||||
|
||||
const STYLESHEET = `
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body,
|
||||
html {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
padding: 2vh;
|
||||
font-size: 2.2vh;
|
||||
}
|
||||
|
||||
html {
|
||||
background-size: auto 100%;
|
||||
background-size: cover;
|
||||
background-position: center center;
|
||||
background-repeat: no-repeat;
|
||||
box-shadow: inset 0 0 0 2000px rgb(0 0 0 / 60%);
|
||||
}
|
||||
|
||||
body {
|
||||
display: flex;
|
||||
font-family: 'Open Sans', Arial, sans-serif;
|
||||
color: white;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 4.5vh;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 2.2vh;
|
||||
font-weight: normal;
|
||||
font-style: italic;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 2.2vh;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
p {
|
||||
margin: 0;
|
||||
text-shadow: 0 0 1vh rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 1.75vh;
|
||||
}
|
||||
|
||||
ul {
|
||||
font-size: 1.75vh;
|
||||
margin: 0;
|
||||
margin-top: 1vh;
|
||||
padding-left: 3vh;
|
||||
}
|
||||
|
||||
a {
|
||||
color: white
|
||||
}
|
||||
|
||||
a.install-link {
|
||||
text-decoration: none
|
||||
}
|
||||
|
||||
button {
|
||||
border: 0;
|
||||
outline: 0;
|
||||
color: white;
|
||||
background: #8A5AAB;
|
||||
padding: 1.2vh 3.5vh;
|
||||
margin: auto;
|
||||
text-align: center;
|
||||
font-family: 'Open Sans', Arial, sans-serif;
|
||||
font-size: 2.2vh;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
box-shadow: 0 0.5vh 1vh rgba(0, 0, 0, 0.2);
|
||||
transition: box-shadow 0.1s ease-in-out;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
button:active {
|
||||
box-shadow: 0 0 0 0.5vh white inset;
|
||||
}
|
||||
|
||||
#addon {
|
||||
width: 40vh;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 14vh;
|
||||
width: 14vh;
|
||||
margin: auto;
|
||||
margin-bottom: 3vh;
|
||||
}
|
||||
|
||||
.logo img {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.name, .version {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.name {
|
||||
line-height: 5vh;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.version {
|
||||
position: relative;
|
||||
line-height: 5vh;
|
||||
opacity: 0.8;
|
||||
margin-bottom: 2vh;
|
||||
}
|
||||
|
||||
.contact {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 4vh;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.contact a {
|
||||
font-size: 1.4vh;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.separator {
|
||||
margin-bottom: 4vh;
|
||||
}
|
||||
|
||||
.form-element {
|
||||
margin-bottom: 2vh;
|
||||
}
|
||||
|
||||
.label-to-top {
|
||||
margin-bottom: 2vh;
|
||||
}
|
||||
|
||||
.label-to-right {
|
||||
margin-left: 1vh !important;
|
||||
}
|
||||
|
||||
.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
|
||||
export function landingTemplate(manifest: ManifestWithConfig) {
|
||||
const background = manifest.background || 'https://dl.strem.io/addon-background.jpg';
|
||||
const logo = manifest.logo || 'https://dl.strem.io/addon-logo.png';
|
||||
const contactHTML = manifest.contactEmail
|
||||
? `<div class="contact">
|
||||
<p>Contact ${manifest.name} creator:</p>
|
||||
<a href="mailto:${manifest.contactEmail}">${manifest.contactEmail}</a>
|
||||
</div>`
|
||||
: '';
|
||||
|
||||
const stylizedTypes = manifest.types
|
||||
.map(types => types.charAt(0).toUpperCase() + types.slice(1) + (types !== 'series' ? 's' : ''));
|
||||
|
||||
let formHTML = '';
|
||||
let script = '';
|
||||
|
||||
if ((manifest.config || []).length) {
|
||||
let options = '';
|
||||
manifest.config.forEach((elem) => {
|
||||
const key = elem.key;
|
||||
if (['text', 'number', 'password'].includes(elem.type)) {
|
||||
const isRequired = elem.required ? ' required' : '';
|
||||
const defaultHTML = elem.default ? ` value="${elem.default}"` : '';
|
||||
const inputType = elem.type;
|
||||
options += `
|
||||
<div class="form-element">
|
||||
<div class="label-to-top">${elem.title}</div>
|
||||
<input type="${inputType}" id="${key}" name="${key}" class="full-width"${defaultHTML}${isRequired}/>
|
||||
</div>
|
||||
`;
|
||||
} else if (elem.type === 'checkbox') {
|
||||
const isChecked = elem.default === 'checked' ? ' checked' : '';
|
||||
options += `
|
||||
<div class="form-element">
|
||||
<label for="${key}">
|
||||
<input type="checkbox" id="${key}" name="${key}"${isChecked}> <span class="label-to-right">${elem.title}</span>
|
||||
</label>
|
||||
</div>
|
||||
`;
|
||||
} else if (elem.type === 'select') {
|
||||
const defaultValue = elem.default || (elem.options || [])[0];
|
||||
options += `<div class="form-element">
|
||||
<div class="label-to-top">${elem.title}</div>
|
||||
<select id="${key}" name="${key}" class="full-width">
|
||||
`;
|
||||
const selections = elem.options || [];
|
||||
selections.forEach((el) => {
|
||||
const isSelected = el === defaultValue ? ' selected' : '';
|
||||
options += `<option value="${el}"${isSelected}>${el}</option>`;
|
||||
});
|
||||
options += `</select>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
});
|
||||
if (options.length) {
|
||||
formHTML = `
|
||||
<form class="pure-form" id="mainForm">
|
||||
${options}
|
||||
</form>
|
||||
|
||||
<div class="separator"></div>
|
||||
`;
|
||||
script += `
|
||||
installLink.onclick = () => {
|
||||
return mainForm.reportValidity()
|
||||
}
|
||||
const updateLink = () => {
|
||||
const config = Object.fromEntries(new FormData(mainForm))
|
||||
installLink.href = 'stremio://' + window.location.host + '/' + encodeURIComponent(JSON.stringify(config)) + '/manifest.json'
|
||||
}
|
||||
mainForm.onchange = updateLink
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html style="background-image: url(${background});" lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>${manifest.name} - Stremio Addon</title>
|
||||
<style>${STYLESHEET}</style>
|
||||
<link rel="shortcut icon" href="${logo}" type="image/x-icon">
|
||||
<link href="https://fonts.googleapis.com/css?family=Open+Sans:400,600,700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/purecss@2.1.0/build/pure-min.css" integrity="sha384-yHIFVG6ClnONEA5yB5DJXfW2/KC173DIQrYoZMEtBvGzmf0PKiGyNEqe9N6BNDBH" crossorigin="anonymous">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="addon">
|
||||
<div class="logo">
|
||||
<img src="${logo}" alt="logo">
|
||||
</div>
|
||||
<h1 class="name">${manifest.name}</h1>
|
||||
<h2 class="version">v${manifest.version || '0.0.0'}</h2>
|
||||
<h2 class="description">${manifest.description || ''}</h2>
|
||||
|
||||
<div class="separator"></div>
|
||||
|
||||
${process.env['CONFIGURATION_DESCRIPTION'] || ''}
|
||||
|
||||
<div class="separator"></div>
|
||||
|
||||
<h3 class="gives">This addon has more :</h3>
|
||||
<ul>
|
||||
${stylizedTypes.map(t => `<li>${t}</li>`).join('')}
|
||||
</ul>
|
||||
|
||||
<div class="separator"></div>
|
||||
|
||||
${formHTML}
|
||||
|
||||
<a id="installLink" class="install-link" href="#">
|
||||
<button name="Install">INSTALL</button>
|
||||
</a>
|
||||
${contactHTML}
|
||||
</div>
|
||||
<script>
|
||||
${script}
|
||||
|
||||
if (typeof updateLink === 'function')
|
||||
updateLink()
|
||||
else
|
||||
installLink.href = 'stremio://' + window.location.host + '/manifest.json'
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>`;
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
import { serveHTTP } from 'stremio-addon-sdk';
|
||||
import addonInterface from './addon';
|
||||
|
||||
serveHTTP(addonInterface, {
|
||||
cacheMaxAge: process.env['NODE_ENV'] === 'production' ? 3600 : undefined,
|
||||
port: parseInt(process.env['PORT'] || '51546'),
|
||||
});
|
||||
5
src/types.ts
Normal file
5
src/types.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { Manifest, ManifestConfig } from 'stremio-addon-sdk';
|
||||
|
||||
export type ManifestWithConfig = Manifest & { config: ManifestConfig[] };
|
||||
|
||||
export type Config = Record<string, string>;
|
||||
|
|
@ -3,4 +3,5 @@ export * from './fetch';
|
|||
export * from './imdb';
|
||||
export * from './language';
|
||||
export * from './log';
|
||||
export * from './manifest';
|
||||
export * from './promise';
|
||||
|
|
|
|||
25
src/utils/manifest.test.ts
Normal file
25
src/utils/manifest.test.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import { buildManifest } from './manifest';
|
||||
import { KinoKiste } from '../handler';
|
||||
|
||||
describe('buildManifest', () => {
|
||||
test('has an empty config without handlers', () => {
|
||||
const manifest = buildManifest([], {});
|
||||
|
||||
expect(manifest.config).toStrictEqual([]);
|
||||
});
|
||||
|
||||
test('has unchecked handler without a config', () => {
|
||||
const manifest = buildManifest([new KinoKiste()], {});
|
||||
|
||||
expect(manifest.config).toHaveLength(1);
|
||||
expect(manifest.config[0]?.default).toBeUndefined();
|
||||
});
|
||||
|
||||
test('has checked handler with appropriate config', () => {
|
||||
const kinokiste = new KinoKiste();
|
||||
const manifest = buildManifest([kinokiste], { [kinokiste.id]: 'on' });
|
||||
|
||||
expect(manifest.config).toHaveLength(1);
|
||||
expect(manifest.config[0]?.default).toBe('checked');
|
||||
});
|
||||
});
|
||||
38
src/utils/manifest.ts
Normal file
38
src/utils/manifest.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import { iso2ToFlag } from './language';
|
||||
import { Handler } from '../handler';
|
||||
import { Config, ManifestWithConfig } from '../types';
|
||||
|
||||
export const buildManifest = (handlers: Handler[], config: Config): ManifestWithConfig => {
|
||||
const manifest: ManifestWithConfig = {
|
||||
id: process.env['MANIFEST_ID'] || 'webstreamr',
|
||||
version: '0.3.0', // x-release-please-version
|
||||
name: process.env['MANIFEST_NAME'] || 'WebStreamr',
|
||||
description: 'Provides HTTP URLs from streaming websites.',
|
||||
resources: [
|
||||
'stream',
|
||||
],
|
||||
types: [
|
||||
'movie',
|
||||
'series',
|
||||
],
|
||||
catalogs: [],
|
||||
idPrefixes: ['tt'],
|
||||
behaviorHints: {
|
||||
p2p: false,
|
||||
configurable: true,
|
||||
configurationRequired: Object.keys(config).length === 0,
|
||||
},
|
||||
config: [],
|
||||
};
|
||||
|
||||
handlers.forEach((handler) => {
|
||||
manifest.config.push({
|
||||
key: handler.id,
|
||||
type: 'checkbox',
|
||||
title: `${handler.languages.map(language => iso2ToFlag(language) + ' ' + language.toUpperCase()).join(', ')} | ${handler.label}`,
|
||||
...(handler.id in config && { default: 'checked' }),
|
||||
});
|
||||
});
|
||||
|
||||
return manifest;
|
||||
};
|
||||
Loading…
Reference in a new issue