From 2ddac6b0715330bd79ee40ef2b12e0328a121b9d Mon Sep 17 00:00:00 2001 From: Viren070 Date: Fri, 10 Jan 2025 11:43:22 +0000 Subject: [PATCH] feat: add easynews and easynews+ addons --- packages/formatters/src/utils.ts | 17 +++++ packages/parser/src/utils.ts | 12 ++++ packages/types/src/types.ts | 1 + packages/utils/src/details.ts | 72 +++++++++++++++++++ packages/utils/src/settings.ts | 4 ++ packages/wrappers/src/base.ts | 4 +- packages/wrappers/src/easynews.ts | 98 +++++++++++++++++++++++++ packages/wrappers/src/easynewsPlus.ts | 100 ++++++++++++++++++++++++++ 8 files changed, 307 insertions(+), 1 deletion(-) create mode 100644 packages/wrappers/src/easynews.ts create mode 100644 packages/wrappers/src/easynewsPlus.ts diff --git a/packages/formatters/src/utils.ts b/packages/formatters/src/utils.ts index 43759420..85c233a9 100644 --- a/packages/formatters/src/utils.ts +++ b/packages/formatters/src/utils.ts @@ -6,6 +6,23 @@ export function formatSize(bytes: number): string { return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; } +export function formatDuration(durationInMs: number): string { + const seconds = Math.floor(durationInMs / 1000); + const minutes = Math.floor(seconds / 60); + const hours = Math.floor(minutes / 60); + + const formattedSeconds = seconds % 60; + const formattedMinutes = minutes % 60; + + if (hours > 0) { + return `${hours}h:${formattedMinutes}m:${formattedSeconds}s`; + } else if (formattedSeconds > 0) { + return `${formattedMinutes}m:${formattedSeconds}s`; + } else { + return `${formattedMinutes}m`; + } +} + export function languageToEmoji(language: string): string | undefined { return languageEmojiMap[language.toLowerCase()]; } diff --git a/packages/parser/src/utils.ts b/packages/parser/src/utils.ts index b835c223..b8c14804 100644 --- a/packages/parser/src/utils.ts +++ b/packages/parser/src/utils.ts @@ -17,3 +17,15 @@ export function extractSizeInBytes(string: string, k: number): number { return 0; } } + +export function extractDurationInMs(string: string): number { + const durationPattern = /(?:(\d+)h:)?(?:(\d+)m:)?(\d+)s/; + const match = string.match(durationPattern); + if (!match) return 0; + + const hours = parseInt(match[1] || '0'); + const minutes = parseInt(match[2] || '0'); + const seconds = parseInt(match[3]); + + return (hours * 60 * 60 + minutes * 60 + seconds) * 1000; +} diff --git a/packages/types/src/types.ts b/packages/types/src/types.ts index 4c1e3cea..e243a7c8 100644 --- a/packages/types/src/types.ts +++ b/packages/types/src/types.ts @@ -29,6 +29,7 @@ export interface ParsedStream extends ParsedNameData { usenet?: { age?: string; }; + duration?: number; url?: string; externalUrl?: string; indexers?: string; diff --git a/packages/utils/src/details.ts b/packages/utils/src/details.ts index 7b333b21..0de3d36c 100644 --- a/packages/utils/src/details.ts +++ b/packages/utils/src/details.ts @@ -203,6 +203,78 @@ export const addonDetails: AddonDetail[] = [ }, ], }, + { + name: 'Easynews', + id: 'easynews', + requiresService: true, + supportedServices: ['easynews'], + options: [ + { + 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 'Easynews'.", + type: 'text', + }, + { + id: 'overrideUrl', + required: false, + label: 'Override URL', + description: + 'Override the URL used to fetch streams from the Easynews addon. By default, the URL is generated based on the username and password provided for the Easynews service. Use this option to override the URL with a custom URL.', + type: 'text', + }, + { + id: 'indexerTimeout', + required: false, + label: 'Override Indexer Timeout', + description: + 'The timeout for fetching streams from the Easynews addon in milliseconds. This is the time in milliseconds that the addon will wait for a response from Easynews before timing out. Leave it empty to use the recommended timeout.', + type: 'number', + constraints: { + min: Settings.MIN_TIMEOUT, + max: Settings.MAX_TIMEOUT, + }, + } + ] + }, + { + name: 'Easynews Plus', + id: 'easynewsplus', + requiresService: true, + supportedServices: ['easynews'], + options: [ + { + 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 'Easynews Plus'.", + type: 'text', + }, + { + id: 'overrideUrl', + required: false, + label: 'Override URL', + description: + 'Override the URL used to fetch streams from the Easynews Plus addon. By default, the URL is generated based on the username and password provided for the Easynews service. Use this option to override the URL with a custom URL.', + type: 'text', + }, + { + id: 'indexerTimeout', + required: false, + label: 'Override Indexer Timeout', + description: + 'The timeout for fetching streams from the Easynews Plus addon in milliseconds. This is the time in milliseconds that the addon will wait for a response from Easynews Plus before timing out. Leave it empty to use the recommended timeout.', + type: 'number', + constraints: { + min: Settings.MIN_TIMEOUT, + max: Settings.MAX_TIMEOUT, + }, + } + ] + }, { name: 'Stremio GDrive', id: 'gdrive', diff --git a/packages/utils/src/settings.ts b/packages/utils/src/settings.ts index 7242a5dc..cbf2955c 100644 --- a/packages/utils/src/settings.ts +++ b/packages/utils/src/settings.ts @@ -18,6 +18,8 @@ export class Settings { public static readonly MEDIAFUSION_URL = process.env.MEDIAFUSION_URL ?? 'https://mediafusion.elfhosted.com/'; public static readonly TORRENTIO_URL = process.env.TORRENTIO_URL ?? 'https://torrentio.strem.fun/'; public static readonly TORBOX_STREMIO_URL = process.env.TORBOX_STREMIO_URL ?? 'https://stremio.torbox.app/'; + public static readonly EASYNEWS_URL = process.env.EASYNEWS_URL ?? 'https://ea627ddf0ee7-easynews.baby-beamup.club/'; + public static readonly EASYNEWS_PLUS_URL = process.env.EASYNEWS_PLUS_URL ?? 'https://b89262c192b0-stremio-easynews-addon.baby-beamup.club/'; public static readonly MAX_ADDONS = process.env.MAX_ADDONS ? parseInt(process.env.MAX_ADDONS) : 15; public static readonly MAX_MOVIE_SIZE = process.env.MAX_MOVIE_SIZE ? parseInt(process.env.MAX_MOVIE_SIZE) : 150000000000; // 150GB public static readonly MAX_EPISODE_SIZE = process.env.MAX_EPISODE_SIZE ? parseInt(process.env.MAX_EPISODE_SIZE) : 15000000000; // 15GB @@ -28,6 +30,8 @@ export class Settings { public static readonly DEFAULT_TORBOX_TIMEOUT = process.env.DEFAULT_TORBOX_TIMEOUT ? parseInt(process.env.DEFAULT_TORBOX_TIMEOUT) : 15000; public static readonly DEFAULT_COMET_TIMEOUT = process.env.DEFAULT_COMET_TIMEOUT ? parseInt(process.env.DEFAULT_COMET_TIMEOUT) : 15000; public static readonly DEFAULT_MEDIAFUSION_TIMEOUT = process.env.DEFAULT_MEDIAFUSION_TIMEOUT ? parseInt(process.env.DEFAULT_MEDIAFUSION_TIMEOUT) : 15000; + public static readonly DEFAULT_EASYNEWS_TIMEMOUT = process.env.DEFAULT_EASYNEWS_TIMEMOUT ? parseInt(process.env.DEFAULT_EASYNEWS_TIMEMOUT) : 15000; + public static readonly DEFAULT_EASYNEWS_PLUS_TIMEMOUT = process.env.DEFAULT_EASYNEWS_PLUS_TIMEMOUT ? parseInt(process.env.DEFAULT_EASYNEWS_PLUS_TIMEMOUT) : 15000; public static readonly SHOW_DIE = process.env.SHOW_DIE ? process.env.SHOW_DIE === 'true' : true; } diff --git a/packages/wrappers/src/base.ts b/packages/wrappers/src/base.ts index 5a27e0f7..32957196 100644 --- a/packages/wrappers/src/base.ts +++ b/packages/wrappers/src/base.ts @@ -98,7 +98,8 @@ export class BaseWrapper { provider?: ParsedStream['provider'], seeders?: number, usenetAge?: string, - indexer?: string + indexer?: string, + duration?: number ): ParsedStream { return { ...parsedInfo, @@ -118,6 +119,7 @@ export class BaseWrapper { age: usenetAge, }, indexers: indexer, + duration: duration, stream: { subtitles: stream.subtitles, behaviorHints: { diff --git a/packages/wrappers/src/easynews.ts b/packages/wrappers/src/easynews.ts new file mode 100644 index 00000000..97b6ebc4 --- /dev/null +++ b/packages/wrappers/src/easynews.ts @@ -0,0 +1,98 @@ +import { AddonDetail, ParsedNameData, StreamRequest } from '@aiostreams/types'; +import { parseFilename, extractSizeInBytes, extractDurationInMs } from '@aiostreams/parser'; +import { ParsedStream, Stream, Config } from '@aiostreams/types'; +import { BaseWrapper } from './base'; +import { addonDetails, serviceDetails } from '@aiostreams/utils'; +import { Settings } from '@aiostreams/utils'; + + +export class Easynews extends BaseWrapper { + constructor( + configString: string | null, + overrideUrl: string | null, + indexerTimeout: number = Settings.DEFAULT_EASYNEWS_TIMEMOUT, + addonName: string = 'Easynews', + addonId: string, + ) { + let url = overrideUrl + ? overrideUrl + : Settings.EASYNEWS_URL + + (configString ? configString + '/' : ''); + + super(addonName, url, indexerTimeout, addonId); + } + + protected parseStream(stream: Stream): ParsedStream { + const [filename, sizeString, durationString] = stream.description?.split('\n') || []; + + const parsedFilename: ParsedNameData = parseFilename( + filename || stream.description || '' + ); + const sizeInBytes = stream.description + ? extractSizeInBytes(sizeString, 1024) + : undefined; + + const provider = { + id: 'easynews', + cached: true, + } + + const durationInMs = extractDurationInMs(durationString || ''); + + const parsedStream: ParsedStream = this.createParsedResult( + parsedFilename, + stream, + filename, + sizeInBytes, + provider, + undefined, + undefined, + undefined, + durationInMs + ); + return parsedStream; + } +} + +const getEasynewsConfigString = (username: string, password: string) => { + return `%7B%22username%22%3A%22${username}%22%2C%22password%22%3A%22${password}%22%7D` +}; + +export async function getEasynewsStreams( + config: Config, + easynewsOptions: { + overrideName?: string; + overrideUrl?: string; + indexerTimeout?: string; + }, + streamRequest: StreamRequest, + addonId: string, +): Promise { + + // look for the 'easynews' id in the services array and destructure the username and password + // if we cant find it, throw an error + const easynewsService = serviceDetails.find((service) => service.id === 'easynews'); + if (!easynewsService) { + throw new Error('Easynews service not found'); + } + + // check for the presence of the username and password in teh easynewsService.credentials object + // if not found, throw an error + const credentails = config.services.find((service) => service.id === 'easynews')?.credentials; + if (!credentails || !credentails.username || !credentails.password) { + throw new Error('Easynews credentials not found'); + } + + const easynewsConfigString = getEasynewsConfigString(credentails.username, credentails.password); + + const easynews = new Easynews( + easynewsConfigString, + easynewsOptions.overrideUrl ?? null, + easynewsOptions.indexerTimeout ? parseInt(easynewsOptions.indexerTimeout) : undefined, + easynewsOptions.overrideName, + addonId + ); + + const streams = await easynews.getParsedStreams(streamRequest); + return streams; +} diff --git a/packages/wrappers/src/easynewsPlus.ts b/packages/wrappers/src/easynewsPlus.ts new file mode 100644 index 00000000..a2e55162 --- /dev/null +++ b/packages/wrappers/src/easynewsPlus.ts @@ -0,0 +1,100 @@ +import { AddonDetail, ParsedNameData, StreamRequest } from '@aiostreams/types'; +import { parseFilename, extractSizeInBytes, extractDurationInMs } from '@aiostreams/parser'; +import { ParsedStream, Stream, Config } from '@aiostreams/types'; +import { BaseWrapper } from './base'; +import { addonDetails, serviceDetails } from '@aiostreams/utils'; +import { Settings } from '@aiostreams/utils'; + + +export class EasynewsPlus extends BaseWrapper { + constructor( + configString: string | null, + overrideUrl: string | null, + indexerTimeout: number = Settings.DEFAULT_EASYNEWS_PLUS_TIMEMOUT, + addonName: string = 'Easynews Plus', + addonId: string, + ) { + let url = overrideUrl + ? overrideUrl + : Settings.EASYNEWS_PLUS_URL + + (configString ? configString + '/' : ''); + + super(addonName, url, indexerTimeout, addonId); + } + + protected parseStream(stream: Stream): ParsedStream { + const [filename, durationString, sizeString] = stream.description?.split('\n') || []; + + const parsedFilename: ParsedNameData = parseFilename( + filename || stream.description || '' + ); + const sizeInBytes = stream.behaviorHints?.videoSize + ? stream.behaviorHints.videoSize + : sizeString + ? extractSizeInBytes(sizeString, 1024) + : undefined; + + const provider = { + id: 'easynews', + cached: true, + } + + const durationInMs = extractDurationInMs(durationString || ''); + + const parsedStream: ParsedStream = this.createParsedResult( + parsedFilename, + stream, + filename, + sizeInBytes, + provider, + undefined, + undefined, + undefined, + durationInMs + ); + return parsedStream; + } +} + +const getEasynewsPlusConfigString = (username: string, password: string) => { + return `%7B%22username%22%3A%22${username}%22%2C%22password%22%3A%22${password}%22%2C%22sort1%22%3A%22Size%22%2C%22sort1Direction%22%3A%22Descending%22%2C%22sort2%22%3A%22Relevance%22%2C%22sort2Direction%22%3A%22Descending%22%2C%22sort3%22%3A%22Date%20%26%20Time%22%2C%22sort3Direction%22%3A%22Descending%22%7D` +}; + +export async function getEasynewsPlusStreams( + config: Config, + easynewsPlusOptions: { + overrideName?: string; + overrideUrl?: string; + indexerTimeout?: string; + }, + streamRequest: StreamRequest, + addonId: string, +): Promise { + + // look for the 'easynews' id in the services array and destructure the username and password + // if we cant find it, throw an error + const easynewsService = serviceDetails.find((service) => service.id === 'easynews'); + if (!easynewsService) { + throw new Error('Easynews service not found'); + } + + // check for the presence of the username and password in teh easynewsService.credentials object + // if not found, throw an error + const credentails = config.services.find((service) => service.id === 'easynews')?.credentials; + if (!credentails || !credentails.username || !credentails.password) { + throw new Error('Easynews credentials not found'); + } + + const easynewsPlusConfigString = getEasynewsPlusConfigString(credentails.username, credentails.password); + + const easynews = new EasynewsPlus( + easynewsPlusConfigString, + easynewsPlusOptions.overrideUrl ?? null, + easynewsPlusOptions.indexerTimeout ? parseInt(easynewsPlusOptions.indexerTimeout) : undefined, + easynewsPlusOptions.overrideName, + addonId + ); + + const streams = await easynews.getParsedStreams(streamRequest); + return streams; +}