From 5fc4cea2ea61835ea24ed8644b226796af4ee012 Mon Sep 17 00:00:00 2001 From: Viren070 Date: Wed, 13 Aug 2025 15:29:16 +0100 Subject: [PATCH] feat: add AStream closes #298 --- packages/core/src/presets/aStream.ts | 183 +++++++++++++++++++++ packages/core/src/presets/presetManager.ts | 4 + packages/core/src/utils/env.ts | 13 ++ 3 files changed, 200 insertions(+) create mode 100644 packages/core/src/presets/aStream.ts diff --git a/packages/core/src/presets/aStream.ts b/packages/core/src/presets/aStream.ts new file mode 100644 index 00000000..0ce077bf --- /dev/null +++ b/packages/core/src/presets/aStream.ts @@ -0,0 +1,183 @@ +import { + Addon, + Option, + ParsedStream, + ParsedFile, + Stream, + UserData, +} from '../db'; +import { Preset, baseOptions } from './preset'; +import { constants, Env, HTTP_STREAM_TYPE, LIVE_STREAM_TYPE } from '../utils'; +import { FileParser, StreamParser } from '../parser'; + +class AStreamStreamParser extends StreamParser { + protected override getParsedFile( + stream: Stream, + parsedStream: ParsedStream + ): ParsedFile | undefined { + const parsed = + typeof stream.language === 'string' + ? FileParser.parse(stream.language) + : undefined; + if (!parsed) { + return undefined; + } + return { + ...parsed, + title: undefined, + }; + } + protected override getIndexer( + stream: Stream, + currentParsedStream: ParsedStream + ): string | undefined { + const indexer = super.getIndexer(stream, currentParsedStream); + if (indexer && typeof indexer === 'string') { + try { + return new URL(indexer).hostname; + } catch {} + } + return indexer; + } + protected override getFilename( + stream: Stream, + currentParsedStream: ParsedStream + ): string | undefined { + return undefined; + } + protected override getMessage( + stream: Stream, + currentParsedStream: ParsedStream + ): string | undefined { + if (stream.language === 'vostfr') return 'VOSTFR'; + } + protected getStreamType( + stream: Stream, + service: ParsedStream['service'], + currentParsedStream: ParsedStream + ): ParsedStream['type'] { + return constants.HTTP_STREAM_TYPE; + } +} + +export class AStreamPreset extends Preset { + static override getParser(): typeof StreamParser { + return AStreamStreamParser; + } + + static override get METADATA() { + const supportedResources = [ + constants.CATALOG_RESOURCE, + constants.META_RESOURCE, + constants.STREAM_RESOURCE, + ]; + + const options: Option[] = [ + ...baseOptions( + 'AStream', + supportedResources, + Env.DEFAULT_ASTREAM_TIMEOUT + ), + { + id: 'language', + name: 'Language', + description: 'Language of the streams', + type: 'select', + options: [ + { label: 'Tout / Both', value: 'Tout' }, + { label: 'VOSTFR / French Subtitles Only', value: 'VOSTFR' }, + { label: 'VF / French Dub Only', value: 'VF' }, + ], + default: 'Tout', + }, + { + id: 'tmdbEnabled', + name: 'Activate TMDB Metadata (Recommended)', + description: + 'Replaces images and descriptions with TMDB images (Requires a TMDB API Key to be provided in the services menu)', + type: 'boolean', + default: true, + }, + { + id: 'tmdbEpisodeMapping', + name: 'TMDB Episode Mapping (Recommended)', + description: + 'Use TMDB titles and descriptions instead of AStream ones (Requires a TMDB API Key to be provided in the services menu)', + type: 'boolean', + default: true, + }, + ]; + + return { + ID: 'astream', + NAME: 'AStream', + LOGO: `https://raw.githubusercontent.com/Dydhzo/astream/refs/heads/main/astream/assets/astream-logo.jpg`, + URL: Env.ASTREAM_URL, + TIMEOUT: Env.DEFAULT_ASTREAM_TIMEOUT || Env.DEFAULT_TIMEOUT, + USER_AGENT: Env.DEFAULT_ASTREAM_USER_AGENT || Env.DEFAULT_USER_AGENT, + SUPPORTED_SERVICES: [], + DESCRIPTION: "Addon non officiel pour accéder au contenu d'Anime-Sama", + OPTIONS: options, + SUPPORTED_STREAM_TYPES: [HTTP_STREAM_TYPE], + SUPPORTED_RESOURCES: supportedResources, + }; + } + + static async generateAddons( + userData: UserData, + options: Record + ): Promise { + return [this.generateAddon(userData, options)]; + } + + private static generateAddon( + userData: UserData, + options: Record + ): Addon { + return { + name: options.name || this.METADATA.NAME, + manifestUrl: this.generateManifestUrl(userData, options), + enabled: true, + library: false, + resources: options.resources || this.METADATA.SUPPORTED_RESOURCES, + timeout: options.timeout || this.METADATA.TIMEOUT, + preset: { + id: '', + type: this.METADATA.ID, + options: options, + }, + headers: { + 'User-Agent': this.METADATA.USER_AGENT, + }, + }; + } + + private static generateManifestUrl( + userData: UserData, + options: Record + ): string { + if (options.url?.endsWith('/manifest.json')) { + return options.url; + } + const url = (options.url || this.METADATA.URL).replace(/\/$/, ''); + + if ( + (options.tmdbEnabled || options.tmdbEpisodeMapping) && + !userData.tmdbApiKey + ) { + throw new Error( + `${this.METADATA.NAME} requires a TMDB API Key to use TMDB features. Please provide it in the services menu. ` + ); + } + + const config = { + language: options.language || 'Tout', + languageOrder: 'VF,VOSTFR', + tmdbApiKey: userData.tmdbApiKey, + tmdbEnabled: options.tmdbEnabled, + tmdbEpisodeMapping: options.tmdbEpisodeMapping, + }; + + return `${url}/${this.base64EncodeJSON(config)}/manifest.json`; + } +} diff --git a/packages/core/src/presets/presetManager.ts b/packages/core/src/presets/presetManager.ts index 119ce947..cf8a441a 100644 --- a/packages/core/src/presets/presetManager.ts +++ b/packages/core/src/presets/presetManager.ts @@ -52,6 +52,7 @@ import { ContentDeepDivePreset } from './contentDeepDive'; import { AICompanionPreset } from './aiCompanion'; import { GoogleOAuth } from '../builtins/gdrive/api'; import { TorBoxSearchPreset } from './torboxSearch'; +import { AStreamPreset } from './aStream'; import { Env } from '../utils/env'; let PRESET_LIST: string[] = [ @@ -76,6 +77,7 @@ let PRESET_LIST: string[] = [ 'dmm-cast', 'nuvio-streams', 'webstreamr', + 'astream', 'streamasia', Env.BUILTIN_GDRIVE_CLIENT_ID && Env.BUILTIN_GDRIVE_CLIENT_SECRET ? 'stremio-gdrive' @@ -184,6 +186,8 @@ export class PresetManager { return NuvioStreamsPreset; case 'webstreamr': return WebStreamrPreset; + case 'astream': + return AStreamPreset; case 'streaming-catalogs': return StreamingCatalogsPreset; case 'anime-catalogs': diff --git a/packages/core/src/utils/env.ts b/packages/core/src/utils/env.ts index 310c8b33..ad448b6c 100644 --- a/packages/core/src/utils/env.ts +++ b/packages/core/src/utils/env.ts @@ -1383,6 +1383,19 @@ export const Env = cleanEnv(process.env, { desc: 'Default AI Companion user agent', }), + ASTREAM_URL: url({ + default: 'https://astream.stremiofr.com', + desc: 'AStream URL', + }), + DEFAULT_ASTREAM_TIMEOUT: num({ + default: undefined, + desc: 'Default AStream timeout', + }), + DEFAULT_ASTREAM_USER_AGENT: userAgent({ + default: undefined, + desc: 'Default AStream user agent', + }), + BUILTIN_STREMTHRU_URL: url({ default: 'https://stremthru.13377001.xyz', desc: 'Builtin StremThru URL',