feat: add StremThru Store support

closes #123
This commit is contained in:
Viren070
2025-04-28 20:32:47 +01:00
parent 67fab12909
commit 8651f2bbc4
5 changed files with 262 additions and 0 deletions
+9
View File
@@ -11,6 +11,7 @@ import {
getOrionStreams,
getPeerflixStreams,
getStremioJackettStreams,
getStremThruStoreStreams,
getTorboxStreams,
getTorrentioStreams,
} from '@aiostreams/wrappers';
@@ -1080,6 +1081,14 @@ export class AIOStreams {
addonId
);
}
case 'stremthru-store': {
return await getStremThruStoreStreams(
this.config,
addon.options,
streamRequest,
addonId
);
}
case 'dmm-cast': {
return await getDMMCastStreams(
this.config,
+55
View File
@@ -687,6 +687,61 @@ export const addonDetails: AddonDetail[] = [
},
],
},
{
name: 'StremThru Store',
id: 'stremthru-store',
requiresService: true,
supportedServices: [
'torbox',
'easydebrid',
'realdebrid',
'debridlink',
'alldebrid',
'premiumize',
'offcloud',
'pikpak',
],
options: [
{
id: 'prioritiseDebrid',
required: false,
label: 'Prioritise Debrid Service',
description:
'Prioritise a specific debrid service when fetching streams. This option is useful when you want to use a specific debrid service for fetching streams. By default, the addon will make a separate request for each debrid service. I highly recommend provding a value for this option as it will speed up the fetching process and remove redundant results.',
type: 'select',
options: [
{ value: 'torbox', label: 'Torbox' },
{ value: 'easydebrid', label: 'EasyDebrid' },
{ value: 'realdebrid', label: 'Real Debrid' },
{ value: 'debridlink', label: 'Debrid Link' },
{ value: 'alldebrid', label: 'All Debrid' },
{ value: 'premiumize', label: 'Premiumize' },
{ value: 'offcloud', label: 'Offcloud' },
{ value: 'pikpak', label: 'PikPak' },
],
},
{
id: 'overrideName',
required: false,
label: 'Override Addon Name',
description:
"Override the name of the addon that shows up in the results. Leave it empty to use the default name of 'StremThru Store'.",
type: 'text',
},
{
id: 'indexerTimeout',
required: false,
label: 'Override Indexer Timeout',
description:
'The timeout for fetching streams from the StremThru Store addon in milliseconds. This is the time in milliseconds that the addon will wait for a response from StremThru Store before timing out. Leave it empty to use the recommended timeout.',
type: 'number',
constraints: {
min: Settings.MIN_TIMEOUT,
max: Settings.MAX_TIMEOUT,
},
},
],
},
{
name: 'DMM Cast',
id: 'dmm-cast',
+8
View File
@@ -237,6 +237,14 @@ export class Settings {
? parseInt(process.env.DEFAULT_DEBRIDIO_TIMEOUT)
: undefined;
public static readonly STREMTHRU_STORE_URL =
process.env.STREMTHRU_STORE_URL ||
'https://stremthru.elfhosted.com/stremio/store/';
public static readonly DEFAULT_STREMTHRU_STORE_TIMEOUT = process.env
.DEFAULT_STREMTHRU_STORE_TIMEOUT
? parseInt(process.env.DEFAULT_STREMTHRU_STORE_TIMEOUT)
: undefined;
public static readonly DEFAULT_DMM_CAST_TIMEOUT = process.env
.DEFAULT_DMM_CAST_TIMEOUT
? parseInt(process.env.DEFAULT_DMM_CAST_TIMEOUT)
+1
View File
@@ -12,3 +12,4 @@ export * from './orion';
export * from './peerflix';
export * from './dmmCast';
export * from './stremio-jackett';
export * from './stremthruStore';
+189
View File
@@ -0,0 +1,189 @@
import { AddonDetail, ParseResult, StreamRequest } from '@aiostreams/types';
import { ParsedStream, Config } from '@aiostreams/types';
import { BaseWrapper } from './base';
import { addonDetails, createLogger } from '@aiostreams/utils';
import { Settings } from '@aiostreams/utils';
import { Stream } from 'stream';
const logger = createLogger('wrappers');
export class StremThruStore extends BaseWrapper {
constructor(
configString: string | null,
overrideUrl: string | null,
addonName: string = 'ST Store',
addonId: string,
userConfig: Config,
indexerTimeout?: number
) {
let url = overrideUrl
? overrideUrl
: Settings.STREMTHRU_STORE_URL + (configString ? configString + '/' : '');
super(
addonName,
url,
addonId,
userConfig,
indexerTimeout || Settings.DEFAULT_STREMTHRU_STORE_TIMEOUT
);
}
protected parseStream(stream: { [key: string]: string }): ParseResult {
const parsedResult = super.parseStream(stream);
if (parsedResult.type === 'stream' && parsedResult.result.provider?.id) {
parsedResult.result.provider = {
...parsedResult.result.provider,
cached: true,
};
// all st store results are "personal" streams.
parsedResult.result.personal = true;
// ST store results use a cogwheel emoji (⚙️) for the release group, this is mistakenly identified as an indexer.
// remove it (personal results don't have an indexer anyway)
parsedResult.result.indexers = undefined;
}
return parsedResult;
}
}
export async function getStremThruStoreStreams(
config: Config,
stremthruStoreOptions: {
prioritiseDebrid?: string;
overrideUrl?: string;
indexerTimeout?: string;
overrideName?: string;
},
streamRequest: StreamRequest,
addonId: string
): Promise<{ addonStreams: ParsedStream[]; addonErrors: string[] }> {
const supportedServices: string[] =
addonDetails.find((addon: AddonDetail) => addon.id === 'stremthru-store')
?.supportedServices || [];
const parsedStreams: ParsedStream[] = [];
const indexerTimeout = stremthruStoreOptions.indexerTimeout
? parseInt(stremthruStoreOptions.indexerTimeout)
: undefined;
// If overrideUrl is provided, use it to get streams and skip all other steps
if (stremthruStoreOptions.overrideUrl) {
const stremthruStore = new StremThruStore(
null,
stremthruStoreOptions.overrideUrl as string,
stremthruStoreOptions.overrideName,
addonId,
config,
indexerTimeout
);
return await stremthruStore.getParsedStreams(streamRequest);
}
// find all usable and enabled services
const usableServices = config.services.filter(
(service) => supportedServices.includes(service.id) && service.enabled
);
// if no usable services found, raise error
if (usableServices.length < 1) {
throw new Error('No supported service(s) enabled');
}
// otherwise, depending on the configuration, create multiple instances of StremThru Store or use a single instance with the prioritised service
if (
stremthruStoreOptions.prioritiseDebrid &&
!supportedServices.includes(stremthruStoreOptions.prioritiseDebrid)
) {
throw new Error('Invalid debrid service');
}
const formServiceCredentialsString = (
service: string,
credentials: { [key: string]: string }
) => {
if (service === 'pikpak' || service === 'offcloud') {
if (!credentials.email || !credentials.password) {
throw new Error(
`Credentials for ${service} are not valid. Please check your configuration. Email and password are required.`
);
}
return `${credentials.email}:${credentials.password}`;
}
if (!credentials.apiKey) {
throw new Error(`API Key is missing for ${service}`);
}
return credentials.apiKey;
};
if (stremthruStoreOptions.prioritiseDebrid) {
const debridService = usableServices.find(
(service) => service.id === stremthruStoreOptions.prioritiseDebrid
);
if (!debridService) {
throw new Error(
'Debrid service not found for ' + stremthruStoreOptions.prioritiseDebrid
);
}
const storeToken = formServiceCredentialsString(
debridService.id,
debridService.credentials
);
const stremthruStore = new StremThruStore(
getConfigString(stremthruStoreOptions.prioritiseDebrid, storeToken),
null,
stremthruStoreOptions.overrideName,
addonId,
config,
indexerTimeout
);
return await stremthruStore.getParsedStreams(streamRequest);
}
// if no prioritised service is provided, create a stremthru instance for each service
const servicesToUse = usableServices.filter((service) => service.enabled);
if (servicesToUse.length < 1) {
throw new Error('No supported service(s) enabled');
}
const errorMessages: string[] = [];
const streamPromises = servicesToUse.map(async (service) => {
logger.info(`Getting StremThru Store streams for ${service.id}`, {
func: 'stremthru-store',
});
const stremthruStore = new StremThruStore(
getConfigString(
service.id,
formServiceCredentialsString(service.id, service.credentials)
),
null,
stremthruStoreOptions.overrideName,
addonId,
config,
indexerTimeout
);
return stremthruStore.getParsedStreams(streamRequest);
});
const results = await Promise.allSettled(streamPromises);
results.forEach((result) => {
if (result.status === 'fulfilled') {
const streams = result.value;
parsedStreams.push(...streams.addonStreams);
errorMessages.push(...streams.addonErrors);
} else {
errorMessages.push(result.reason.message);
}
});
return { addonStreams: parsedStreams, addonErrors: errorMessages };
}
function getConfigString(storeName: string, storeToken: string) {
return Buffer.from(
JSON.stringify({
store_name: storeName,
store_token: storeToken,
})
).toString('base64');
}