diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4b4cb0ea..b19f39e1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,6 +1,6 @@ name: Build Addon -on: +on: push: branches: - main @@ -11,7 +11,7 @@ on: - 'tsconfig.json' - 'tsconfig.base.json' pull_request: - branches: + branches: - main paths: - 'packages/**' @@ -29,8 +29,8 @@ jobs: uses: actions/setup-node@v4 with: node-version: '22.x' - - - name: Install Dependencies + + - name: Install Dependencies run: npm ci - name: Build Addon run: npm run build diff --git a/.vscode/settings.json b/.vscode/settings.json index 89d1965f..1b6457c5 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,4 @@ { - "editor.formatOnSave": true, - "editor.defaultFormatter": "esbenp.prettier-vscode" -} \ No newline at end of file + "editor.formatOnSave": true, + "editor.defaultFormatter": "esbenp.prettier-vscode" +} diff --git a/README.md b/README.md index f2b6abdf..3bec9b7c 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,21 @@

AIOStreams

-## Table of Contents +## Table of Contents - [Description](#description) - [FAQ](#faq) - - [How does it work?](#how-does-it-work) + - [How does it work?](#how-does-it-work) - [Usage](#usage) - - [Public Instance](#public-instance) - - [Personal Instance](#deploying-your-own-instance) - - [Hugging Face](#hugging-face) - - [Cloudflare Workers](#cloudflare-workers) - - [Render](#render) - - [ElfHosted](#elfhosted-paid) - - [Self Hosting](#self-hosting) - - [Docker](#docker) - - [From Source](#from-source) + - [Public Instance](#public-instance) + - [Personal Instance](#deploying-your-own-instance) + - [Hugging Face](#hugging-face) + - [Cloudflare Workers](#cloudflare-workers) + - [Render](#render) + - [ElfHosted](#elfhosted-paid) + - [Self Hosting](#self-hosting) + - [Docker](#docker) + - [From Source](#from-source) - [Configuring](#configuring) - [Environment Variables](#environment-variables) - [Cloudflare Workers](#cloudflare-workers-1) @@ -24,7 +24,7 @@ - [Local](#local) - [Development](#development) - [Credits](#credits) - + ## Description Combines streams from other addons into one and provides much greater customisation: @@ -106,12 +106,13 @@ The addon can display your results in different formats. The formats available a ![image](https://github.com/user-attachments/assets/21f90ee2-e81d-4a56-9e64-8937fb7ab2bc) ## Usage -### Public Instance -[ElfHosted](https://elfhosted.com/) have been kind enough to host a [community instance of AIOStreams](https://aiostreams.elfhosted.com/configure). +### Public Instance -This community instance does have a ratelimit in place, but it is unlikely you will reach it. It also avoids the ratelimits of ElfHosted addons like Comet and MediaFusion as AIOStreams' requests to these addons are routed internally. -However, other non-ElfHosted addons may rate limit the community instance. +[ElfHosted](https://elfhosted.com/) have been kind enough to host a [community instance of AIOStreams](https://aiostreams.elfhosted.com/configure). + +This community instance does have a ratelimit in place, but it is unlikely you will reach it. It also avoids the ratelimits of ElfHosted addons like Comet and MediaFusion as AIOStreams' requests to these addons are routed internally. +However, other non-ElfHosted addons may rate limit the community instance. ### Deploying your own instance @@ -172,7 +173,7 @@ COPY --from=builder /build/packages/formatters/package.*json ./packages/formatte COPY --from=builder /build/packages/parser/package.*json ./packages/parser/ COPY --from=builder /build/packages/types/package.*json ./packages/types/ COPY --from=builder /build/packages/wrappers/package.*json ./packages/wrappers/ -COPY --from=builder /build/packages/utils/package.*json ./packages/utils/ +COPY --from=builder /build/packages/utils/package.\*json ./packages/utils/ COPY --from=builder /build/packages/addon/dist ./packages/addon/dist COPY --from=builder /build/packages/frontend/out ./packages/frontend/out @@ -197,10 +198,9 @@ ENTRYPOINT ["npm", "run", "start:addon"] 6. Your addon will be hosted at {username}-{space-name}.hf.space. You can also find a direct URL to it by clicking the 3 dots > Embed this space > Direct URL > Copy +##### Updating -##### Updating - -To update the addon, you can simply go to the `Settings` tab and click `Factory rebuild`. This will rebuild the addon with the latest changes. +To update the addon, you can simply go to the `Settings` tab and click `Factory rebuild`. This will rebuild the addon with the latest changes. #### Cloudflare Workers @@ -212,7 +212,8 @@ This addon can be deployed as a [Cloudflare Worker](https://workers.cloudflare.c 1. Sign up for a [Cloudflare Account](https://dash.cloudflare.com/sign-up/workers-and-pages) 2. Install Node.js (I would recommend using package managers e.g. fnm on Windows) 3. Install Git -4. Run the following commands: +4. Run the following commands: + ``` git clone https://github.com/Viren070/AIOStreams.git cd AIOStreams @@ -244,15 +245,15 @@ https://render.com/ 3. Enter `https://github.com/Viren070/AIOStreams` 4. Deploy -##### Updating +##### Updating -When you deploy with Render, it automatically builds the addon every time a commit is pushed to this repository. You can also manually trigger a build by clicking the `Deploy` button. +When you deploy with Render, it automatically builds the addon every time a commit is pushed to this repository. You can also manually trigger a build by clicking the `Deploy` button. It is recommend to disable the `Auto Deploy` feature as the latest changes may not be stable. You can do this by going to the `Settings` tab and scrolling down to the `Auto Deploy` setting near the bottom of the `Build & Deploy` section. #### ElfHosted (paid) -AIOStreams is available as a [paid product](https://store.elfhosted.com/product/aiostreams/) on [ElfHosted](https://elfhosted.com). This offers you a no-hassle setup where everything is done for you. +AIOStreams is available as a [paid product](https://store.elfhosted.com/product/aiostreams/) on [ElfHosted](https://elfhosted.com). This offers you a no-hassle setup where everything is done for you. ### Self-Hosting @@ -312,126 +313,146 @@ With encryption, someone who has your manifest URL can't directly see your API k To modify the behaviour of the addon, you can provide the following environment variables: - **ADDON_NAME** - - Type: string - - Default: 'AIOStreams' - - Description: The name of the addon. + + - Type: string + - Default: 'AIOStreams' + - Description: The name of the addon. - **ADDON_ID** - - Type: string - - Default: 'aiostreams.viren070.com' - - Description: The unique identifier for the addon. + - Type: string + - Default: 'aiostreams.viren070.com' + - Description: The unique identifier for the addon. - **PORT** - - Type: number - - Default: 3000 - - Description: The port on which the server will run. + + - Type: number + - Default: 3000 + - Description: The port on which the server will run. - **BRANDING** - - Type: string - - Default: undefined - - Description: Custom branding HTML content. This can be customized by setting the BRANDING environment variable. This HTML content will be displayed at the top of the addon configuration page. ->[!NOTE] ->`BRANDING` is a **build-time** environment variable. It must be present during the build process. + + - Type: string + - Default: undefined + - Description: Custom branding HTML content. This can be customized by setting the BRANDING environment variable. This HTML content will be displayed at the top of the addon configuration page. + > [!NOTE] >`BRANDING` is a **build-time** environment variable. It must be present during the build process. - **SECRET_KEY** - - Type: string - - Default: '' - - Description: The secret key used for encryption. This should be set using the SECRET_KEY environment variable. ```openssl rand -hex 16``` or ```[System.Guid]::NewGuid().ToString("N").Substring(0, 32)``` can be used to generate a new secret key for Linux/MacoS and Windows respectively. + + - Type: string + - Default: '' + - Description: The secret key used for encryption. This should be set using the SECRET_KEY environment variable. `openssl rand -hex 16` or `[System.Guid]::NewGuid().ToString("N").Substring(0, 32)` can be used to generate a new secret key for Linux/MacoS and Windows respectively. - **COMET_URL** - - Type: string - - Default: `'https://comet.elfhosted.com/'` - - Description: The URL for the Comet addon. This URL is used internally by the addon to fetch results from the Comet addon. This environment variable allows you to modify the base URL being used without having to configure the addon yourself and then providing an `Override URL` in the addon configuration. + + - Type: string + - Default: `'https://comet.elfhosted.com/'` + - Description: The URL for the Comet addon. This URL is used internally by the addon to fetch results from the Comet addon. This environment variable allows you to modify the base URL being used without having to configure the addon yourself and then providing an `Override URL` in the addon configuration. - **MEDIAFUSION_URL** - - Type: string - - Default: `'https://mediafusion.elfhosted.com/'` - - Description: The URL for the MediaFusion addon. + + - Type: string + - Default: `'https://mediafusion.elfhosted.com/'` + - Description: The URL for the MediaFusion addon. - **TORRENTIO_URL** - - Type: string - - Default: `'https://torrentio.strem.fun/'` - - Description: The URL for the Torrentio addon. + + - Type: string + - Default: `'https://torrentio.strem.fun/'` + - Description: The URL for the Torrentio addon. - **TORBOX_STREMIO_URL** - - Type: string - - Default: `'https://stremio.torbox.app/'` - - Description: The URL for the Torbox Stremio addon. + + - Type: string + - Default: `'https://stremio.torbox.app/'` + - Description: The URL for the Torbox Stremio addon. - **EASYNEWS_URL** - - Type: string - - Default: `'https://ea627ddf0ee7-easynews.baby-beamup.club/'` - - Description: The url for the easynews addon + + - Type: string + - Default: `'https://ea627ddf0ee7-easynews.baby-beamup.club/'` + - Description: The url for the easynews addon - **EASYNEWS_URL** - - Type: string - - Default: `'https://b89262c192b0-stremio-easynews-addon.baby-beamup.club/'` - - Description: The url for the easynews+ addon + + - Type: string + - Default: `'https://b89262c192b0-stremio-easynews-addon.baby-beamup.club/'` + - Description: The url for the easynews+ addon - **MAX_ADDONS** - - Type: number - - Default: 15 - - Description: The maximum number of addons that is allowed. This is checked when the addon is configured and when you make a request to the addon. + + - Type: number + - Default: 15 + - Description: The maximum number of addons that is allowed. This is checked when the addon is configured and when you make a request to the addon. - **MAX_MOVIE_SIZE** - - Type: number - - Default: 150000000000 (150GB) - - Description: The maximum size for movie files in bytes. This URL controls the maximum size you can set for the movie size filters. This affects the maximum value for the sliders in the configure page. + - Type: number + - Default: 150000000000 (150GB) + - Description: The maximum size for movie files in bytes. This URL controls the maximum size you can set for the movie size filters. This affects the maximum value for the sliders in the configure page. - **MAX_EPISODE_SIZE** - - Type: number - - Default: 15000000000 (15GB) - - Description: The maximum size for episode files in bytes. This URL controls the maximum size you can set for the episode size filters. This affects the maximum value for the sliders in the configure page. + + - Type: number + - Default: 15000000000 (15GB) + - Description: The maximum size for episode files in bytes. This URL controls the maximum size you can set for the episode size filters. This affects the maximum value for the sliders in the configure page. - **MAX_TIMEOUT** - - Type: number - - Default: 50000 - - Description: The maximum timeout value in milliseconds. The timeout value controls how long the addon will wait for a response from an addon before moving on to the next addon. This controls the max value the user can set for the timeout slider in the configure page. + + - Type: number + - Default: 50000 + - Description: The maximum timeout value in milliseconds. The timeout value controls how long the addon will wait for a response from an addon before moving on to the next addon. This controls the max value the user can set for the timeout slider in the configure page. - **MIN_TIMEOUT** - - Type: number - - Default: 1000 - - Description: The minimum timeout value in milliseconds. + + - Type: number + - Default: 1000 + - Description: The minimum timeout value in milliseconds. - **DEFAULT_TIMEOUT** - - Type: number - - Default: 15000 - - Description: The default timeout value in milliseconds. + + - Type: number + - Default: 15000 + - Description: The default timeout value in milliseconds. - **DEFAULT_TORRENTIO_TIMEOUT** - - Type: number - - Default: 5000 - - Description: The default timeout value for Torrentio in milliseconds. + + - Type: number + - Default: 5000 + - Description: The default timeout value for Torrentio in milliseconds. - **DEFAULT_TORBOX_TIMEOUT** - - Type: number - - Default: 15000 - - Description: The default timeout value for Torbox in milliseconds. + + - Type: number + - Default: 15000 + - Description: The default timeout value for Torbox in milliseconds. - **DEFAULT_COMET_TIMEOUT** - - Type: number - - Default: 15000 - - Description: The default timeout value for Comet in milliseconds. + + - Type: number + - Default: 15000 + - Description: The default timeout value for Comet in milliseconds. - **DEFAULT_MEDIAFUSION_TIMEOUT** - - Type: number - - Default: 15000 - - Description: The default timeout value for MediaFusion in milliseconds. + + - Type: number + - Default: 15000 + - Description: The default timeout value for MediaFusion in milliseconds. - **DEFAULT_EASYNEWS_TIMEOUT** - - Type: number - - Default: 15000 - - Description: The default timeout value for the easynews addon in milliseconds + + - Type: number + - Default: 15000 + - Description: The default timeout value for the easynews addon in milliseconds - **DEFAULT_EASYNEWS_PLUS_TIMEOUT** - - Type: number - - Default: 15000 - - Description: The default timeout value for the easynews+ addon in milliseconds + + - Type: number + - Default: 15000 + - Description: The default timeout value for the easynews+ addon in milliseconds - **SHOW_DIE** - - Type: boolean - - Default: true - - Description: A flag to indicate whether to show a die (singular dice) emoji in the stream result names. This is available to distinguish results from the AIOStreams addon from other addons. + - Type: boolean + - Default: true + - Description: A flag to indicate whether to show a die (singular dice) emoji in the stream result names. This is available to distinguish results from the AIOStreams addon from other addons. Below, you can find how to set environment variables for the different methods of deployment. @@ -439,7 +460,7 @@ Below, you can find how to set environment variables for the different methods o Unfortunately, it is not currently possible to set environment variables for this addon on a Cloudflare Worker. You will have to modify the code directly. You can look in `packages/utils/src/settings.ts` to change the default values. -### Render +### Render You can set environment variables in the Render dashboard. @@ -449,19 +470,17 @@ You can set environment variables in the Render dashboard. 4. Click `Add Environment Variable` and enter the name and value of the environment variable you want to set. 5. Once you have added all the environment variables you want to set, click `Save, build, and deploy`. - ### Hugging Face 1. Go to your Hugging Face space and click on the `Settings` tab. -2. Scroll down to `Variables and Secrets` and click on `New secret`. -> [!WARNING] -> Ensure you are using `Secrets`, especially for `SECRET_KEY`. Variables are public and can be seen by anyone. -3. Enter the name and value of the environment variable you want to set. The description is optional and can be left empty. +2. Scroll down to `Variables and Secrets` and click on `New secret`. + > [!WARNING] + > Ensure you are using `Secrets`, especially for `SECRET_KEY`. Variables are public and can be seen by anyone. +3. Enter the name and value of the environment variable you want to set. The description is optional and can be left empty. +### Local -### Local - -You can set environment variables using a .env file in the root of the project. +You can set environment variables using a .env file in the root of the project. ``` ADDON_NAME=AIOStreams @@ -472,9 +491,7 @@ COMET_URL=https://comet.elfhosted.com/ ... ``` -## Development - - +## Development 1. Clone the project and set it as the current directory ``` @@ -488,21 +505,19 @@ COMET_URL=https://comet.elfhosted.com/ npm i ``` - Now, you can run various aspects of the project in development. > [!NOTE] > Most of these commands require that you build the project beforehand. Changes in other packages do not reflect immediately as it needs to be compiled into JavaScript first. > Run `npm run build` to build the project. - -To start the addon in development mode, run the following command: +To start the addon in development mode, run the following command: ``` npm run start:addon:dev ``` -To run the cloudflare worker in development mode, run the following command +To run the cloudflare worker in development mode, run the following command ``` npm run start:cloudflare-worker:dev @@ -514,15 +529,14 @@ To run the frontend of the project, run the following command npm run start:frontend:dev ``` -### Deploying +### Deploying -To deploy your cloudflare worker, run the following command: +To deploy your cloudflare worker, run the following command: ``` npm run deploy:cloudflare-worker ``` - ## Credits Thanks to [sleeyax/stremio-easynews-addon](https://github.com/Sleeyax/stremio-easynews-addon) for the repository structure and dockerfile. diff --git a/packages/addon/src/addon.ts b/packages/addon/src/addon.ts index 252eab6c..72299f6c 100644 --- a/packages/addon/src/addon.ts +++ b/packages/addon/src/addon.ts @@ -34,22 +34,24 @@ export class AIOStreams { const getTimeTakenSincePoint = (point: number) => { const timeNow = new Date().getTime(); const duration = timeNow - point; - // format duration and choose unit and return + // format duration and choose unit and return const nanos = duration * 1_000_000; // Convert to nanoseconds const micros = duration * 1_000; // Convert to microseconds - + if (nanos < 1) { - return `${nanos.toFixed(2)}ns`; + return `${nanos.toFixed(2)}ns`; } else if (micros < 1) { - return `${micros.toFixed(2)}ยตs`; + return `${micros.toFixed(2)}ยตs`; } else if (duration < 1000) { - return `${duration.toFixed(2)}ms`; + return `${duration.toFixed(2)}ms`; } else { - return `${(duration / 1000).toFixed(2)}s`; + return `${(duration / 1000).toFixed(2)}s`; } - } + }; const parsedStreams = await this.getParsedStreams(streamRequest); - console.log(`|INF| addon > getStreams: Got ${parsedStreams.length} total parsed streams in ${getTimeTakenSincePoint(startTime)}`); + console.log( + `|INF| addon > getStreams: Got ${parsedStreams.length} total parsed streams in ${getTimeTakenSincePoint(startTime)}` + ); const filterStartTime = new Date().getTime(); let filteredResults = parsedStreams.filter((parsedStream) => { @@ -64,28 +66,37 @@ export class AIOStreams { if (!qualityFilter) return false; // Check for HDR and DV tags in the parsed stream - const hasHDR = parsedStream.visualTags.some((tag) => tag.startsWith('HDR')); + const hasHDR = parsedStream.visualTags.some((tag) => + tag.startsWith('HDR') + ); const hasDV = parsedStream.visualTags.includes('DV'); const hasHDRAndDV = hasHDR && hasDV; - const HDRAndDVEnabled = this.config.visualTags.some((visualTag) => visualTag['HDR+DV'] === true); - + const HDRAndDVEnabled = this.config.visualTags.some( + (visualTag) => visualTag['HDR+DV'] === true + ); + // Helper function to check if a specific tag is enabled - const isTagEnabled = (tag: string) => this.config.visualTags.some((visualTag) => visualTag[tag] === true); - + const isTagEnabled = (tag: string) => + this.config.visualTags.some((visualTag) => visualTag[tag] === true); + if (hasHDRAndDV) { if (!HDRAndDVEnabled) { return false; } } else if (hasHDR) { - const specificHdrTags = parsedStream.visualTags.filter((tag) => tag.startsWith('HDR')); - const disabledTags = specificHdrTags.filter((tag) => !isTagEnabled(tag)); + const specificHdrTags = parsedStream.visualTags.filter((tag) => + tag.startsWith('HDR') + ); + const disabledTags = specificHdrTags.filter( + (tag) => !isTagEnabled(tag) + ); if (disabledTags.length > 0) { return false; } } else if (hasDV && !isTagEnabled('DV')) { return false; } - + // Check other visual tags for explicit disabling for (const tag of parsedStream.visualTags) { if (tag.startsWith('HDR') || tag === 'DV') continue; @@ -93,13 +104,15 @@ export class AIOStreams { return false; } } - - // apply excludedLanguages filter const excludedLanguages = this.config.excludedLanguages; if (excludedLanguages && parsedStream.languages.length > 0) { - if (parsedStream.languages.every(lang => excludedLanguages.includes(lang))) { + if ( + parsedStream.languages.every((lang) => + excludedLanguages.includes(lang) + ) + ) { return false; } } @@ -171,103 +184,130 @@ export class AIOStreams { return true; }); - console.log(`|INF| addon > getStreams: Initial filter to ${filteredResults.length} streams in ${getTimeTakenSincePoint(filterStartTime)}`); + console.log( + `|INF| addon > getStreams: Initial filter to ${filteredResults.length} streams in ${getTimeTakenSincePoint(filterStartTime)}` + ); if (this.config.cleanResults) { const cleanResultsStartTime = new Date().getTime(); const uniqueStreams: ParsedStream[] = []; - + // Group streams by normalized filename - const streamsByHashOrName = filteredResults.reduce((acc, stream) => { + const streamsByHashOrName = filteredResults.reduce( + (acc, stream) => { const normalizedFilename = stream.filename - ? stream.filename.replace(/[^\p{L}\p{N}]/gu, '').toLowerCase() - : undefined; + ? stream.filename.replace(/[^\p{L}\p{N}]/gu, '').toLowerCase() + : undefined; const key = stream._infoHash || normalizedFilename; if (!key) { - uniqueStreams.push(stream); - return acc; + uniqueStreams.push(stream); + return acc; } - + acc[key] = acc[key] || []; acc[key].push(stream); return acc; - }, {} as Record); - + }, + {} as Record + ); + Object.values(streamsByHashOrName).forEach((groupedStreams) => { - if (groupedStreams.length === 1) { - uniqueStreams.push(groupedStreams[0]); - return; - } - //console.log(`==================\nDetermining unique streams for ${groupedStreams[0].filename} from ${groupedStreams.length} total duplicates`); - //console.log(groupedStreams.map(stream => `Addon ID: ${stream.addon.id}, Provider ID: ${stream.provider?.id}, Provider Cached: ${stream.provider?.cached}`)); - // Separate streams into categories - const cachedStreams = groupedStreams.filter(stream => stream.provider?.cached); - const uncachedStreams = groupedStreams.filter(stream => stream.provider && !stream.provider.cached); - const noProviderStreams = groupedStreams.filter(stream => !stream.provider); - - // Select uncached streams by addon priority (one per provider) - const selectedUncachedStreams = Object.values(uncachedStreams.reduce((acc, stream) => { + if (groupedStreams.length === 1) { + uniqueStreams.push(groupedStreams[0]); + return; + } + //console.log(`==================\nDetermining unique streams for ${groupedStreams[0].filename} from ${groupedStreams.length} total duplicates`); + //console.log(groupedStreams.map(stream => `Addon ID: ${stream.addon.id}, Provider ID: ${stream.provider?.id}, Provider Cached: ${stream.provider?.cached}`)); + // Separate streams into categories + const cachedStreams = groupedStreams.filter( + (stream) => stream.provider?.cached + ); + const uncachedStreams = groupedStreams.filter( + (stream) => stream.provider && !stream.provider.cached + ); + const noProviderStreams = groupedStreams.filter( + (stream) => !stream.provider + ); + + // Select uncached streams by addon priority (one per provider) + const selectedUncachedStreams = Object.values( + uncachedStreams.reduce( + (acc, stream) => { acc[stream.provider!.id] = acc[stream.provider!.id] || []; acc[stream.provider!.id].push(stream); return acc; - }, {} as Record)).map(providerGroup => { - return providerGroup.sort((a, b) => { - const aIndex = this.config.addons.findIndex( - addon => `${addon.id}-${JSON.stringify(addon.options)}` === a.addon.id - ); - const bIndex = this.config.addons.findIndex( - addon => `${addon.id}-${JSON.stringify(addon.options)}` === b.addon.id - ); - return aIndex - bIndex; - })[0]; - }); - //selectedUncachedStreams.forEach(stream => console.log(`Selected uncached stream for provider ${stream.provider!.id}: Addon ID: ${stream.addon.id}`)); - - // Select cached streams by provider and addon priority - const selectedCachedStream = cachedStreams.sort((a, b) => { - const aProviderIndex = this.config.services.findIndex(service => service.id === a.provider!.id); - const bProviderIndex = this.config.services.findIndex(service => service.id === b.provider!.id); - - if (aProviderIndex !== bProviderIndex) { - return aProviderIndex - bProviderIndex; - } - - const aAddonIndex = this.config.addons.findIndex( - addon => `${addon.id}-${JSON.stringify(addon.options)}` === a.addon.id - ); - const bAddonIndex = this.config.addons.findIndex( - addon => `${addon.id}-${JSON.stringify(addon.options)}` === b.addon.id - ); - - return aAddonIndex - bAddonIndex; + }, + {} as Record + ) + ).map((providerGroup) => { + return providerGroup.sort((a, b) => { + const aIndex = this.config.addons.findIndex( + (addon) => + `${addon.id}-${JSON.stringify(addon.options)}` === a.addon.id + ); + const bIndex = this.config.addons.findIndex( + (addon) => + `${addon.id}-${JSON.stringify(addon.options)}` === b.addon.id + ); + return aIndex - bIndex; })[0]; - // Select one non-provider stream (highest addon priority) - const selectedNoProviderStream = noProviderStreams.sort((a, b) => { - const aIndex = this.config.addons.findIndex( - addon => `${addon.id}-${JSON.stringify(addon.options)}` === a.addon.id - ); - const bIndex = this.config.addons.findIndex( - addon => `${addon.id}-${JSON.stringify(addon.options)}` === b.addon.id - ); - return aIndex - bIndex; - })[0]; - - - // Combine selected streams for this group - if (selectedNoProviderStream) { - //console.log(`Selected no provider stream: Addon ID: ${selectedNoProviderStream.addon.id}`); - uniqueStreams.push(selectedNoProviderStream); + }); + //selectedUncachedStreams.forEach(stream => console.log(`Selected uncached stream for provider ${stream.provider!.id}: Addon ID: ${stream.addon.id}`)); + + // Select cached streams by provider and addon priority + const selectedCachedStream = cachedStreams.sort((a, b) => { + const aProviderIndex = this.config.services.findIndex( + (service) => service.id === a.provider!.id + ); + const bProviderIndex = this.config.services.findIndex( + (service) => service.id === b.provider!.id + ); + + if (aProviderIndex !== bProviderIndex) { + return aProviderIndex - bProviderIndex; } - if (selectedCachedStream) { - //console.log(`Selected cached stream for provider ${selectedCachedStream.provider!.id} from Addon ID: ${selectedCachedStream.addon.id}`); - uniqueStreams.push(selectedCachedStream); - } - uniqueStreams.push(...selectedUncachedStreams); + + const aAddonIndex = this.config.addons.findIndex( + (addon) => + `${addon.id}-${JSON.stringify(addon.options)}` === a.addon.id + ); + const bAddonIndex = this.config.addons.findIndex( + (addon) => + `${addon.id}-${JSON.stringify(addon.options)}` === b.addon.id + ); + + return aAddonIndex - bAddonIndex; + })[0]; + // Select one non-provider stream (highest addon priority) + const selectedNoProviderStream = noProviderStreams.sort((a, b) => { + const aIndex = this.config.addons.findIndex( + (addon) => + `${addon.id}-${JSON.stringify(addon.options)}` === a.addon.id + ); + const bIndex = this.config.addons.findIndex( + (addon) => + `${addon.id}-${JSON.stringify(addon.options)}` === b.addon.id + ); + return aIndex - bIndex; + })[0]; + + // Combine selected streams for this group + if (selectedNoProviderStream) { + //console.log(`Selected no provider stream: Addon ID: ${selectedNoProviderStream.addon.id}`); + uniqueStreams.push(selectedNoProviderStream); + } + if (selectedCachedStream) { + //console.log(`Selected cached stream for provider ${selectedCachedStream.provider!.id} from Addon ID: ${selectedCachedStream.addon.id}`); + uniqueStreams.push(selectedCachedStream); + } + uniqueStreams.push(...selectedUncachedStreams); }); - + filteredResults = uniqueStreams; - console.log(`|INF| addon > getStreams: Cleaned results to ${filteredResults.length} streams in ${getTimeTakenSincePoint(cleanResultsStartTime)}`); + console.log( + `|INF| addon > getStreams: Cleaned results to ${filteredResults.length} streams in ${getTimeTakenSincePoint(cleanResultsStartTime)}` + ); } // Apply sorting const sortStartTime = new Date().getTime(); @@ -291,42 +331,66 @@ export class AIOStreams { return 0; }); - console.log(`|INF| addon > getStreams: Sorted results in ${getTimeTakenSincePoint(sortStartTime)}`); + console.log( + `|INF| addon > getStreams: Sorted results in ${getTimeTakenSincePoint(sortStartTime)}` + ); // apply config.maxResultsPerResolution if (this.config.maxResultsPerResolution) { const startTime = new Date().getTime(); - const streamsByResolution = filteredResults.reduce((acc, stream) => { - acc[stream.resolution] = acc[stream.resolution] || []; - acc[stream.resolution].push(stream); - return acc; - }, {} as Record); + const streamsByResolution = filteredResults.reduce( + (acc, stream) => { + acc[stream.resolution] = acc[stream.resolution] || []; + acc[stream.resolution].push(stream); + return acc; + }, + {} as Record + ); - const limitedStreams = Object.values(streamsByResolution).map((streams) => { - return streams.slice(0, this.config.maxResultsPerResolution!); - }); + const limitedStreams = Object.values(streamsByResolution).map( + (streams) => { + return streams.slice(0, this.config.maxResultsPerResolution!); + } + ); filteredResults = limitedStreams.flat(); - console.log(`|INF| addon > getStreams: Limited results to ${filteredResults.length} streams after applying maxResultsPerResolution in ${getTimeTakenSincePoint(startTime)}`); + console.log( + `|INF| addon > getStreams: Limited results to ${filteredResults.length} streams after applying maxResultsPerResolution in ${getTimeTakenSincePoint(startTime)}` + ); } - // Create stream objects const streamsStartTime = new Date().getTime(); - const streamObjects = await Promise.all(filteredResults.map(this.createStreamObject.bind(this))); - streams.push(...streamObjects.filter(s => s !== null)); + const streamObjects = await Promise.all( + filteredResults.map(this.createStreamObject.bind(this)) + ); + streams.push(...streamObjects.filter((s) => s !== null)); - console.log(`|INF| addon > getStreams: Created ${streams.length} stream objects in ${getTimeTakenSincePoint(streamsStartTime)}`); - console.log(`|INF| addon > getStreams: Total time taken to serve streams: ${getTimeTakenSincePoint(startTime)}`); + console.log( + `|INF| addon > getStreams: Created ${streams.length} stream objects in ${getTimeTakenSincePoint(streamsStartTime)}` + ); + console.log( + `|INF| addon > getStreams: Total time taken to serve streams: ${getTimeTakenSincePoint(startTime)}` + ); return streams; } - private createMediaFlowStream(parsedStream: ParsedStream, name: string, description: string): Stream { + private createMediaFlowStream( + parsedStream: ParsedStream, + name: string, + description: string + ): Stream { if (!parsedStream.url) { - console.error(`|ERR| addon > createMediaFlowStream: Stream URL is missing, cannot proxy a stream without a URL`); + console.error( + `|ERR| addon > createMediaFlowStream: Stream URL is missing, cannot proxy a stream without a URL` + ); throw new Error('Stream URL is missing'); } - const proxiedUrl = createProxiedMediaFlowUrl(parsedStream.url, this.config.mediaFlowConfig, parsedStream.stream?.behaviorHints?.proxyHeaders); + const proxiedUrl = createProxiedMediaFlowUrl( + parsedStream.url, + this.config.mediaFlowConfig, + parsedStream.stream?.behaviorHints?.proxyHeaders + ); if (!proxiedUrl) { throw new Error('Could not create MediaFlow proxied URL'); } @@ -341,10 +405,10 @@ export class AIOStreams { return { url: proxiedUrl, - name: this.config.addonNameInDescription + name: this.config.addonNameInDescription ? Settings.ADDON_NAME : `๐Ÿ•ต๏ธ ${name}`, - description: this.config.addonNameInDescription + description: this.config.addonNameInDescription ? `๐Ÿ•ต๏ธ ${name}\n${description}` : description, subtitles: parsedStream.stream?.subtitles, @@ -354,12 +418,13 @@ export class AIOStreams { videoSize: Math.floor(parsedStream.size || 0) || undefined, videoHash: parsedStream.stream?.behaviorHints?.videoHash, bingeGroup: `mfp.${Settings.ADDON_ID}|${parsedStream.addon.name}|${combinedTags.join('|')}`, - } - } - + }, + }; } - private async createStreamObject(parsedStream: ParsedStream): Promise { + private async createStreamObject( + parsedStream: ParsedStream + ): Promise { let name: string = ''; let description: string = ''; switch (this.config.formatter) { @@ -402,7 +467,11 @@ export class AIOStreams { if (this.config.mediaFlowConfig?.mediaFlowEnabled && parsedStream.url) { try { - const mediaFlowStream = this.createMediaFlowStream(parsedStream, name, description); + const mediaFlowStream = this.createMediaFlowStream( + parsedStream, + name, + description + ); if (!mediaFlowStream) { throw new Error('Unknown error creating MediaFlow stream'); } @@ -420,7 +489,9 @@ export class AIOStreams { fileIdx: parsedStream.torrent?.fileIdx, name: this.config.addonNameInDescription ? Settings.ADDON_NAME - : Settings.SHOW_DIE ? `๐ŸŽฒ ${name}` : name, + : Settings.SHOW_DIE + ? `๐ŸŽฒ ${name}` + : name, description: this.config.addonNameInDescription ? `๐ŸŽฒ ${name}\n${description}` : description, @@ -449,7 +520,6 @@ export class AIOStreams { if (aHasPrioritisedLanguage && !bHasPrioritisedLanguage) return -1; if (!aHasPrioritisedLanguage && bHasPrioritisedLanguage) return 1; - } return 0; } @@ -478,9 +548,15 @@ export class AIOStreams { // prioritise a false value over undefined if (aCached === false && bCached === undefined) return -1; if (aCached === undefined && bCached === false) return 1; - return this.config.sortBy.find((sort) => Object.keys(sort)[0] === 'cached')?.direction === 'asc' - ? aCached ? 1 : -1 // uncached > cached - : aCached ? -1 : 1; // cached > uncached + return this.config.sortBy.find( + (sort) => Object.keys(sort)[0] === 'cached' + )?.direction === 'asc' + ? aCached + ? 1 + : -1 // uncached > cached + : aCached + ? -1 + : 1; // cached > uncached } } else if (field === 'hasProvider') { // files from a provider should be prioritised and then @@ -503,17 +579,29 @@ export class AIOStreams { return aIndex - bIndex; } } else if (field === 'size') { - return this.config.sortBy.find((sort) => Object.keys(sort)[0] === 'size')?.direction === 'asc' + return this.config.sortBy.find((sort) => Object.keys(sort)[0] === 'size') + ?.direction === 'asc' ? (a.size || 0) - (b.size || 0) - : (b.size || 0) - (a.size || 0) + : (b.size || 0) - (a.size || 0); } else if (field === 'seeders') { - if (a.torrent?.seeders !== undefined && b.torrent?.seeders !== undefined) { - return this.config.sortBy.find((sort) => Object.keys(sort)[0] === 'seeders')?.direction === 'asc' + if ( + a.torrent?.seeders !== undefined && + b.torrent?.seeders !== undefined + ) { + return this.config.sortBy.find( + (sort) => Object.keys(sort)[0] === 'seeders' + )?.direction === 'asc' ? a.torrent.seeders - b.torrent.seeders : b.torrent.seeders - a.torrent.seeders; - } else if (a.torrent?.seeders !== undefined && b.torrent?.seeders === undefined) { + } else if ( + a.torrent?.seeders !== undefined && + b.torrent?.seeders === undefined + ) { return -1; - } else if (a.torrent?.seeders === undefined && b.torrent?.seeders !== undefined) { + } else if ( + a.torrent?.seeders === undefined && + b.torrent?.seeders !== undefined + ) { return 1; } } else if (field === 'quality') { @@ -523,21 +611,24 @@ export class AIOStreams { ); } else if (field === 'visualTag') { // Find the highest priority visual tag in each file - const getIndexOfTag = (tag: string) => this.config.visualTags.findIndex((t) => t[tag]); - + const getIndexOfTag = (tag: string) => + this.config.visualTags.findIndex((t) => t[tag]); + const getHighestPriorityTagIndex = (tags: string[]) => { // Check if the file contains both any HDR tag and DV const hasHDR = tags.some((tag) => tag.startsWith('HDR')); const hasDV = tags.includes('DV'); - + if (hasHDR && hasDV) { // Sort according to the position of the HDR+DV tag - const hdrDvIndex = this.config.visualTags.findIndex((t) => t['HDR+DV']); + const hdrDvIndex = this.config.visualTags.findIndex( + (t) => t['HDR+DV'] + ); if (hdrDvIndex !== -1) { return hdrDvIndex; } } - + // If the file contains multiple HDR tags, look at the HDR tag that has the highest priority const hdrTagIndices = tags .filter((tag) => tag.startsWith('HDR')) @@ -545,17 +636,17 @@ export class AIOStreams { if (hdrTagIndices.length > 0) { return Math.min(...hdrTagIndices); } - + // Always consider the highest priority visual tag when a file has multiple visual tags return tags.reduce( (minIndex, tag) => Math.min(minIndex, getIndexOfTag(tag)), this.config.visualTags.length ); }; - + const aVisualTagIndex = getHighestPriorityTagIndex(a.visualTags); const bVisualTagIndex = getHighestPriorityTagIndex(b.visualTags); - + // Sort by the visual tag index return aVisualTagIndex - bVisualTagIndex; } else if (field === 'audioTag') { @@ -593,7 +684,7 @@ export class AIOStreams { if (!this.config.prioritisedLanguages) { return 0; } - // else, we look at the array of prioritisedLanguages. + // else, we look at the array of prioritisedLanguages. // any file with a language in the prioritisedLanguages array should be prioritised // if both files contain a prioritisedLanguage, we compare the index of the highest priority language @@ -610,14 +701,20 @@ export class AIOStreams { if (aHasPrioritisedLanguage && bHasPrioritisedLanguage) { const getHighestPriorityLanguageIndex = (languages: string[]) => { return languages.reduce((minIndex, lang) => { - const index = this.config.prioritisedLanguages?.indexOf(lang) ?? this.config.prioritisedLanguages!.length; + const index = + this.config.prioritisedLanguages?.indexOf(lang) ?? + this.config.prioritisedLanguages!.length; return index !== -1 ? Math.min(minIndex, index) : minIndex; }, this.config.prioritisedLanguages!.length); }; - - const aHighestPriorityLanguageIndex = getHighestPriorityLanguageIndex(a.languages); - const bHighestPriorityLanguageIndex = getHighestPriorityLanguageIndex(b.languages); - + + const aHighestPriorityLanguageIndex = getHighestPriorityLanguageIndex( + a.languages + ); + const bHighestPriorityLanguageIndex = getHighestPriorityLanguageIndex( + b.languages + ); + return aHighestPriorityLanguageIndex - bHighestPriorityLanguageIndex; } } @@ -629,7 +726,8 @@ export class AIOStreams { ): Promise { const parsedStreams: ParsedStream[] = []; const addonPromises = this.config.addons.map(async (addon) => { - const addonName = addon.options.name || addon.options.overrideName || addon.id; + const addonName = + addon.options.name || addon.options.overrideName || addon.id; try { const addonId = `${addon.id}-${JSON.stringify(addon.options)}`; const streams = await this.getStreamsFromAddon( @@ -638,9 +736,13 @@ export class AIOStreams { streamRequest ); parsedStreams.push(...streams); - console.log(`|INF| addon > getParsedStreams: Got ${streams.length} streams from addon ${addonName}`); + console.log( + `|INF| addon > getParsedStreams: Got ${streams.length} streams from addon ${addonName}` + ); } catch (error) { - console.error(`|ERR| addon > getParsedStreams: Failed to get streams from ${addonName}: ${error}`); + console.error( + `|ERR| addon > getParsedStreams: Failed to get streams from ${addonName}: ${error}` + ); } }); @@ -675,7 +777,7 @@ export class AIOStreams { this.config, addon.options, streamRequest, - addonId, + addonId ); } case 'mediafusion': { @@ -683,7 +785,7 @@ export class AIOStreams { this.config, addon.options, streamRequest, - addonId, + addonId ); } case 'easynews': { @@ -692,7 +794,7 @@ export class AIOStreams { addon.options, streamRequest, addonId - ) + ); } case 'easynews-plus': { return await getEasynewsPlusStreams( @@ -713,7 +815,7 @@ export class AIOStreams { ? parseInt(addon.options.indexerTimeout) : undefined, addonId, - this.config, + this.config ); return await wrapper.getParsedStreams(streamRequest); } @@ -730,7 +832,7 @@ export class AIOStreams { ? parseInt(addon.options.indexerTimeout) : undefined, addonId, - this.config, + this.config ); return await wrapper.getParsedStreams(streamRequest); } diff --git a/packages/addon/src/config.ts b/packages/addon/src/config.ts index 58021077..8172db39 100644 --- a/packages/addon/src/config.ts +++ b/packages/addon/src/config.ts @@ -129,7 +129,9 @@ export function validateConfig(config: Config): { if ( option.id.toLowerCase().includes('url') && addon.options[option.id] && - addon.options[option.id]?.match(/^E-[0-9a-fA-F]{32}-[0-9a-fA-F]+$/) === null + addon.options[option.id]?.match( + /^E-[0-9a-fA-F]{32}-[0-9a-fA-F]+$/ + ) === null ) { try { new URL(addon.options[option.id] as string); diff --git a/packages/addon/src/manifest.ts b/packages/addon/src/manifest.ts index b334914e..c5ac3edd 100644 --- a/packages/addon/src/manifest.ts +++ b/packages/addon/src/manifest.ts @@ -15,8 +15,8 @@ const manifest = (configured: boolean) => { types: ['movie', 'series'], behaviorHints: { configurable: true, - configurationRequired: configured ? false : true - } + configurationRequired: configured ? false : true, + }, }; }; diff --git a/packages/addon/src/responses.ts b/packages/addon/src/responses.ts index 26bd4b7b..3c2fcf6c 100644 --- a/packages/addon/src/responses.ts +++ b/packages/addon/src/responses.ts @@ -1,4 +1,4 @@ -import { Settings } from "@aiostreams/utils"; +import { Settings } from '@aiostreams/utils'; export const missingConfig = (origin: string) => { return { diff --git a/packages/addon/src/server.ts b/packages/addon/src/server.ts index 8db1e17e..724e0b65 100644 --- a/packages/addon/src/server.ts +++ b/packages/addon/src/server.ts @@ -6,14 +6,21 @@ import { Config, StreamRequest } from '@aiostreams/types'; import { validateConfig } from './config'; import manifest from './manifest'; import { invalidConfig, missingConfig } from './responses'; -import {Settings, compressAndEncrypt, decryptAndDecompress, parseAndDecryptString} from '@aiostreams/utils'; +import { + Settings, + compressAndEncrypt, + decryptAndDecompress, + parseAndDecryptString, +} from '@aiostreams/utils'; const app = express(); console.log(`|INF| server > init: Starting server and loading settings...`); (Object.keys(Settings) as Array).forEach((key) => { if (key === 'SECRET_KEY' || key === 'BRANDING') { if (Settings[key]) { - console.log(`|INF| server > init: ${key} = ${Settings[key].replace(/./g, '*').slice(0, 32)}`) + console.log( + `|INF| server > init: ${key} = ${Settings[key].replace(/./g, '*').slice(0, 32)}` + ); } if (key === 'SECRET_KEY' && !Settings[key]) { console.warn(`|WRN| server > init: ${key} = ${Settings[key]}`); @@ -21,9 +28,11 @@ console.log(`|INF| server > init: Starting server and loading settings...`); return; } console.log(`|INF| server > init: ${key} = ${Settings[key]}`); -}) +}); if (!Settings.SECRET_KEY) { - console.warn(`|WRN| server > init: SECRET_KEY is not set, data encryption is disabled! `); + console.warn( + `|WRN| server > init: SECRET_KEY is not set, data encryption is disabled! ` + ); } const rootUrl = (req: Request) => @@ -37,7 +46,9 @@ app.use(express.urlencoded({ extended: true })); app.use((req, res, next) => { res.append('Access-Control-Allow-Origin', '*'); res.append('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE'); - console.log(`|DBG| server > ${req.method} ${req.path.replace(/\/eyJ[\w\=]+/g, '/*******').replace(/\/E-[\w-]+/g, '/*******')}`); + console.log( + `|DBG| server > ${req.method} ${req.path.replace(/\/eyJ[\w\=]+/g, '/*******').replace(/\/E-[\w-]+/g, '/*******')}` + ); next(); }); @@ -45,9 +56,12 @@ app.get('/', (req, res) => { res.redirect('/configure'); }); -app.get(['/_next/*', '/assets/*', '/icon.ico', '/configure.txt', ], (req, res) => { - res.sendFile(path.join(__dirname, '../../frontend/out', req.path)); -}); +app.get( + ['/_next/*', '/assets/*', '/icon.ico', '/configure.txt'], + (req, res) => { + res.sendFile(path.join(__dirname, '../../frontend/out', req.path)); + } +); app.get('/configure', (req, res) => { res.sendFile(path.join(__dirname, '../../frontend/out/configure.html')); @@ -55,10 +69,12 @@ app.get('/configure', (req, res) => { app.get('/:config/configure', (req, res) => { const config = req.params.config; - if (config.startsWith("E-")) { + if (config.startsWith('E-')) { if (!Settings.SECRET_KEY) { res.status(302).redirect('/configure'); - console.log(`|INF| server > Received encrypted config but no secret key set, redirecting to /configure`); + console.log( + `|INF| server > Received encrypted config but no secret key set, redirecting to /configure` + ); return; } const encryptedConfig = config.replace('E-', ''); @@ -74,12 +90,17 @@ app.get('/:config/configure', (req, res) => { // encrypt the value and replace the value with the encrypted value Object.keys(service.credentials).forEach((key) => { const value = service.credentials[key]; - if (value && value.match(/^E-[0-9a-fA-F]{32}-[0-9a-fA-F]+$/) === null) { + if ( + value && + value.match(/^E-[0-9a-fA-F]{32}-[0-9a-fA-F]+$/) === null + ) { // if the value is not already encrypted, encrypt it try { service.credentials[key] = compressAndEncrypt(value); } catch (error: any) { - console.error(`Failed to encrypt ${key} for service ${service.id}`); + console.error( + `Failed to encrypt ${key} for service ${service.id}` + ); service.credentials[key] = ''; } } @@ -88,25 +109,46 @@ app.get('/:config/configure', (req, res) => { }); } // also encrypt the apiPassword, proxyUrl, and publicIp if they are not already encrypted - if (configJson.mediaFlowConfig?.apiPassword && configJson.mediaFlowConfig.apiPassword.match(/^E-[0-9a-fA-F]{32}-[0-9a-fA-F]+$/) === null) { + if ( + configJson.mediaFlowConfig?.apiPassword && + configJson.mediaFlowConfig.apiPassword.match( + /^E-[0-9a-fA-F]{32}-[0-9a-fA-F]+$/ + ) === null + ) { try { - configJson.mediaFlowConfig.apiPassword = compressAndEncrypt(configJson.mediaFlowConfig.apiPassword); + configJson.mediaFlowConfig.apiPassword = compressAndEncrypt( + configJson.mediaFlowConfig.apiPassword + ); } catch (error: any) { console.error(`Failed to encrypt apiPassword for MediaFlow`); configJson.mediaFlowConfig.apiPassword = ''; } } - if (configJson.mediaFlowConfig?.proxyUrl && configJson.mediaFlowConfig.proxyUrl.match(/^E-[0-9a-fA-F]{32}-[0-9a-fA-F]+$/) === null) { + if ( + configJson.mediaFlowConfig?.proxyUrl && + configJson.mediaFlowConfig.proxyUrl.match( + /^E-[0-9a-fA-F]{32}-[0-9a-fA-F]+$/ + ) === null + ) { try { - configJson.mediaFlowConfig.proxyUrl = compressAndEncrypt(configJson.mediaFlowConfig.proxyUrl); + configJson.mediaFlowConfig.proxyUrl = compressAndEncrypt( + configJson.mediaFlowConfig.proxyUrl + ); } catch (error: any) { console.error(`Failed to encrypt proxyUrl for MediaFlow`); configJson.mediaFlowConfig.proxyUrl = ''; } } - if (configJson.mediaFlowConfig?.publicIp && configJson.mediaFlowConfig.publicIp.match(/^E-[0-9a-fA-F]{32}-[0-9a-fA-F]+$/) === null) { + if ( + configJson.mediaFlowConfig?.publicIp && + configJson.mediaFlowConfig.publicIp.match( + /^E-[0-9a-fA-F]{32}-[0-9a-fA-F]+$/ + ) === null + ) { try { - configJson.mediaFlowConfig.publicIp = compressAndEncrypt(configJson.mediaFlowConfig.publicIp); + configJson.mediaFlowConfig.publicIp = compressAndEncrypt( + configJson.mediaFlowConfig.publicIp + ); } catch (error: any) { console.error(`Failed to encrypt publicIp for MediaFlow`); configJson.mediaFlowConfig.publicIp = ''; @@ -120,8 +162,14 @@ app.get('/:config/configure', (req, res) => { Object.keys(addon.options).forEach((key) => { const value = addon.options[key]; console.log(`|DBG| server > ${key} = ${value}`); - if (value && value.match(/^E-[0-9a-fA-F]{32}-[0-9a-fA-F]+$/) === null && key.match(/url/i)) { - console.log(`|DBG| server > Encrypting ${key} for addon ${addon.id}`); + if ( + value && + value.match(/^E-[0-9a-fA-F]{32}-[0-9a-fA-F]+$/) === null && + key.match(/url/i) + ) { + console.log( + `|DBG| server > Encrypting ${key} for addon ${addon.id}` + ); try { addon.options[key] = compressAndEncrypt(value); } catch (error: any) { @@ -133,11 +181,13 @@ app.get('/:config/configure', (req, res) => { } }); } - const base64Config = Buffer.from(JSON.stringify(configJson)).toString('base64'); + const base64Config = Buffer.from(JSON.stringify(configJson)).toString( + 'base64' + ); res.redirect(`/${base64Config}/configure`); return; } - res.sendFile(path.join(__dirname, '../../frontend/out/configure.html')); + res.sendFile(path.join(__dirname, '../../frontend/out/configure.html')); }); app.get('/manifest.json', (req, res) => { @@ -160,7 +210,9 @@ app.get('/:config/stream/:type/:id.json', (req, res: Response): void => { let configJson: Config; if (config.startsWith('E-')) { if (!Settings.SECRET_KEY) { - console.error(`|ERR| server > Secret key not set, cannot decrypt encrypted config`); + console.error( + `|ERR| server > Secret key not set, cannot decrypt encrypted config` + ); res.status(200).json(invalidConfig(rootUrl(req), 'Secret key not set')); return; } @@ -171,17 +223,21 @@ app.get('/:config/stream/:type/:id.json', (req, res: Response): void => { } configJson = JSON.parse(decryptedConfig); } catch (error: any) { - res.status(200).json(invalidConfig(rootUrl(req), 'Unable to decrypt config')); + res + .status(200) + .json(invalidConfig(rootUrl(req), 'Unable to decrypt config')); return; } } else { // Decode Base64 encoded JSON config const decodedConfig = Buffer.from(config, 'base64').toString('utf-8'); try { - configJson = JSON.parse((decodedConfig)); + configJson = JSON.parse(decodedConfig); } catch (error: any) { console.error(`|ERR| server > Unable to parse config: ${error.message}`); - res.status(200).json(invalidConfig(rootUrl(req), 'Unable to parse config')); + res + .status(200) + .json(invalidConfig(rootUrl(req), 'Unable to parse config')); return; } } @@ -195,39 +251,57 @@ app.get('/:config/stream/:type/:id.json', (req, res: Response): void => { const value = service.credentials[key]; const decrypted = parseAndDecryptString(value); if (decrypted === null) { - console.error(`|ERR| server > Failed to decrypt ${key} for service ${service.id}`); - res.status(200).json(invalidConfig(rootUrl(req), 'Failed to decrypt config')); + console.error( + `|ERR| server > Failed to decrypt ${key} for service ${service.id}` + ); + res + .status(200) + .json(invalidConfig(rootUrl(req), 'Failed to decrypt config')); return; } service.credentials[key] = decrypted; }); } - }) - }; + }); + } // also decrypt the apiPassword, proxyUrl, and publicIp if they are encrypted if (configJson.mediaFlowConfig?.apiPassword) { - const decrypted = parseAndDecryptString(configJson.mediaFlowConfig.apiPassword); + const decrypted = parseAndDecryptString( + configJson.mediaFlowConfig.apiPassword + ); if (!decrypted) { - console.error(`|ERR| server > Failed to decrypt apiPassword for MediaFlow`); - res.status(200).json(invalidConfig(rootUrl(req), 'Failed to decrypt config')); + console.error( + `|ERR| server > Failed to decrypt apiPassword for MediaFlow` + ); + res + .status(200) + .json(invalidConfig(rootUrl(req), 'Failed to decrypt config')); return; } configJson.mediaFlowConfig.apiPassword = decrypted; } if (configJson.mediaFlowConfig?.proxyUrl) { - const decrypted = parseAndDecryptString(configJson.mediaFlowConfig.proxyUrl); + const decrypted = parseAndDecryptString( + configJson.mediaFlowConfig.proxyUrl + ); if (!decrypted) { console.error(`|ERR| server > Failed to decrypt proxyUrl for MediaFlow`); - res.status(200).json(invalidConfig(rootUrl(req), 'Failed to decrypt config')); + res + .status(200) + .json(invalidConfig(rootUrl(req), 'Failed to decrypt config')); return; } configJson.mediaFlowConfig.proxyUrl = decrypted; } if (configJson.mediaFlowConfig?.publicIp) { - const decrypted = parseAndDecryptString(configJson.mediaFlowConfig.publicIp); + const decrypted = parseAndDecryptString( + configJson.mediaFlowConfig.publicIp + ); if (!decrypted) { console.error(`|ERR| server > Failed to decrypt publicIp for MediaFlow`); - res.status(200).json(invalidConfig(rootUrl(req), 'Failed to decrypt config')); + res + .status(200) + .json(invalidConfig(rootUrl(req), 'Failed to decrypt config')); return; } configJson.mediaFlowConfig.publicIp = decrypted; @@ -240,8 +314,12 @@ app.get('/:config/stream/:type/:id.json', (req, res: Response): void => { const value = addon.options[key]; const decrypted = parseAndDecryptString(value); if (decrypted === null) { - console.error(`|ERR| server > Failed to decrypt ${key} for addon ${addon.id}`); - res.status(200).json(invalidConfig(rootUrl(req), 'Failed to decrypt config')); + console.error( + `|ERR| server > Failed to decrypt ${key} for addon ${addon.id}` + ); + res + .status(200) + .json(invalidConfig(rootUrl(req), 'Failed to decrypt config')); return; } addon.options[key] = decrypted; @@ -249,8 +327,6 @@ app.get('/:config/stream/:type/:id.json', (req, res: Response): void => { } }); } - - const decodedPath = decodeURIComponent(req.path); @@ -278,7 +354,9 @@ app.get('/:config/stream/:type/:id.json', (req, res: Response): void => { try { const { valid, errorCode, errorMessage } = validateConfig(configJson); if (!valid) { - console.error(`|ERR| server > Received invalid config: ${errorCode} - ${errorMessage}`); + console.error( + `|ERR| server > Received invalid config: ${errorCode} - ${errorMessage}` + ); res .status(200) .json(invalidConfig(rootUrl(req), errorMessage ?? 'Unknown')); diff --git a/packages/formatters/src/gdrive.ts b/packages/formatters/src/gdrive.ts index 1e6ed4ee..a7e6acae 100644 --- a/packages/formatters/src/gdrive.ts +++ b/packages/formatters/src/gdrive.ts @@ -8,13 +8,15 @@ export function gdriveFormat(stream: ParsedStream): { } { let name: string = ''; - if (stream.provider) { + if (stream.provider) { const cacheStatus = stream.provider.cached ? 'โšก' : stream.provider.cached === undefined ? 'โ“' : 'โณ'; - const serviceShortName = serviceDetails.find((service) => service.id === stream.provider!.id)?.shortName || stream.provider.id; + const serviceShortName = + serviceDetails.find((service) => service.id === stream.provider!.id) + ?.shortName || stream.provider.id; name += `[${serviceShortName}${cacheStatus}] `; } @@ -36,14 +38,22 @@ export function gdriveFormat(stream: ParsedStream): { description += stream.audioTags.length > 0 ? `๐ŸŽง ${stream.audioTags.join(' | ')}` : ''; } - if (stream.size || stream.torrent?.seeders || stream.usenet?.age || stream.duration) { + if ( + stream.size || + stream.torrent?.seeders || + stream.usenet?.age || + stream.duration + ) { description += '\n'; description += `๐Ÿ“ฆ ${formatSize(stream.size || 0)} `; - description += stream.duration ? `โฑ๏ธ ${formatDuration(stream.duration)} ` : ''; - description += stream.torrent?.seeders !== undefined - ? `๐Ÿ‘ฅ ${stream.torrent.seeders}` + description += stream.duration + ? `โฑ๏ธ ${formatDuration(stream.duration)} ` : ''; + description += + stream.torrent?.seeders !== undefined + ? `๐Ÿ‘ฅ ${stream.torrent.seeders}` + : ''; description += stream.usenet?.age ? `๐Ÿ“… ${stream.usenet.age}` : ''; } diff --git a/packages/formatters/src/torbox.ts b/packages/formatters/src/torbox.ts index fb3cbc2e..1d8a3b07 100644 --- a/packages/formatters/src/torbox.ts +++ b/packages/formatters/src/torbox.ts @@ -10,7 +10,9 @@ export function torboxFormat(stream: ParsedStream): { name += `${stream.addon.name}`; if (stream.provider) { - const serviceShortName = serviceDetails.find((service) => service.id === stream.provider!.id)?.shortName || stream.provider.id; + const serviceShortName = + serviceDetails.find((service) => service.id === stream.provider!.id) + ?.shortName || stream.provider.id; name += ` (${serviceShortName}${stream.provider.cached === undefined ? ' Unknown' : stream.provider.cached ? ' Instant' : ''})`; } diff --git a/packages/formatters/src/torrentio.ts b/packages/formatters/src/torrentio.ts index 24af9fbc..9df3c099 100644 --- a/packages/formatters/src/torrentio.ts +++ b/packages/formatters/src/torrentio.ts @@ -14,7 +14,9 @@ export function torrentioFormat(stream: ParsedStream): { : stream.provider.cached === undefined ? ' Unknown' : ' download'; - const serviceShortName = serviceDetails.find((service) => service.id === stream.provider!.id)?.shortName || stream.provider.id; + const serviceShortName = + serviceDetails.find((service) => service.id === stream.provider!.id) + ?.shortName || stream.provider.id; name += `[${serviceShortName}${cacheStatus}] `; } @@ -37,9 +39,10 @@ export function torrentioFormat(stream: ParsedStream): { ) { description += '\n'; - description += stream.torrent?.seeders !== undefined - ? `๐Ÿ‘ค ${stream.torrent.seeders} ` - : ''; + description += + stream.torrent?.seeders !== undefined + ? `๐Ÿ‘ค ${stream.torrent.seeders} ` + : ''; description += stream.usenet?.age ? `๐Ÿ“… ${stream.usenet.age} ` : ''; diff --git a/packages/formatters/src/utils.ts b/packages/formatters/src/utils.ts index 08687641..f41e70d9 100644 --- a/packages/formatters/src/utils.ts +++ b/packages/formatters/src/utils.ts @@ -88,5 +88,5 @@ const languageEmojiMap: Record = { norwegian: '๐Ÿ‡ณ๐Ÿ‡ด', malay: '๐Ÿ‡ฒ๐Ÿ‡พ', latino: '๐Ÿ’ƒ๐Ÿป', - Latino: '๐Ÿ‡ฒ๐Ÿ‡ฝ' + Latino: '๐Ÿ‡ฒ๐Ÿ‡ฝ', }; diff --git a/packages/frontend/next.config.ts b/packages/frontend/next.config.ts index f31d10ac..6515352b 100644 --- a/packages/frontend/next.config.ts +++ b/packages/frontend/next.config.ts @@ -1,6 +1,6 @@ import type { NextConfig } from 'next'; import dotenv from 'dotenv'; -import path from 'path' +import path from 'path'; try { dotenv.config({ path: path.resolve(__dirname, '../../.env') }); @@ -8,7 +8,8 @@ try { console.error('Error loading .env file:', error); } -const branding = process.env.NEXT_PUBLIC_ELFHOSTED_BRANDING ?? process.env.BRANDING; +const branding = + process.env.NEXT_PUBLIC_ELFHOSTED_BRANDING ?? process.env.BRANDING; if (branding) { console.log(`Branding set`); @@ -23,7 +24,7 @@ const nextConfig: NextConfig = { }, env: { NEXT_PUBLIC_BRANDING: branding, - } + }, }; export default nextConfig; diff --git a/packages/frontend/src/app/configure/page.module.css b/packages/frontend/src/app/configure/page.module.css index dda2da7b..a1a7c0fb 100644 --- a/packages/frontend/src/app/configure/page.module.css +++ b/packages/frontend/src/app/configure/page.module.css @@ -13,7 +13,6 @@ margin: 0 auto; /* Center the header within the content */ } - .content { background-color: #000000; /* Slightly lighter black */ border-radius: var(--borderRadius); @@ -143,12 +142,18 @@ border-radius: var(--borderRadius); border-color: #777777; padding: 10px; - transition: margin-top 0.4s, opacity 0.3s, transform 0.6s; + transition: + margin-top 0.4s, + opacity 0.3s, + transform 0.6s; } .mediaFlowConfig.hidden { opacity: 0; margin-top: -340px; transform: scale(0); - transition: margin-top 0.5s, opacity 0.2s, transform 0.4s; -} \ No newline at end of file + transition: + margin-top 0.5s, + opacity 0.2s, + transform 0.4s; +} diff --git a/packages/frontend/src/app/configure/page.tsx b/packages/frontend/src/app/configure/page.tsx index aaa56e10..613e6eac 100644 --- a/packages/frontend/src/app/configure/page.tsx +++ b/packages/frontend/src/app/configure/page.tsx @@ -51,12 +51,12 @@ const defaultQualities: Quality[] = [ ]; const defaultVisualTags: VisualTag[] = [ - { "HDR+DV": true }, + { 'HDR+DV': true }, { 'HDR10+': true }, { HDR10: true }, { HDR: true }, { DV: true }, - { "3D": true }, + { '3D': true }, { IMAX: true }, { AI: true }, ]; @@ -86,10 +86,10 @@ const defaultEncodes: Encode[] = [ const defaultSortCriteria: SortBy[] = [ { cached: true, direction: 'desc' }, { resolution: true }, - { language: true}, + { language: true }, { size: true, direction: 'desc' }, { visualTag: false }, - { service: false, }, + { service: false }, { audioTag: false }, { encode: false }, { quality: false }, @@ -129,7 +129,6 @@ const defaultResolutions: Resolution[] = [ { Unknown: true }, ]; - const defaultServices = serviceDetails.map((service) => ({ name: service.name, id: service.id, @@ -138,7 +137,7 @@ const defaultServices = serviceDetails.map((service) => ({ })); const selectStyles: StylesConfig = { - control: (baseStyles: any, state: { isFocused: boolean; }) => ({ + control: (baseStyles: any, state: { isFocused: boolean }) => ({ ...baseStyles, borderWidth: '0px', backgroundColor: 'white', @@ -149,10 +148,12 @@ const selectStyles: StylesConfig = { margin: '10px 0 0 -0px', transition: 'border-color 0.2s, box-shadow 0.2s', boxShadow: state.isFocused ? '0 0 0 3px rgb(112, 112, 112)' : 'none', - "&:hover": { + '&:hover': { borderColor: '#5c5c5c', - boxShadow: state.isFocused ? '0 0 0 3px rgb(128, 128, 128)' : '0 0 0 2px rgb(161, 161, 161)', - } + boxShadow: state.isFocused + ? '0 0 0 3px rgb(128, 128, 128)' + : '0 0 0 2px rgb(161, 161, 161)', + }, }), input: (baseStyles: any) => ({ ...baseStyles, @@ -176,7 +177,7 @@ const selectStyles: StylesConfig = { ...baseStyles, color: 'white', transition: 'color 0.2s', - "&:hover": { + '&:hover': { backgroundColor: 'transparent', color: 'rgb(141, 141, 141)', cursor: 'pointer', @@ -192,17 +193,17 @@ const selectStyles: StylesConfig = { valueContainer: (baseStyles: any) => ({ ...baseStyles, }), - option: (baseStyles: any, state: { isFocused: any; }) => ({ + option: (baseStyles: any, state: { isFocused: any }) => ({ ...baseStyles, color: state.isFocused ? 'white' : 'black', backgroundColor: state.isFocused ? 'rgb(68, 68, 68)' : 'white', - "&:hover": { + '&:hover': { backgroundColor: 'rgb(68, 68, 68)', //'#9c9c9c', }, - "&:active": { - transition: 'background-color 0.4s, color 0.1s', - backgroundColor: 'rgb(26, 26, 26)', - color: 'white', + '&:active': { + transition: 'background-color 0.4s, color 0.1s', + backgroundColor: 'rgb(26, 26, 26)', + color: 'white', }, }), }; @@ -221,8 +222,12 @@ export default function Configure() { const [services, setServices] = useState(defaultServices); const [onlyShowCachedStreams, setOnlyShowCachedStreams] = useState(false); - const [prioritisedLanguages, setPrioritisedLanguages] = useState(null); - const [excludedLanguages, setExcludedLanguages] = useState(null); + const [prioritisedLanguages, setPrioritisedLanguages] = useState< + string[] | null + >(null); + const [excludedLanguages, setExcludedLanguages] = useState( + null + ); const [addons, setAddons] = useState([]); /* const [maxSize, setMaxSize] = useState(null); @@ -232,15 +237,20 @@ export default function Configure() { const [minMovieSize, setMinMovieSize] = useState(null); const [maxEpisodeSize, setMaxEpisodeSize] = useState(null); const [minEpisodeSize, setMinEpisodeSize] = useState(null); - const [addonNameInDescription, setAddonNameInDescription] = useState(false); + const [addonNameInDescription, setAddonNameInDescription] = + useState(false); const [cleanResults, setCleanResults] = useState(false); - const [maxResultsPerResolution, setMaxResultsPerResolution] = useState(null); + const [maxResultsPerResolution, setMaxResultsPerResolution] = useState< + number | null + >(null); const [mediaFlowEnabled, setMediaFlowEnabled] = useState(false); const [mediaFlowProxyUrl, setmediaFlowProxyUrl] = useState(''); const [mediaFlowApiPassword, setmediaFlowApiPassword] = useState(''); const [mediaFlowPublicIp, setMediaFlowPublicIp] = useState(''); const [disableButtons, setDisableButtons] = useState(false); - const [manualManifestUrl, setManualManifestUrl] = useState(null); + const [manualManifestUrl, setManualManifestUrl] = useState( + null + ); const getChoosableAddons = () => { // only if torbox service is enabled we can use torbox addon @@ -288,14 +298,18 @@ export default function Configure() { mediaFlowEnabled, proxyUrl: mediaFlowProxyUrl, apiPassword: mediaFlowApiPassword, - publicIp: mediaFlowPublicIp + publicIp: mediaFlowPublicIp, }, addons, services, }; }; - const fetchWithTimeout = async (url: string, options: RequestInit | undefined, timeoutMs = 5000) => { + const fetchWithTimeout = async ( + url: string, + options: RequestInit | undefined, + timeoutMs = 5000 + ) => { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), timeoutMs); try { @@ -309,42 +323,61 @@ export default function Configure() { } }; - const getManifestUrl = async (protocol = window.location.protocol, root = window.location.host) => { + const getManifestUrl = async ( + protocol = window.location.protocol, + root = window.location.host + ) => { const config = createConfig(); console.log('Config', config); setDisableButtons(true); - - + try { - const encryptPath = `/encrypt-user-data`; - const response = await fetchWithTimeout(encryptPath, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ data: JSON.stringify(config) }), - }); - if (!response) { - throw new Error('encrypt-user-data failed: no response within timeout'); - } - if (!response.ok) { - throw new Error(`encrypt-user-data failed with status ${response.status}`); - } + const encryptPath = `/encrypt-user-data`; + const response = await fetchWithTimeout(encryptPath, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ data: JSON.stringify(config) }), + }); + if (!response) { + throw new Error('encrypt-user-data failed: no response within timeout'); + } + if (!response.ok) { + throw new Error( + `encrypt-user-data failed with status ${response.status}` + ); + } - const data = await response.json(); - if (!data.success) throw new Error(`Encryption service failed, ${data.message}`); + const data = await response.json(); + if (!data.success) + throw new Error(`Encryption service failed, ${data.message}`); - const encryptedConfig = data.data; - return { success: true, manifest: `${protocol}//${root}/${encryptedConfig}/manifest.json` }; + const encryptedConfig = data.data; + return { + success: true, + manifest: `${protocol}//${root}/${encryptedConfig}/manifest.json`, + }; } catch (error: any) { - console.error('Error during encryption:', error.message, '\nFalling back to base64 encoding'); - try { - const base64Config = btoa(JSON.stringify(config)); - return { success: true, manifest: `${protocol}//${root}/${base64Config}/manifest.json` }; - } catch (base64Error: any) { - console.error('Error during base64 encoding:', base64Error.message); - return { success: false, manifest: null, message: 'Failed to encode config' }; - } + console.error( + 'Error during encryption:', + error.message, + '\nFalling back to base64 encoding' + ); + try { + const base64Config = btoa(JSON.stringify(config)); + return { + success: true, + manifest: `${protocol}//${root}/${base64Config}/manifest.json`, + }; + } catch (base64Error: any) { + console.error('Error during base64 encoding:', base64Error.message); + return { + success: false, + manifest: null, + message: 'Failed to encode config', + }; + } } -}; + }; const createAndValidateConfig = () => { const config = createConfig(); @@ -408,7 +441,9 @@ export default function Configure() { } }; - const handleInstallToWeb = async (event: React.MouseEvent) => { + const handleInstallToWeb = async ( + event: React.MouseEvent + ) => { event.preventDefault(); if (createAndValidateConfig()) { const id = toast.loading('Generating manifest URL...', toastOptions); @@ -449,7 +484,9 @@ export default function Configure() { autoClose: 5000, isLoading: false, }); - setManualManifestUrl(`https://web.stremio.com/#/addons?addon=${encodedManifestUrl}`); + setManualManifestUrl( + `https://web.stremio.com/#/addons?addon=${encodedManifestUrl}` + ); } setDisableButtons(false); } @@ -470,16 +507,19 @@ export default function Configure() { setDisableButtons(false); return; } - navigator.clipboard.writeText(manifestUrl.manifest).then(() => { - toast.update(id, { - render: 'Manifest URL copied to clipboard', - type: 'success', - autoClose: 5000, - toastId: 'copiedManifestUrl', - isLoading: false, - }); - setManualManifestUrl(null); - }).catch((err: any) => { + navigator.clipboard + .writeText(manifestUrl.manifest) + .then(() => { + toast.update(id, { + render: 'Manifest URL copied to clipboard', + type: 'success', + autoClose: 5000, + toastId: 'copiedManifestUrl', + isLoading: false, + }); + setManualManifestUrl(null); + }) + .catch((err: any) => { console.error('Failed to copy manifest URL to clipboard', err); toast.update(id, { render: 'Failed to copy manifest URL to clipboard.', @@ -488,7 +528,7 @@ export default function Configure() { isLoading: false, }); setManualManifestUrl(manifestUrl.manifest); - }); + }); setDisableButtons(false); } }; @@ -513,9 +553,11 @@ export default function Configure() { return defaultSortCriteria; } return defaultSortCriteria.map((defaultSort) => { - // we find the one with the same key in the first index. + // we find the one with the same key in the first index. // and we only load direction if it exists in the defaultSort - const sort = sortCriteria.find((s) => Object.keys(s)[0] === Object.keys(defaultSort)[0]); + const sort = sortCriteria.find( + (s) => Object.keys(s)[0] === Object.keys(defaultSort)[0] + ); if (sort && defaultSort.direction) { return { ...sort, direction: sort.direction || defaultSort.direction }; } @@ -555,7 +597,7 @@ export default function Configure() { }; // Load config from the window path if it exists - useEffect(() => { + useEffect(() => { setIsClient(true); async function decodeConfig(config: string) { let decodedConfig: Config; @@ -570,10 +612,7 @@ export default function Configure() { function loadFromConfig(decodedConfig: Config) { console.log('Loaded config', decodedConfig); setResolutions( - loadValidValuesFromObject( - decodedConfig.resolutions, - defaultResolutions - ) + loadValidValuesFromObject(decodedConfig.resolutions, defaultResolutions) ); setQualities( loadValidValuesFromObject(decodedConfig.qualities, defaultQualities) @@ -587,12 +626,11 @@ export default function Configure() { setEncodes( loadValidValuesFromObject(decodedConfig.encodes, defaultEncodes) ); - setSortCriteria( - loadValidSortCriteria(decodedConfig.sortBy) - ); + setSortCriteria(loadValidSortCriteria(decodedConfig.sortBy)); setOnlyShowCachedStreams(decodedConfig.onlyShowCachedStreams || false); // create an array for prioritised languages. if the old prioritiseLanguage is set, add it to the array - const finalPrioritisedLanguages = decodedConfig.prioritisedLanguages || []; + const finalPrioritisedLanguages = + decodedConfig.prioritisedLanguages || []; if (decodedConfig.prioritiseLanguage) { finalPrioritisedLanguages.push(decodedConfig.prioritiseLanguage); } @@ -626,7 +664,9 @@ export default function Configure() { setAddonNameInDescription(decodedConfig.addonNameInDescription || false); setCleanResults(decodedConfig.cleanResults || false); setMaxResultsPerResolution(decodedConfig.maxResultsPerResolution || null); - setMediaFlowEnabled(decodedConfig.mediaFlowConfig?.mediaFlowEnabled || false); + setMediaFlowEnabled( + decodedConfig.mediaFlowConfig?.mediaFlowEnabled || false + ); setmediaFlowProxyUrl(decodedConfig.mediaFlowConfig?.proxyUrl || ''); setmediaFlowApiPassword(decodedConfig.mediaFlowConfig?.apiPassword || ''); } @@ -659,16 +699,28 @@ export default function Configure() {

AIOStreams

v{version} - {process.env.NEXT_PUBLIC_BRANDING &&
} + {process.env.NEXT_PUBLIC_BRANDING && ( +
+ )}

AIOStreams, the all-in-one streaming addon for Stremio. Combine your streams from all your addons into one and filter them by resolution, quality, visual tags and more. -

- This addon will return any result from the addons you enable. These can be P2P results, direct links, or anything else. - Results that are P2P are marked as P2P, however. -

- This addon also has no persistence. Nothing you enter here is stored. They are encrypted within the manifest URL and are only used to retrieve streams from any addons you enable. +
+
+ This addon will return any result from the addons you enable. These + can be P2P results, direct links, or anything else. Results that are + P2P are marked as P2P, however. +
+
+ This addon also has no persistence. Nothing you enter here is + stored. They are encrypted within the manifest URL and are only used + to retrieve streams from any addons you enable.

Made by Viren070. Source code on{' '} @@ -682,7 +734,7 @@ export default function Configure() {

- +

Services

@@ -733,7 +785,10 @@ export default function Configure() { const [movedService] = newServices.splice(serviceIndex, 1); if (direction === 'up' && serviceIndex > 0) { newServices.splice(serviceIndex - 1, 0, movedService); - } else if (direction === 'down' && serviceIndex < newServices.length) { + } else if ( + direction === 'down' && + serviceIndex < newServices.length + ) { newServices.splice(serviceIndex + 1, 0, movedService); } setServices(newServices); @@ -836,72 +891,110 @@ export default function Configure() {

-

Languages -

-

- Choose which languages you want to prioritise and exclude from the results +

Languages

+

+ Choose which languages you want to prioritise and exclude from the + results

-

Prioritise Languages

-

- Any results that are detected to have one of the prioritised languages will be sorted according to your sort criteria. - You must have the Langage sort criteria enabled for this to work. - If there are multiple results with a different prioritised language, the order is determined by the order of the prioritised languages. +

Prioritise Languages

+

+ Any results that are detected to have one of the prioritised + languages will be sorted according to your sort criteria. You + must have the Langage sort criteria enabled for + this to work. If there are multiple results with a different + prioritised language, the order is determined by the order of + the prioritised languages.

{isClient ? ( // https://github.com/JedWatson/react-select/issues/5859 - a.localeCompare(b)) + .map((language) => ({ value: language, label: language }))} + value={ + prioritisedLanguages?.map((language) => ({ + value: language, + label: language, + })) || [] + } + onChange={(selectedOptions: any) => { + const selectedLanguages = selectedOptions.map( + (option: any) => option.value + ); + setPrioritisedLanguages(selectedLanguages || null); + }} + styles={selectStyles} + /> + ) : ( + // render a fake select box until the actual one is rendered +
+

Select...

+
+ )}
-
+
-

Exclude Languages

-

+

Exclude Languages

+

Any results that are detected to have an excluded language will - be removed from the results. A result will only be excluded if - it only has one of or more of the excluded languages. If it contains a - language that is not excluded, it will still be included. + be removed from the results. A result will only be excluded if + it only has one of or more of the excluded languages. If it + contains a language that is not excluded, it will still be + included.

{isClient ? ( - a.localeCompare(b)) + .map((language) => ({ value: language, label: language }))} + value={ + excludedLanguages?.map((language) => ({ + value: language, + label: language, + })) || [] + } + onChange={(selectedOptions: any) => { + const selectedLanguages = selectedOptions.map( + (option: any) => option.value + ); + setExcludedLanguages(selectedLanguages || null); + }} + styles={selectStyles} + /> + ) : ( + // render a fake select box until the actual one is rendered +
+

Select...

+
+ )}
@@ -911,7 +1004,8 @@ export default function Configure() {

Size Filter

- Filter streams by size. Leave the maximum and minimum size sliders at opposite ends to disable the filter. + Filter streams by size. Leave the maximum and minimum size + sliders at opposite ends to disable the filter.

@@ -927,7 +1021,9 @@ export default function Configure() {

Limit results per resolution

- Limit the number of results per resolution. Leave empty to show all results. + Limit the number of results per resolution. Leave empty to show + all results.

-
setMaxResultsPerResolution(e.target.value ? parseInt(e.target.value) : null)} + onChange={(e) => + setMaxResultsPerResolution( + e.target.value ? parseInt(e.target.value) : null + ) + } style={{ marginLeft: 'auto', marginRight: '20px', @@ -1018,8 +1120,10 @@ export default function Configure() {

Move Addon Name to Description

- Move the addon name to the description of the stream. This will show AIOStreams as the stream title, - but move the name of the addon that the stream is from to the description. This is useful for Vidi users. + Move the addon name to the description of the stream. This will + show AIOStreams as the stream title, but move the + name of the addon that the stream is from to the description. + This is useful for Vidi users.

@@ -1037,14 +1141,18 @@ export default function Configure() { />
-
+

Clean Results

- Attempt to remove duplicate results. For a given file with duplicate streams: one uncached stream from all uncached streams is selected per provider. One cached stream from only one provider is selected. For duplicates without a provider, one stream is selected at random. + Attempt to remove duplicate results. For a given file with + duplicate streams: one uncached stream from all uncached streams + is selected per provider. One cached stream from only one + provider is selected. For duplicates without a provider, one + stream is selected at random.

@@ -1088,70 +1196,69 @@ export default function Configure() { />
- {( -
-
+ { +
-

Proxy URL

-

- The URL of the MediaFlow proxy server -

+
+

Proxy URL

+

+ The URL of the MediaFlow proxy server +

+
+
+ +
- +
+

API Password

+

+ Your MediaFlow's API password +

+
+
+ +
+
+
+
+

Public IP (Optional)

+

+ Configure this only when running MediaFlow locally with a + proxy service. Leave empty if MediaFlow is configured + locally without a proxy server or if it's hosted on a + remote server. +

+
+
+ +
-
-
-

API Password

-

- Your MediaFlow's API password -

-
-
- -
-
-
-
-

Public IP (Optional)

-

- Configure this only when running MediaFlow locally with a proxy service. Leave empty if MediaFlow is configured locally without a proxy server or if it's hosted on a remote server. -

-
-
- -
-
-
- )} + }
@@ -1178,17 +1285,24 @@ export default function Configure() { {manualManifestUrl && ( <> -
-

- If the above buttons do not work, you can use the following manifest URL to install the addon. -

- -
+
+

+ If the above buttons do not work, you can use the following + manifest URL to install the addon. +

+ +
)}
diff --git a/packages/frontend/src/components/AddonsList.module.css b/packages/frontend/src/components/AddonsList.module.css index 2eca649f..3e998976 100644 --- a/packages/frontend/src/components/AddonsList.module.css +++ b/packages/frontend/src/components/AddonsList.module.css @@ -53,8 +53,6 @@ align-items: center; } - - .cardBody { display: flex; flex-direction: column; @@ -110,4 +108,4 @@ .actionButton:active { transform: scale(0.9); -} \ No newline at end of file +} diff --git a/packages/frontend/src/components/AddonsList.tsx b/packages/frontend/src/components/AddonsList.tsx index a13e9db0..da882655 100644 --- a/packages/frontend/src/components/AddonsList.tsx +++ b/packages/frontend/src/components/AddonsList.tsx @@ -182,8 +182,8 @@ const AddonsList: React.FC = ({ )} {option.description && {option.description}} - {option.type === 'text' && ( - option.id.toLowerCase().includes('url') ? ( + {option.type === 'text' && + (option.id.toLowerCase().includes('url') ? ( @@ -203,8 +203,7 @@ const AddonsList: React.FC = ({ } className={styles.textInput} /> - ) - )} + ))} {option.type === 'select' && ( setCredential(e.target.value.trim())} - className={styles.credentialInput} - {...inputProps} - disabled={isEncrypted(credential) ? true : inputProps.disabled || false} + type={isEncrypted(credential) ? 'password' : 'text'} + value={ + isEncrypted(credential) ? 'โ€ขโ€ขโ€ขโ€ขโ€ขโ€ขโ€ขโ€ขโ€ขโ€ขโ€ขโ€ขโ€ขโ€ขโ€ขโ€ขโ€ขโ€ขโ€ขโ€ขโ€ขโ€ขโ€ขโ€ข' : credential + } + onChange={(e) => setCredential(e.target.value.trim())} + className={styles.credentialInput} + {...inputProps} + disabled={isEncrypted(credential) ? true : inputProps.disabled || false} /> {isEncrypted(credential) && ( - + + + + + + + )}
); diff --git a/packages/frontend/src/components/ServiceInput.tsx b/packages/frontend/src/components/ServiceInput.tsx index a887d6ca..2c21db71 100644 --- a/packages/frontend/src/components/ServiceInput.tsx +++ b/packages/frontend/src/components/ServiceInput.tsx @@ -118,25 +118,26 @@ const ServiceInput: React.FC = ({ {fields.map((field, index) => (
= ({ const toggleDirection = (itemKey: string) => { const newItems = items.map((item) => { if (Object.keys(item)[0] === itemKey) { - return { [itemKey]: item[itemKey], direction: item.direction === 'asc' ? 'desc' : 'asc' }; + return { + [itemKey]: item[itemKey], + direction: item.direction === 'asc' ? 'desc' : 'asc', + }; } return item; }); setItems(newItems); - } + }; return (
{items.map((item, index) => { const itemKey = Object.keys(item)[0]; - const isEnabled = typeof item[itemKey] === 'boolean' ? item[itemKey] : false; + const isEnabled = + typeof item[itemKey] === 'boolean' ? item[itemKey] : false; const directionChangeable = item.direction; return (
-
-
- {index > 0 && ( - - )} - {index < items.length - 1 && ( - + )} + {index < items.length - 1 && ( + - )} + + {' '} + {' '} + {' '} + + + + )} +
+ +

{itemKey}

+
+ toggleItem(itemKey)} + className={styles.checkbox} + />
- -

{itemKey}

-
- toggleItem(itemKey)} - className={styles.checkbox} - /> - -
- {directionChangeable && - ( -
+ {directionChangeable && ( +
)} diff --git a/packages/parser/src/regex.ts b/packages/parser/src/regex.ts index d327bdcc..a6e80d3b 100644 --- a/packages/parser/src/regex.ts +++ b/packages/parser/src/regex.ts @@ -24,11 +24,13 @@ export const PARSE_REGEX = { SCR: /(? { const padding = blockSize - (data.length % blockSize); @@ -40,7 +40,9 @@ export const decryptAndDecompress = ( ): string => { const secretKey = Settings.SECRET_KEY; if (!secretKey) { - console.error('|ERR| crypto > decryptAndDecompress > No secret key provided'); + console.error( + '|ERR| crypto > decryptAndDecompress > No secret key provided' + ); throw new Error('No secret key provided'); } const decipher = createDecipheriv('aes-256-cbc', secretKey, iv); @@ -60,8 +62,6 @@ export const decryptAndDecompress = ( return decompressedData.toString('utf-8'); }; - - export function parseAndDecryptString(data: string): string | null { try { if (data.startsWith('E-')) { @@ -72,7 +72,9 @@ export function parseAndDecryptString(data: string): string | null { } return data; } catch (error: any) { - console.error(`|ERR| crypto > parseAndDecryptString > Failed to decrypt data: ${error.message}`); + console.error( + `|ERR| crypto > parseAndDecryptString > Failed to decrypt data: ${error.message}` + ); return null; } -} \ No newline at end of file +} diff --git a/packages/utils/src/details.ts b/packages/utils/src/details.ts index 817e056b..3b438937 100644 --- a/packages/utils/src/details.ts +++ b/packages/utils/src/details.ts @@ -1,6 +1,5 @@ import { AddonDetail } from '@aiostreams/types'; -import {Settings} from './settings'; - +import { Settings } from './settings'; export const addonDetails: AddonDetail[] = [ { @@ -38,9 +37,9 @@ export const addonDetails: AddonDetail[] = [ required: false, label: 'Use Multiple Instances', description: - 'Torrentio supports multiple services. By default, AIOStreams will pass all services to Torrentio. However, Torrentio has its\ - own service priority system which can\'t be overriden. If you want to use a custom priority system for your services, you can enable this option.\ - to create a separate request for each service.', + "Torrentio supports multiple services. By default, AIOStreams will pass all services to Torrentio. However, Torrentio has its\ + own service priority system which can't be overriden. If you want to use a custom priority system for your services, you can enable this option.\ + to create a separate request for each service.", type: 'checkbox', }, { @@ -238,8 +237,8 @@ export const addonDetails: AddonDetail[] = [ min: Settings.MIN_TIMEOUT, max: Settings.MAX_TIMEOUT, }, - } - ] + }, + ], }, { name: 'Easynews+', @@ -274,8 +273,8 @@ export const addonDetails: AddonDetail[] = [ min: Settings.MIN_TIMEOUT, max: Settings.MAX_TIMEOUT, }, - } - ] + }, + ], }, { name: 'Stremio GDrive', @@ -502,7 +501,8 @@ export const serviceDetails = [ knownNames: ['SR', 'Seedr', 'SDR'], credentials: [ { - label: 'Encoded Token. Please authorise at MediaFusion and copy the token into here.', + label: + 'Encoded Token. Please authorise at MediaFusion and copy the token into here.', id: 'apiKey', }, ], diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index a96c7869..58bfa1a3 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -1,4 +1,4 @@ export * from './crypto'; export * from './details'; export * from './settings'; -export * from './mediaflow'; \ No newline at end of file +export * from './mediaflow'; diff --git a/packages/utils/src/mediaflow.ts b/packages/utils/src/mediaflow.ts index a11ac6d8..dd9db0af 100644 --- a/packages/utils/src/mediaflow.ts +++ b/packages/utils/src/mediaflow.ts @@ -1,64 +1,81 @@ -import { Config } from "@aiostreams/types"; -import path from "path"; +import { Config } from '@aiostreams/types'; +import path from 'path'; const PRIVATE_CIDR = /^(10\.|127\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\.)/; -export function createProxiedMediaFlowUrl(url: string, mediaFlowConfig: Config["mediaFlowConfig"], headers?: - { request?: Record, response?: Record }) { - const streamUrl = url; - const mediaFlowUrl = mediaFlowConfig?.proxyUrl?.replace(/\/$/, ''); - const mediaFlowApiPassword = mediaFlowConfig?.apiPassword; - if (!streamUrl) { - console.error('|ERR| mediaflow > createProxiedMediaFlowUrl > streamUrl is missing, could not create proxied URL'); - throw new Error('Stream URL is missing'); - } - if (!mediaFlowConfig) { - console.error('|ERR| mediaflow > createProxiedMediaFlowUrl > mediaFlowConfig is missing'); - throw new Error('MediaFlow configuration is missing'); - } - if (!mediaFlowUrl || !mediaFlowApiPassword) { - console.error('|ERR| mediaflow > createProxiedMediaFlowUrl > mediaFlowUrl or API password is missing'); - throw new Error('MediaFlow URL or API password is missing'); - } +export function createProxiedMediaFlowUrl( + url: string, + mediaFlowConfig: Config['mediaFlowConfig'], + headers?: { + request?: Record; + response?: Record; + } +) { + const streamUrl = url; + const mediaFlowUrl = mediaFlowConfig?.proxyUrl?.replace(/\/$/, ''); + const mediaFlowApiPassword = mediaFlowConfig?.apiPassword; + if (!streamUrl) { + console.error( + '|ERR| mediaflow > createProxiedMediaFlowUrl > streamUrl is missing, could not create proxied URL' + ); + throw new Error('Stream URL is missing'); + } + if (!mediaFlowConfig) { + console.error( + '|ERR| mediaflow > createProxiedMediaFlowUrl > mediaFlowConfig is missing' + ); + throw new Error('MediaFlow configuration is missing'); + } + if (!mediaFlowUrl || !mediaFlowApiPassword) { + console.error( + '|ERR| mediaflow > createProxiedMediaFlowUrl > mediaFlowUrl or API password is missing' + ); + throw new Error('MediaFlow URL or API password is missing'); + } - const queryParams: Record = { - api_password: mediaFlowApiPassword, - } - queryParams.d = streamUrl; + const queryParams: Record = { + api_password: mediaFlowApiPassword, + }; + queryParams.d = streamUrl; - const responseHeaders = headers?.response || { - "Content-Disposition": `attachment; filename=${path.basename(streamUrl)}` - }; - const requestHeaders = headers?.request || {}; + const responseHeaders = headers?.response || { + 'Content-Disposition': `attachment; filename=${path.basename(streamUrl)}`, + }; + const requestHeaders = headers?.request || {}; - if (requestHeaders) { - Object.entries(requestHeaders).forEach(([key, value]) => { - queryParams[`h_${key}`] = value; - }); - } + if (requestHeaders) { + Object.entries(requestHeaders).forEach(([key, value]) => { + queryParams[`h_${key}`] = value; + }); + } - if (responseHeaders) { - Object.entries(responseHeaders).forEach(([key, value]) => { - queryParams[`r_${key}`] = value; - }); - } - - const encodedParams = new URLSearchParams(queryParams).toString(); - const baseUrl = new URL('/proxy/stream', mediaFlowUrl).toString(); - const proxiedUrl = `${baseUrl}?${encodedParams}`; - //console.debug(`|DBG| mediaflow > createProxiedMediaFlowUrl > Proxied URL: ${proxiedUrl.replace(mediaFlowApiPassword, '***').replace(streamUrl, '***')}`); - return proxiedUrl; + if (responseHeaders) { + Object.entries(responseHeaders).forEach(([key, value]) => { + queryParams[`r_${key}`] = value; + }); + } + + const encodedParams = new URLSearchParams(queryParams).toString(); + const baseUrl = new URL('/proxy/stream', mediaFlowUrl).toString(); + const proxiedUrl = `${baseUrl}?${encodedParams}`; + //console.debug(`|DBG| mediaflow > createProxiedMediaFlowUrl > Proxied URL: ${proxiedUrl.replace(mediaFlowApiPassword, '***').replace(streamUrl, '***')}`); + return proxiedUrl; } - -export async function getMediaFlowPublicIp(mediaFlowConfig: Config["mediaFlowConfig"]) { +export async function getMediaFlowPublicIp( + mediaFlowConfig: Config['mediaFlowConfig'] +) { if (!mediaFlowConfig) { - console.error('|ERR| mediaflow > getMediaFlowPublicIp > mediaFlowConfig is missing'); + console.error( + '|ERR| mediaflow > getMediaFlowPublicIp > mediaFlowConfig is missing' + ); throw new Error('MediaFlow configuration is missing'); } const mediaFlowUrl = mediaFlowConfig?.proxyUrl?.replace(/\/$/, ''); if (!mediaFlowUrl) { - console.error('|ERR| mediaflow > getMediaFlowPublicIp > mediaFlowUrl is missing'); + console.error( + '|ERR| mediaflow > getMediaFlowPublicIp > mediaFlowUrl is missing' + ); throw new Error('MediaFlow URL is missing'); } if (mediaFlowConfig.publicIp) { @@ -68,20 +85,29 @@ export async function getMediaFlowPublicIp(mediaFlowConfig: Config["mediaFlowCon const parsedUrl = new URL(mediaFlowUrl); if (PRIVATE_CIDR.test(parsedUrl.hostname)) { // MediaFlow proxy URL is a private IP address - console.debug('|DBG| mediaflow > getMediaFlowPublicIp > MediaFlow proxy URL is a private IP address so returning null'); + console.debug( + '|DBG| mediaflow > getMediaFlowPublicIp > MediaFlow proxy URL is a private IP address so returning null' + ); return null; } try { - console.debug('|DBG| mediaflow > getMediaFlowPublicIp > GET /proxy/ip?api_password=***'); - const response = await fetch(new URL(`/proxy/ip?api_password=${encodeURIComponent(mediaFlowConfig.apiPassword)}`, mediaFlowUrl).toString(), { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - }, - signal: AbortSignal.timeout(5000), - - }); + console.debug( + '|DBG| mediaflow > getMediaFlowPublicIp > GET /proxy/ip?api_password=***' + ); + const response = await fetch( + new URL( + `/proxy/ip?api_password=${encodeURIComponent(mediaFlowConfig.apiPassword)}`, + mediaFlowUrl + ).toString(), + { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + signal: AbortSignal.timeout(5000), + } + ); if (!response.ok) { throw new Error(`${response.status}: ${response.statusText}`); @@ -90,14 +116,10 @@ export async function getMediaFlowPublicIp(mediaFlowConfig: Config["mediaFlowCon const data = await response.json(); const publicIp = data.ip; return publicIp; - } catch (error: any) { - console.error(`|ERR| mediaflow > getMediaFlowPublicIp > Failed to get public IP from MediaFlow - ${error.message}`); + console.error( + `|ERR| mediaflow > getMediaFlowPublicIp > Failed to get public IP from MediaFlow - ${error.message}` + ); return null; } - - - - - -} \ No newline at end of file +} diff --git a/packages/utils/src/settings.ts b/packages/utils/src/settings.ts index cbf2955c..e08a688d 100644 --- a/packages/utils/src/settings.ts +++ b/packages/utils/src/settings.ts @@ -7,32 +7,71 @@ try { console.error('Error loading .env file:', error); } - export class Settings { public static readonly ADDON_NAME = process.env.ADDON_NAME ?? 'AIOStreams'; - public static readonly ADDON_ID = process.env.ADDON_ID ?? 'aiostreams.viren070.com'; + public static readonly ADDON_ID = + process.env.ADDON_ID ?? 'aiostreams.viren070.com'; public static readonly PORT = process.env.PORT ?? 3000; - public static readonly BRANDING = process.env.BRANDING ?? process.env.NEXT_PUBLIC_ELFHOSTED_BRANDING + public static readonly BRANDING = + process.env.BRANDING ?? process.env.NEXT_PUBLIC_ELFHOSTED_BRANDING; public static readonly SECRET_KEY = process.env.SECRET_KEY ?? ''; - public static readonly COMET_URL = process.env.COMET_URL ?? 'https://comet.elfhosted.com/'; - 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 - public static readonly MAX_TIMEOUT = process.env.MAX_TIMEOUT ? parseInt(process.env.MAX_TIMEOUT) : 50000; - public static readonly MIN_TIMEOUT = process.env.MIN_TIMEOUT ? parseInt(process.env.MIN_TIMEOUT) : 1000; - public static readonly DEFAULT_TIMEOUT = process.env.DEFAULT_TIMEOUT ? parseInt(process.env.DEFAULT_TIMEOUT) : 15000; - public static readonly DEFAULT_TORRENTIO_TIMEOUT = process.env.DEFAULT_TORRENTIO_TIMEOUT ? parseInt(process.env.DEFAULT_TORRENTIO_TIMEOUT) : 5000; - 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; + public static readonly COMET_URL = + process.env.COMET_URL ?? 'https://comet.elfhosted.com/'; + 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 + public static readonly MAX_TIMEOUT = process.env.MAX_TIMEOUT + ? parseInt(process.env.MAX_TIMEOUT) + : 50000; + public static readonly MIN_TIMEOUT = process.env.MIN_TIMEOUT + ? parseInt(process.env.MIN_TIMEOUT) + : 1000; + public static readonly DEFAULT_TIMEOUT = process.env.DEFAULT_TIMEOUT + ? parseInt(process.env.DEFAULT_TIMEOUT) + : 15000; + public static readonly DEFAULT_TORRENTIO_TIMEOUT = process.env + .DEFAULT_TORRENTIO_TIMEOUT + ? parseInt(process.env.DEFAULT_TORRENTIO_TIMEOUT) + : 5000; + 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/utils/tsconfig.json b/packages/utils/tsconfig.json index 511d8bf0..15094e48 100644 --- a/packages/utils/tsconfig.json +++ b/packages/utils/tsconfig.json @@ -1,8 +1,7 @@ { - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "outDir": "dist", - "rootDir": "src" - } + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src" } - \ No newline at end of file +} diff --git a/packages/wrappers/src/base.ts b/packages/wrappers/src/base.ts index 5f1e44a4..8e31599d 100644 --- a/packages/wrappers/src/base.ts +++ b/packages/wrappers/src/base.ts @@ -6,7 +6,11 @@ import { Config, } from '@aiostreams/types'; import { extractSizeInBytes, parseFilename } from '@aiostreams/parser'; -import { getMediaFlowPublicIp, serviceDetails, Settings } from '@aiostreams/utils'; +import { + getMediaFlowPublicIp, + serviceDetails, + Settings, +} from '@aiostreams/utils'; export class BaseWrapper { private readonly streamPath: string = 'stream/{type}/{id}.json'; @@ -69,20 +73,28 @@ export class BaseWrapper { if (this.userConfig.requestingIp) { headers.set('X-Forwarded-For', this.userConfig.requestingIp); headers.set('X-Real-IP', this.userConfig.requestingIp); - } + } if (this.userConfig.mediaFlowConfig?.mediaFlowEnabled) { - const mediaFlowIp = await getMediaFlowPublicIp(this.userConfig.mediaFlowConfig); + const mediaFlowIp = await getMediaFlowPublicIp( + this.userConfig.mediaFlowConfig + ); if (mediaFlowIp) { - console.log(`|DBG| wrappers > base > Forwarding IP from MediaFlow to ${this.addonName}`); + console.log( + `|DBG| wrappers > base > Forwarding IP from MediaFlow to ${this.addonName}` + ); headers.set('X-Forwarded-For', mediaFlowIp); headers.set('X-Real-IP', mediaFlowIp); } } else { - console.log(`|DBG| wrappers > base > Forwarding IP from request to ${this.addonName}`); + console.log( + `|DBG| wrappers > base > Forwarding IP from request to ${this.addonName}` + ); } const urlParts = url.split('/'); const sanitisedUrl = `${urlParts[0]}//${urlParts[2]}/*************/${urlParts.slice(-3).join('/')}`; - console.log(`|INF| wrappers > base > ${this.addonName}: GET ${sanitisedUrl}`); + console.log( + `|INF| wrappers > base > ${this.addonName}: GET ${sanitisedUrl}` + ); const response = await fetch(url, { headers: headers, signal: controller.signal, @@ -127,7 +139,9 @@ export class BaseWrapper { size: size, url: stream.url, externalUrl: stream.externalUrl, - _infoHash: stream.infoHash || (stream.url ? (stream.url.match(/[a-fA-F0-9]{40}/)?.[0]) : undefined), + _infoHash: + stream.infoHash || + (stream.url ? stream.url.match(/[a-fA-F0-9]{40}/)?.[0] : undefined), torrent: { infoHash: stream.infoHash, fileIdx: stream.fileIdx, @@ -145,12 +159,14 @@ export class BaseWrapper { behaviorHints: { countryWhitelist: stream.behaviorHints?.countryWhitelist, notWebReady: stream.behaviorHints?.notWebReady, - proxyHeaders: stream.behaviorHints?.proxyHeaders?.request || stream.behaviorHints?.proxyHeaders?.response - ? { - request: stream.behaviorHints?.proxyHeaders?.request, - response: stream.behaviorHints?.proxyHeaders?.response, - } - : undefined, + proxyHeaders: + stream.behaviorHints?.proxyHeaders?.request || + stream.behaviorHints?.proxyHeaders?.response + ? { + request: stream.behaviorHints?.proxyHeaders?.request, + response: stream.behaviorHints?.proxyHeaders?.response, + } + : undefined, videoHash: stream.behaviorHints?.videoHash, }, }, @@ -165,7 +181,9 @@ export class BaseWrapper { let description = stream.description || stream.title; if (!filename && description) { - console.log(`|DBG| wrappers > base > parseStream: No filename found in behaviorHints, attempting to parse from description`); + console.log( + `|DBG| wrappers > base > parseStream: No filename found in behaviorHints, attempting to parse from description` + ); const lines = description.split('\n'); filename = lines.find( @@ -174,9 +192,13 @@ export class BaseWrapper { /(? base > parseStream: With description: ${description}, found filename: ${filename}`); + console.log( + `|DBG| wrappers > base > parseStream: With description: ${description}, found filename: ${filename}` + ); } else if (!description) { - console.log(`|WRN| wrappers > base > parseStream: No description found, filename could not be determined`); + console.log( + `|WRN| wrappers > base > parseStream: No description found, filename could not be determined` + ); } let parsedInfo: ParsedNameData = parseFilename(filename || ''); @@ -187,11 +209,9 @@ export class BaseWrapper { stream.behaviorHints?.videoSize || stream.size || stream.sizebytes || - description && extractSizeInBytes(description, 1024) || - stream.name && extractSizeInBytes(stream.name, 1024) || + (description && extractSizeInBytes(description, 1024)) || + (stream.name && extractSizeInBytes(stream.name, 1024)) || undefined; - - // look for seeders let seeders: string | undefined; @@ -202,10 +222,10 @@ export class BaseWrapper { // look for indexer let indexer: string | undefined; if (description) { - const indexerMatch = RegExp(/[๐ŸŒโš™๏ธ๐Ÿ”—] ([^\s\p{Emoji_Presentation}]+(?:\s[^\s\p{Emoji_Presentation}]+)*)/u).exec( - description || '' - ); - indexer = indexerMatch ? indexerMatch[1] : undefined + const indexerMatch = RegExp( + /[๐ŸŒโš™๏ธ๐Ÿ”—] ([^\s\p{Emoji_Presentation}]+(?:\s[^\s\p{Emoji_Presentation}]+)*)/u + ).exec(description || ''); + indexer = indexerMatch ? indexerMatch[1] : undefined; } // look for providers diff --git a/packages/wrappers/src/comet.ts b/packages/wrappers/src/comet.ts index 884c6ae3..a44e79f3 100644 --- a/packages/wrappers/src/comet.ts +++ b/packages/wrappers/src/comet.ts @@ -21,8 +21,7 @@ export class Comet extends BaseWrapper { ) { let url = overrideUrl ? overrideUrl - : Settings.COMET_URL + - (configString ? configString + '/' : ''); + : Settings.COMET_URL + (configString ? configString + '/' : ''); super(addonName, url, indexerTimeout, addonId, userConfig); } @@ -45,7 +44,10 @@ export class Comet extends BaseWrapper { const debridMatch = RegExp(/^\[([a-zA-Z]{2})(\โšก)\]/).exec(stream.name!); const debrid = debridMatch ? { - id: serviceDetails.find((service) => service.knownNames.includes(debridMatch[1]))?.id || debridMatch[1], + id: + serviceDetails.find((service) => + service.knownNames.includes(debridMatch[1]) + )?.id || debridMatch[1], cached: debridMatch[2] === 'โšก', } : undefined; @@ -96,7 +98,7 @@ export async function getCometStreams( overrideName?: string; }, streamRequest: StreamRequest, - addonId: string, + addonId: string ): Promise { const supportedServices: string[] = addonDetails.find((addon: AddonDetail) => addon.id === 'comet') @@ -114,15 +116,14 @@ export async function getCometStreams( indexerTimeout, cometOptions.overrideName, addonId, - config, + config ); return comet.getParsedStreams(streamRequest); } // find all usable and enabled services const usableServices = config.services.filter( - (service) => - supportedServices.includes(service.id) && service.enabled + (service) => supportedServices.includes(service.id) && service.enabled ); // if no usable services found, throw an error diff --git a/packages/wrappers/src/easynews.ts b/packages/wrappers/src/easynews.ts index ba741b77..3d7e76e2 100644 --- a/packages/wrappers/src/easynews.ts +++ b/packages/wrappers/src/easynews.ts @@ -1,11 +1,14 @@ import { AddonDetail, ParsedNameData, StreamRequest } from '@aiostreams/types'; -import { parseFilename, extractSizeInBytes, extractDurationInMs } from '@aiostreams/parser'; +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, @@ -17,26 +20,26 @@ export class Easynews extends BaseWrapper { ) { let url = overrideUrl ? overrideUrl - : Settings.EASYNEWS_URL + - (configString ? configString + '/' : ''); + : Settings.EASYNEWS_URL + (configString ? configString + '/' : ''); super(addonName, url, indexerTimeout, addonId, userConfig); } protected parseStream(stream: Stream): ParsedStream { - const [filename, sizeString, durationString] = stream.description?.split('\n') || []; + const [filename, sizeString, durationString] = + stream.description?.split('\n') || []; const parsedFilename: ParsedNameData = parseFilename( filename || stream.description || '' ); const sizeInBytes = stream.description - ? extractSizeInBytes(sizeString, 1024) - : undefined; + ? extractSizeInBytes(sizeString, 1024) + : undefined; const provider = { - id: 'easynews', - cached: true, - } + id: 'easynews', + cached: true, + }; const durationInMs = extractDurationInMs(durationString || ''); @@ -56,7 +59,7 @@ export class Easynews extends BaseWrapper { } const getEasynewsConfigString = (username: string, password: string) => { - return `%7B%22username%22%3A%22${username}%22%2C%22password%22%3A%22${password}%22%7D` + return `%7B%22username%22%3A%22${username}%22%2C%22password%22%3A%22${password}%22%7D`; }; export async function getEasynewsStreams( @@ -67,29 +70,37 @@ export async function getEasynewsStreams( indexerTimeout?: string; }, streamRequest: StreamRequest, - addonId: string, + 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'); + 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; + 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 easynewsConfigString = getEasynewsConfigString( + credentails.username, + credentails.password + ); const easynews = new Easynews( easynewsConfigString, easynewsOptions.overrideUrl ?? null, - easynewsOptions.indexerTimeout ? parseInt(easynewsOptions.indexerTimeout) : undefined, + easynewsOptions.indexerTimeout + ? parseInt(easynewsOptions.indexerTimeout) + : undefined, easynewsOptions.overrideName, addonId, config diff --git a/packages/wrappers/src/easynewsPlus.ts b/packages/wrappers/src/easynewsPlus.ts index 7b60e49a..0d5a6064 100644 --- a/packages/wrappers/src/easynewsPlus.ts +++ b/packages/wrappers/src/easynewsPlus.ts @@ -1,11 +1,14 @@ import { AddonDetail, ParsedNameData, StreamRequest } from '@aiostreams/types'; -import { parseFilename, extractSizeInBytes, extractDurationInMs } from '@aiostreams/parser'; +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, @@ -17,28 +20,28 @@ export class EasynewsPlus extends BaseWrapper { ) { let url = overrideUrl ? overrideUrl - : Settings.EASYNEWS_PLUS_URL + - (configString ? configString + '/' : ''); + : Settings.EASYNEWS_PLUS_URL + (configString ? configString + '/' : ''); super(addonName, url, indexerTimeout, addonId, userConfig); } protected parseStream(stream: Stream): ParsedStream { - const [filename, durationString, sizeString] = stream.description?.split('\n') || []; + const [filename, durationString, sizeString] = + stream.description?.split('\n') || []; const parsedFilename: ParsedNameData = parseFilename( filename || stream.description || '' ); - const sizeInBytes = stream.behaviorHints?.videoSize - ? stream.behaviorHints.videoSize - : sizeString + const sizeInBytes = stream.behaviorHints?.videoSize + ? stream.behaviorHints.videoSize + : sizeString ? extractSizeInBytes(sizeString, 1024) : undefined; const provider = { - id: 'easynews', - cached: true, - } + id: 'easynews', + cached: true, + }; const durationInMs = extractDurationInMs(durationString || ''); @@ -58,7 +61,7 @@ export class EasynewsPlus extends BaseWrapper { } 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` + 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( @@ -69,29 +72,37 @@ export async function getEasynewsPlusStreams( indexerTimeout?: string; }, streamRequest: StreamRequest, - addonId: string, + 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'); + 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; + 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 easynewsPlusConfigString = getEasynewsPlusConfigString( + credentails.username, + credentails.password + ); const easynews = new EasynewsPlus( easynewsPlusConfigString, easynewsPlusOptions.overrideUrl ?? null, - easynewsPlusOptions.indexerTimeout ? parseInt(easynewsPlusOptions.indexerTimeout) : undefined, + easynewsPlusOptions.indexerTimeout + ? parseInt(easynewsPlusOptions.indexerTimeout) + : undefined, easynewsPlusOptions.overrideName, addonId, config diff --git a/packages/wrappers/src/index.ts b/packages/wrappers/src/index.ts index 04d44aa6..ffb1e29e 100644 --- a/packages/wrappers/src/index.ts +++ b/packages/wrappers/src/index.ts @@ -4,4 +4,4 @@ export * from './torrentio'; export * from './comet'; export * from './mediafusion'; export * from './easynews'; -export * from './easynewsPlus'; \ No newline at end of file +export * from './easynewsPlus'; diff --git a/packages/wrappers/src/mediafusion.ts b/packages/wrappers/src/mediafusion.ts index 65faa429..35f55471 100644 --- a/packages/wrappers/src/mediafusion.ts +++ b/packages/wrappers/src/mediafusion.ts @@ -16,8 +16,7 @@ export class MediaFusion extends BaseWrapper { ) { let url = overrideUrl ? overrideUrl - : Settings.MEDIAFUSION_URL + - (configString ? configString + '/' : ''); + : Settings.MEDIAFUSION_URL + (configString ? configString + '/' : ''); super(addonName, url, indexerTimeout, addonId, userConfig); } @@ -27,7 +26,11 @@ export class MediaFusion extends BaseWrapper { stream.behaviorHints?.filename?.trim() || stream.description?.split('\n')[0].replace('๐Ÿ“‚ ', ''); - if (filename && stream.description && filename.includes('Content Warning')) { + if ( + filename && + stream.description && + filename.includes('Content Warning') + ) { filename = stream.description.split('\n').join(' '); } @@ -55,9 +58,9 @@ export class MediaFusion extends BaseWrapper { } : undefined; - const indexerMatch = RegExp(/๐Ÿ”— ([^\s\p{Emoji_Presentation}]+(?:\s[^\s\p{Emoji_Presentation}]+)*)/u).exec( - stream.description || '' - ); + const indexerMatch = RegExp( + /๐Ÿ”— ([^\s\p{Emoji_Presentation}]+(?:\s[^\s\p{Emoji_Presentation}]+)*)/u + ).exec(stream.description || ''); const indexer = indexerMatch ? indexerMatch[1] : undefined; const seedersMatch = RegExp(/๐Ÿ‘ค (\d+)/).exec(stream.description || ''); @@ -65,12 +68,14 @@ export class MediaFusion extends BaseWrapper { stream.description?.split('\n').forEach((line) => { if (line.startsWith('๐ŸŒ')) { - // the line contains the languages separated by ' + '. + // the line contains the languages separated by ' + '. // the languages can either be flag emojis or the language name. const normaliseLanguage = (lang: string) => { // convert emojis to language names, and uppercase the first letter of each word - return (emojiToLanguage(lang) || lang).replace(/\b\w/g, (char) => char.toUpperCase()); - } + return (emojiToLanguage(lang) || lang).replace(/\b\w/g, (char) => + char.toUpperCase() + ); + }; const languages = line.replace('๐ŸŒ ', '').split(' + '); languages.forEach((lang) => { const normalisedLanguage = normaliseLanguage(lang); @@ -104,7 +109,7 @@ export async function getMediafusionStreams( overrideName?: string; }, streamRequest: StreamRequest, - addonId: string, + addonId: string ): Promise { const supportedServices: string[] = addonDetails.find((addon: AddonDetail) => addon.id === 'mediafusion') @@ -129,15 +134,12 @@ export async function getMediafusionStreams( // find all usable and enabled services const usableServices = config.services.filter( - (service) => - supportedServices.includes(service.id) && service.enabled + (service) => supportedServices.includes(service.id) && service.enabled ); // if no usable services found, use mediafusion without debrid if (usableServices.length < 1) { - const configString = await getConfigString( - getMediaFusionConfig() - ); + const configString = await getConfigString(getMediaFusionConfig()); const mediafusion = new MediaFusion( configString, null, @@ -179,9 +181,7 @@ export async function getMediafusionStreams( debridService.id, debridService.credentials ); - const encryptedStr = await getConfigString( - mediafusionConfig - ); + const encryptedStr = await getConfigString(mediafusionConfig); const mediafusion = new MediaFusion( encryptedStr, null, @@ -204,9 +204,7 @@ export async function getMediafusionStreams( service.id, service.credentials ); - const encryptedStr = await getConfigString( - mediafusionConfig - ); + const encryptedStr = await getConfigString(mediafusionConfig); const mediafusion = new MediaFusion( encryptedStr, null, @@ -224,11 +222,14 @@ export async function getMediafusionStreams( return parsedStreams; } -const getMediaFusionConfig = (service?: string, credentials: { [key: string]: string } = {}): any => { +const getMediaFusionConfig = ( + service?: string, + credentials: { [key: string]: string } = {} +): any => { return { streaming_provider: service ? { - token: !["pikpak"].includes(service) ? credentials.apiKey : undefined, + token: !['pikpak'].includes(service) ? credentials.apiKey : undefined, email: credentials.email, password: credentials.password, service: service, @@ -313,9 +314,7 @@ const getMediaFusionConfig = (service?: string, credentials: { [key: string]: st }; }; -async function getConfigString( - data: any -): Promise { +async function getConfigString(data: any): Promise { const encryptUrl = `${Settings.MEDIAFUSION_URL}encrypt-user-data`; const response = await fetch(encryptUrl, { method: 'POST', diff --git a/packages/wrappers/src/torbox.ts b/packages/wrappers/src/torbox.ts index c0896553..25d824cf 100644 --- a/packages/wrappers/src/torbox.ts +++ b/packages/wrappers/src/torbox.ts @@ -46,26 +46,28 @@ export class Torbox extends BaseWrapper { protected parseStream(stream: TorboxStream): ParsedStream | undefined { let type = stream.type; - const [dQuality, dFilename, dSize, dLanguage, dAgeOrSeeders] = stream.description - .split('\n') - .map((field: string) => { + const [dQuality, dFilename, dSize, dLanguage, dAgeOrSeeders] = + stream.description.split('\n').map((field: string) => { if (field.startsWith('Type')) { // the last line can either contain only the type or the type and the seeders/age // we will always return the age or seeders and assign the type to the variable declared outside the map const parts = field.split('|'); - type = ['torrent', 'usenet', 'web'].includes(type || '') ? type : parts[0].split(':')[1].trim().toLowerCase(); + type = ['torrent', 'usenet', 'web'].includes(type || '') + ? type + : parts[0].split(':')[1].trim().toLowerCase(); if (parts.length > 1) { return parts[1].split(':')[1].trim(); - } + } // since the last line only contains the type, we will return undefined return undefined; - } const [_, value] = field.split(':'); return value.trim(); }); - const filename = stream.behaviorHints?.filename || dFilename - const parsedFilename: ParsedNameData = parseFilename(filename || stream.description); + const filename = stream.behaviorHints?.filename || dFilename; + const parsedFilename: ParsedNameData = parseFilename( + filename || stream.description + ); /* If the quality from Torbox is not one of the qualities in the Config, they get filtered out So, for now, we will not update the quality from Torbox @@ -74,31 +76,47 @@ export class Torbox extends BaseWrapper { parsedFilename.quality = quality; } */ - + const language = stream.language || dLanguage; const normaliseLanguage = (lang: string) => { if (lang.toLowerCase() === 'multi audio') { - return 'Multi'; + return 'Multi'; } - return lang.split(' ').map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join(' '); + return lang + .split(' ') + .map( + (word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase() + ) + .join(' '); }; if (language) { const normalisedLanguage = normaliseLanguage(language); - if (normalisedLanguage !== 'Unknown' && !parsedFilename.languages.includes(normalisedLanguage)) { + if ( + normalisedLanguage !== 'Unknown' && + !parsedFilename.languages.includes(normalisedLanguage) + ) { parsedFilename.languages.push(normalisedLanguage); } } // usenet results provide size as a string, we need to convert it to a number - const validateBehaviorHintSize = (size: string | number | undefined) => typeof size === 'string' ? parseInt(size) : size; - const sizeInBytes = stream.size || validateBehaviorHintSize(stream.behaviorHints?.videoSize) || (dSize ? extractSizeInBytes(dSize, 1000) : undefined) - + const validateBehaviorHintSize = (size: string | number | undefined) => + typeof size === 'string' ? parseInt(size) : size; + const sizeInBytes = + stream.size || + validateBehaviorHintSize(stream.behaviorHints?.videoSize) || + (dSize ? extractSizeInBytes(dSize, 1000) : undefined); + const provider = { id: 'torbox', cached: stream.is_cached, }; - const seeders = type === 'torrent' ? (stream.seeders || (dAgeOrSeeders ? parseInt(dAgeOrSeeders) : undefined)) : undefined; + const seeders = + type === 'torrent' + ? stream.seeders || + (dAgeOrSeeders ? parseInt(dAgeOrSeeders) : undefined) + : undefined; const age = type === 'usenet' ? dAgeOrSeeders || undefined : undefined; const parsedStream: ParsedStream = this.createParsedResult( diff --git a/packages/wrappers/src/torrentio.ts b/packages/wrappers/src/torrentio.ts index eeda35ac..b3a82191 100644 --- a/packages/wrappers/src/torrentio.ts +++ b/packages/wrappers/src/torrentio.ts @@ -2,7 +2,7 @@ import { AddonDetail, ParsedNameData, StreamRequest } from '@aiostreams/types'; import { parseFilename, extractSizeInBytes } from '@aiostreams/parser'; import { ParsedStream, Stream, Config } from '@aiostreams/types'; import { BaseWrapper } from './base'; -import { addonDetails, serviceDetails } from '@aiostreams/utils' +import { addonDetails, serviceDetails } from '@aiostreams/utils'; import { Settings } from '@aiostreams/utils'; import { emojiToLanguage } from '@aiostreams/formatters'; @@ -17,17 +17,16 @@ export class Torrentio extends BaseWrapper { ) { let url = overrideUrl ? overrideUrl - : Settings.TORRENTIO_URL + - (configString ? configString + '/' : ''); + : Settings.TORRENTIO_URL + (configString ? configString + '/' : ''); super(addonName, url, indexerTimeout, addonId, userConfig); } protected parseStream(stream: Stream): ParsedStream { - const filename = stream.title - ? stream.title.split('\n')[0] + const filename = stream.title + ? stream.title.split('\n')[0] : stream.behaviorHints?.filename?.trim(); - + const parsedFilename: ParsedNameData = parseFilename(filename || ''); const sizeInBytes = stream.title ? extractSizeInBytes(stream.title, 1024) @@ -37,22 +36,34 @@ export class Torrentio extends BaseWrapper { ); const debrid = debridMatch ? { - id: serviceDetails.find((service) => service.knownNames.includes(debridMatch[1]))?.id || debridMatch[1], + id: + serviceDetails.find((service) => + service.knownNames.includes(debridMatch[1]) + )?.id || debridMatch[1], cached: debridMatch[2] === '+', } : undefined; const seedersMatch = RegExp(/๐Ÿ‘ค (\d+)/).exec(stream.title!); const seeders = seedersMatch ? parseInt(seedersMatch[1]) : undefined; - const indexerMatch = RegExp(/โš™๏ธ (.+)/).exec(stream.title?.split('\n')[1] || ''); + const indexerMatch = RegExp(/โš™๏ธ (.+)/).exec( + stream.title?.split('\n')[1] || '' + ); const indexer = indexerMatch ? indexerMatch[1] : undefined; const lastLine = stream.title?.split('\n').pop(); - if (lastLine && !(lastLine.includes('๐Ÿ‘ค') && lastLine.includes('๐Ÿ’พ') && lastLine.includes('โš™๏ธ'))) { - // this line contains languages separated by ' / '. + if ( + lastLine && + !( + lastLine.includes('๐Ÿ‘ค') && + lastLine.includes('๐Ÿ’พ') && + lastLine.includes('โš™๏ธ') + ) + ) { + // this line contains languages separated by ' / '. const languages = lastLine.split(' / '); // 'Multi Audio' can be converted to 'Multi' - // other ones are flag emojis and need to be converted to languages. + // other ones are flag emojis and need to be converted to languages. languages.forEach((language, index) => { let convertedLanguage = language.trim(); if (convertedLanguage === 'Multi Audio') { @@ -60,7 +71,9 @@ export class Torrentio extends BaseWrapper { } else { convertedLanguage = emojiToLanguage(language) || language; // uppercase the first letter of each word - convertedLanguage = convertedLanguage.replace(/\b\w/g, (char) => char.toUpperCase()); + convertedLanguage = convertedLanguage.replace(/\b\w/g, (char) => + char.toUpperCase() + ); } if (!parsedFilename.languages.includes(convertedLanguage)) { parsedFilename.languages.push(convertedLanguage); @@ -115,8 +128,8 @@ export async function getTorrentioStreams( } // find all usable services - const usableServices = config.services.filter((service) => - supportedServices.includes(service.id) && service.enabled + const usableServices = config.services.filter( + (service) => supportedServices.includes(service.id) && service.enabled ); // if no usable services found, use torrentio without any configuration