├── providers
├── hianime
│ └── .hianime_cache
│ │ ├── hianime_slug_attack_on_titan_s1.json
│ │ └── hianime_episodes_attack-on-titan-112.json
├── VidZee.js
├── soapertv.js
├── vixsrc.js
├── 4khdhub.js
├── hdrezkas.js
├── vidsrcextractor.js
└── Showbox.js
├── Dockerfile
├── docker-compose.yml
├── vercel.json
├── manifest.json
├── LICENSE
├── package.json
├── .gitignore
├── .env.example
├── cookies.txt
├── utils
├── requestContext.js
├── redisCache.js
└── linkResolver.js
├── README.md
├── scrapersdirect
├── animeflix_scraper.js
├── myflixer-extractor.js
└── animepahe-scraper.js
└── DOCUMENTATION.md
/providers/hianime/.hianime_cache/hianime_slug_attack_on_titan_s1.json:
--------------------------------------------------------------------------------
1 | {"value":{"slug":"attack-on-titan-112","useRelativeEpisodeNumber":true},"expiry":1751443035481}
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:18-alpine
2 |
3 | WORKDIR /app
4 |
5 | COPY package*.json ./
6 | RUN npm install --production
7 |
8 | COPY . .
9 |
10 | CMD ["npm", "start"]
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | services:
2 | nuvio-addon:
3 | build: .
4 | image: nuvio-streams-addon:latest
5 | container_name: nuvio-streams-addon
6 | ports:
7 | - "7000:7000"
8 | env_file:
9 | - .env
10 | restart: unless-stopped
11 |
--------------------------------------------------------------------------------
/vercel.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": 2,
3 | "builds": [
4 | {
5 | "src": "server.js",
6 | "use": "@vercel/node",
7 | "config": { "includeFiles": ["providers/**", "views/**"] }
8 | }
9 | ],
10 | "routes": [
11 | {
12 | "src": "/(.*)",
13 | "dest": "server.js"
14 | }
15 | ]
16 | }
--------------------------------------------------------------------------------
/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "org.nuvio.streams",
3 | "version": "0.5.10",
4 | "name": "Nuvio Streams | Elfhosted",
5 | "description": "Stremio addon for high-quality streaming links.",
6 | "logo": "https://raw.githubusercontent.com/tapframe/NuvioStreaming/refs/heads/appstore/assets/titlelogo.png",
7 | "resources": ["stream"],
8 | "types": ["movie", "series"],
9 | "idPrefixes": ["tt", "tmdb"],
10 | "catalogs": [],
11 | "behaviorHints": {
12 | "adult": false,
13 | "p2pNotSupported": true,
14 | "configurable": true
15 | },
16 | "stremioAddonsConfig": {
17 | "issuer": "https://stremio-addons.net",
18 | "signature": "eyJhbGciOiJkaXIiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2In0..2YiTzY5hiYNywrpyMgv9Vg.vtorJ0blJ2dja5h7HSdL2eg0Hho7g8nJJnX2HbjJXSqzOU64niitT3FQItz9Q0rtF2sIswFcm842XgAnih0vPCTksxPwlkfDqolZcCjirQMl5hhcSzxJo88Mvonq2dxG.OYTTCCQcFthlmdn6c117CQ"}
19 | }
20 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 tapframe and contributors
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nuvio-streaming-addon",
3 | "version": "0.5.10",
4 | "description": "Stremio addon for HTTP streaming links",
5 | "main": "server.js",
6 | "scripts": {
7 | "start": "node server.js"
8 | },
9 | "dependencies": {
10 | "aromanize": "^0.1.5",
11 | "axios": "^1.9.0",
12 | "axios-cookiejar-support": "^6.0.2",
13 | "body-parser": "^2.2.0",
14 | "bytes": "^3.1.2",
15 | "cheerio": "^1.0.0",
16 | "cors": "^2.8.5",
17 | "crypto-js": "^4.2.0",
18 | "dotenv": "^16.5.0",
19 | "express": "^4.21.2",
20 | "fast-levenshtein": "^3.0.0",
21 | "form-data": "^4.0.3",
22 | "hls-parser": "^0.13.5",
23 | "ioredis": "^5.6.1",
24 | "jsdom": "^26.1.0",
25 | "node-fetch": "^2.7.0",
26 | "p-limit": "^6.2.0",
27 | "puppeteer": "^24.11.0",
28 | "redis": "^5.5.5",
29 | "rot13-cipher": "^1.0.0",
30 | "stremio-addon-sdk": "^1.6.10",
31 | "string-similarity": "^4.0.4",
32 | "tough-cookie": "^5.1.2",
33 | "ttl-cache": "^1.0.2",
34 | "vm2": "^3.9.19",
35 | "yargs": "^17.7.2"
36 | },
37 | "engines": {
38 | "node": ">=14.0.0"
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
3 | package-lock.json
4 | .vercel
5 |
6 | reddit_post.md
7 | reddit_post_title.md
8 | .env
9 | .cache
10 | misscalleneous/reddit_post_title.md
11 | misscalleneous/logs.txt
12 | misscalleneous/reddit_post.md
13 | misscalleneous
14 |
15 | # Environment variables
16 | .env
17 |
18 | # Cookies
19 | cookies.txt
20 |
21 | # IDE / OS specific
22 | .vscode/
23 | .idea/
24 | *.DS_Store
25 | *.project
26 | *.classpath
27 | *.cproject
28 | *.buildpath
29 | *.settings/
30 | *.suo
31 | *.user
32 | *.sln.docstates
33 |
34 | # Python specific
35 | __pycache__/
36 | *.pyc
37 | *.pyo
38 | *.pyd
39 | *.egg-info/
40 | dist/
41 | build/
42 | *.egg
43 | venv/
44 | env/
45 | pip-wheel-metadata/
46 | .Python
47 | *.tar.gz
48 | collaboration_message.md
49 | .streams_cache
50 | important_announcement.md
51 | envs.zip
52 | qa.md
53 | discord_announcement.md
54 | uhdmovies_scraper.js
55 |
56 | announcement_v2.md
57 | providers/hianime_back.js
58 | hianime_extractor.js
59 | cloudstream-extensions-phisher
60 | hianime-extractor.js
61 | /provider-service
62 | /web-gui
63 | providers/hdhub4u.js
64 | dahmer-movies-fetcher.js
65 |
66 | Extractor.kt
67 |
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | # Cache Settings
2 | DISABLE_CACHE=false
3 | DISABLE_STREAM_CACHE=true
4 | USE_REDIS_CACHE=false
5 | REDIS_URL=
6 | ENABLE_PSTREAM_API=false
7 |
8 | # URL Validation Settings
9 | # Set to true to disable URL validation for streaming links (faster but may return broken links)
10 | DISABLE_URL_VALIDATION=false
11 | # Set to true to disable URL validation specifically for 4KHDHub provider
12 | DISABLE_4KHDHUB_URL_VALIDATION=true
13 |
14 | # Global Proxy Configuration
15 | SHOWBOX_PROXY_URL_VALUE=
16 | SHOWBOX_PROXY_URL_ALTERNATE=
17 | SHOWBOX_USE_ROTATING_PROXY=false
18 |
19 | # Provider-specific Proxy URLs
20 | # If empty, will make direct requests without proxy
21 | VIDSRC_PROXY_URL=
22 | VIDZEE_PROXY_URL=
23 | SOAPERTV_PROXY_URL=
24 | HOLLYMOVIEHD_PROXY_URL=
25 | UHDMOVIES_PROXY_URL=
26 | MOVIESMOD_PROXY_URL=
27 | DRAMADRIP_PROXY_URL=
28 | HDHUB4U_PROXY_URL=
29 | TOPMOVIES_PROXY_URL=
30 |
31 |
32 | # Provider Enablement
33 | ENABLE_VIDZEE_PROVIDER=true
34 | ENABLE_HOLLYMOVIEHD_PROVIDER=false
35 | ENABLE_VIXSRC_PROVIDER=true
36 |
37 | # Anime configuration (placeholders kept minimal)
38 |
39 | # API Keys and External Services
40 | TMDB_API_KEY=
41 |
42 |
43 | # External Provider Services
44 | # Set to true to enable external provider microservices, false to use local providers only
45 | USE_EXTERNAL_PROVIDERS=false
46 | # External Provider Service URLs
47 | # Leave empty to use local providers, set URLs to use external microservices
48 | EXTERNAL_UHDMOVIES_URL=
49 | EXTERNAL_DRAMADRIP_URL=
50 | EXTERNAL_TOPMOVIES_URL=
51 | EXTERNAL_MOVIESMOD_URL=
52 |
53 | # Showbox proxy configuration
54 | # Comma-separated list for rotation; falls back to SHOWBOX_PROXY_URL_VALUE if empty
55 | SHOWBOX_PROXY_URLS=
56 | # SCRAPER_MODE=proxy
57 | # SCRAPER_API_KEY_VALUE=
58 |
59 | # Port configuration
60 | PORT=7777
61 |
62 | # Febbox proxy rotation list
63 | FEBBOX_PROXY_URLS=
--------------------------------------------------------------------------------
/providers/hianime/.hianime_cache/hianime_episodes_attack-on-titan-112.json:
--------------------------------------------------------------------------------
1 | {"value":[{"title":"To You Two Thousand Years Later","episodeId":"attack-on-titan-112?ep=3303","number":1,"isFiller":false},{"title":"That Day","episodeId":"attack-on-titan-112?ep=3304","number":2,"isFiller":false},{"title":"Shining Dimly in the Midst of Despair","episodeId":"attack-on-titan-112?ep=3305","number":3,"isFiller":false},{"title":"Night of the Disbanding","episodeId":"attack-on-titan-112?ep=3306","number":4,"isFiller":false},{"title":"First Battle","episodeId":"attack-on-titan-112?ep=3307","number":5,"isFiller":false},{"title":"The World She Saw","episodeId":"attack-on-titan-112?ep=3308","number":6,"isFiller":false},{"title":"The Small Blade","episodeId":"attack-on-titan-112?ep=3309","number":7,"isFiller":false},{"title":"Hearing the Heartbeat","episodeId":"attack-on-titan-112?ep=3310","number":8,"isFiller":false},{"title":"The Left Arm's Trace","episodeId":"attack-on-titan-112?ep=3311","number":9,"isFiller":false},{"title":"Answer","episodeId":"attack-on-titan-112?ep=3312","number":10,"isFiller":false},{"title":"Idol","episodeId":"attack-on-titan-112?ep=3313","number":11,"isFiller":false},{"title":"Wound","episodeId":"attack-on-titan-112?ep=3314","number":12,"isFiller":false},{"title":"Primordial Desire","episodeId":"attack-on-titan-112?ep=3315","number":13,"isFiller":false},{"title":"Can't Look Into His Eyes","episodeId":"attack-on-titan-112?ep=3316","number":14,"isFiller":false},{"title":"Special Operations Squad","episodeId":"attack-on-titan-112?ep=3317","number":15,"isFiller":false},{"title":"What To Do Now","episodeId":"attack-on-titan-112?ep=3318","number":16,"isFiller":false},{"title":"The Female Titan","episodeId":"attack-on-titan-112?ep=3319","number":17,"isFiller":false},{"title":"The Forest of Giant Trees","episodeId":"attack-on-titan-112?ep=3320","number":18,"isFiller":false},{"title":"Bite","episodeId":"attack-on-titan-112?ep=3321","number":19,"isFiller":false},{"title":"Erwin Smith","episodeId":"attack-on-titan-112?ep=3322","number":20,"isFiller":false},{"title":"Crushing Blow","episodeId":"attack-on-titan-112?ep=3323","number":21,"isFiller":false},{"title":"The Defeated","episodeId":"attack-on-titan-112?ep=3324","number":22,"isFiller":false},{"title":"Smile","episodeId":"attack-on-titan-112?ep=3325","number":23,"isFiller":false},{"title":"Mercy","episodeId":"attack-on-titan-112?ep=3326","number":24,"isFiller":false},{"title":"Wall","episodeId":"attack-on-titan-112?ep=3327","number":25,"isFiller":false}],"expiry":1750924635892}
--------------------------------------------------------------------------------
/cookies.txt:
--------------------------------------------------------------------------------
1 | eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE3NDg0Mzk5NDYsIm5iZiI6MTc0ODQzOTk0NiwiZXhwIjoxNzc5NTQzOTY2LCJkYXRhIjp7InVpZCI6NzkxNzY4LCJ0b2tlbiI6ImZlNTg4MjBjOGYxMmJkZWYzZDk4MmZmY2RjMTA2ZmFmIn19.xPu6Vw565rIuIsuYYZci-yL1ugC9-PdDcNRk40QVVsc
2 |
3 | eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE3NDg0NDAwMTQsIm5iZiI6MTc0ODQ0MDAxNCwiZXhwIjoxNzc5NTQ0MDM0LCJkYXRhIjp7InVpZCI6NzkxNzcwLCJ0b2tlbiI6ImM0NDc3OTJhNTFiN2U4YzI0OWFlOTNhNjRlZGIwZTU2In19.oxBR1Nr4AD8sAAZm1dT0XhvTZ8Q6iiG8e3J3FgiFkQg
4 |
5 | eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE3NDg0NDAwNTgsIm5iZiI6MTc0ODQ0MDA1OCwiZXhwIjoxNzc5NTQ0MDc4LCJkYXRhIjp7InVpZCI6NzkxNzcxLCJ0b2tlbiI6IjdmNGEwNmViMjMyY2Y4NDE0MTc3Yzg3N2U1OTE5NjgzIn19.ONPgKWnQyjdMIMBExTHWVLK5zT72tIjq92VYmx9M90M
6 |
7 | eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE3NDg0NDAxMjUsIm5iZiI6MTc0ODQ0MDEyNSwiZXhwIjoxNzc5NTQ0MTQ1LCJkYXRhIjp7InVpZCI6NzkxNzczLCJ0b2tlbiI6Ijg3OTFmODQ1YjYxZmI2MTBiMjU5YzdkYTg5OWU5ZmI1In19.EWtWz_g1TCvyjeSI4ZPuQIlrxD5HkJQo4r7KD-77ZUQ
8 |
9 | eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE3NDg0NDAxODMsIm5iZiI6MTc0ODQ0MDE4MywiZXhwIjoxNzc5NTQ0MjAzLCJkYXRhIjp7InVpZCI6NzkxNzc0LCJ0b2tlbiI6ImIxZmZhMmViYThjZTQ0ZTQ5Njg0ZWZlNjdjODlmYmY2In19.gpmYPsOXw-9fiCvyvhHX1IVfwB2pzR0tXdUFl1e4Y-w
10 |
11 | eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE3NDg0NDAyMTgsIm5iZiI6MTc0ODQ0MDIxOCwiZXhwIjoxNzc5NTQ0MjM4LCJkYXRhIjp7InVpZCI6NzkxNzc1LCJ0b2tlbiI6ImM2MTIyNmNhNDc1YTE3MzVjZDc3MTgwYjA0ZGM5NzVmIn19.eXYMkm-LLEVNPS8k06zywNHyh78g_pJG0qq58IX_QRo
12 |
13 | eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE3NDg0NDAyNTMsIm5iZiI6MTc0ODQ0MDI1MywiZXhwIjoxNzc5NTQ0MjczLCJkYXRhIjp7InVpZCI6NzkxNzc3LCJ0b2tlbiI6IjFmMWUwNWRhMjVlZDhlZTgxMjJjZDE3MDVjYzBmYTBkIn19.mvUeUXPMfQHKkjnSOX6ESiflhxP2c9LZY22CShyWwdM
14 |
15 | eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE3NDgwNjYzMjgsIm5iZiI6MTc0ODA2NjMyOCwiZXhwIjoxNzc5MTcwMzQ4LCJkYXRhIjp7InVpZCI6NzgyNDcwLCJ0b2tlbiI6ImUwMTAyNjIyOWMyOTVlOTFlOTY0MWJjZWZiZGE4MGUxIn19.Za7tx60gu8rq9pLw1LVuIjROaBJzgF_MV049B8NO3L8
16 |
17 | eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE3NDg1NDg5MzksIm5iZiI6MTc0ODU0ODkzOSwiZXhwIjoxNzc5NjUyOTU5LCJkYXRhIjp7InVpZCI6NzkzMzk5LCJ0b2tlbiI6IjlhNzBmYzM0OGE5NTI5MmM0MmUxMWZhNzEwY2Y2MzFjIn19.KjyDpQh_sEdEXYSrB7TojsThb_R935zTVd3WoeTFiRE
18 |
19 | eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE3NDg1NDkwNDEsIm5iZiI6MTc0ODU0OTA0MSwiZXhwIjoxNzc5NjUzMDYxLCJkYXRhIjp7InVpZCI6Nzk0NDE0LCJ0b2tlbiI6IjRkNjY2NjhiY2U1NWJkM2NhNzkxYTkyNzlkOTU3M2I0In19.nDXwZeGZPWLC5IZRyNMekpPp88ANMNLueCQZnJcBAB8
20 |
21 | eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE3NDg1NDkxNjgsIm5iZiI6MTc0ODU0OTE2OCwiZXhwIjoxNzc5NjUzMTg4LCJkYXRhIjp7InVpZCI6Nzk0NDIxLCJ0b2tlbiI6IjY1MWJhNDhhM2IzODQyM2M5NWE1NTE1YmZmMDUzMjkxIn19.G99ckRadTLbb0DFr4YomBDIWLYxa3mQ59-KjO3SOhmA
--------------------------------------------------------------------------------
/utils/requestContext.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Request Context using AsyncLocalStorage for per-request isolation
3 | * This ensures cookies and other request-specific data don't leak between concurrent requests
4 | */
5 |
6 | const { AsyncLocalStorage } = require('async_hooks');
7 |
8 | // Create the AsyncLocalStorage instance
9 | const requestContext = new AsyncLocalStorage();
10 |
11 | /**
12 | * Get the current request's config
13 | * @returns {Object} The current request config or empty object if not in a request context
14 | */
15 | function getRequestConfig() {
16 | const store = requestContext.getStore();
17 | return store?.config || {};
18 | }
19 |
20 | /**
21 | * Set a value in the current request's config
22 | * @param {string} key - The key to set
23 | * @param {any} value - The value to set
24 | */
25 | function setRequestConfigValue(key, value) {
26 | const store = requestContext.getStore();
27 | if (store && store.config) {
28 | store.config[key] = value;
29 | }
30 | }
31 |
32 | /**
33 | * Get a specific value from the current request's config
34 | * @param {string} key - The key to get
35 | * @param {any} defaultValue - Default value if key doesn't exist
36 | * @returns {any} The value or defaultValue
37 | */
38 | function getRequestConfigValue(key, defaultValue = null) {
39 | const config = getRequestConfig();
40 | return config[key] !== undefined ? config[key] : defaultValue;
41 | }
42 |
43 | /**
44 | * Run a function within a request context
45 | * @param {Object} config - The request config to use
46 | * @param {Function} fn - The function to run
47 | * @returns {any} The result of the function
48 | */
49 | function runWithRequestContext(config, fn) {
50 | return requestContext.run({ config }, fn);
51 | }
52 |
53 | /**
54 | * Express middleware to set up request context
55 | * @param {Object} config - The config object to use for this request
56 | * @returns {Function} Express middleware function
57 | */
58 | function createRequestContextMiddleware() {
59 | return (req, res, next) => {
60 | // Initialize empty config - will be populated by other middleware
61 | const config = {};
62 |
63 | // Run the rest of the request within this context
64 | requestContext.run({ config }, () => {
65 | // Store reference on req for easy access in middleware
66 | req.nuvioConfig = config;
67 | next();
68 | });
69 | };
70 | }
71 |
72 | module.exports = {
73 | requestContext,
74 | getRequestConfig,
75 | setRequestConfigValue,
76 | getRequestConfigValue,
77 | runWithRequestContext,
78 | createRequestContextMiddleware
79 | };
80 |
--------------------------------------------------------------------------------
/providers/VidZee.js:
--------------------------------------------------------------------------------
1 | const axios = require('axios');
2 |
3 | // Function to parse command line arguments
4 | const parseArgs = () => {
5 | const args = process.argv.slice(2);
6 | const options = {};
7 | let i = 0;
8 | while (i < args.length) {
9 | const arg = args[i];
10 | if (arg.startsWith('--')) {
11 | const key = arg.substring(2);
12 | if (i + 1 < args.length && !args[i + 1].startsWith('--')) {
13 | options[key] = args[i + 1];
14 | i++;
15 | } else {
16 | options[key] = true; // Flag argument
17 | }
18 | } else {
19 | // Positional arguments, if any (none expected for this script based on current design)
20 | }
21 | i++;
22 | }
23 | return options;
24 | };
25 |
26 | const getVidZeeStreams = async (tmdbId, mediaType, seasonNum, episodeNum) => {
27 | if (!tmdbId) {
28 | console.error('[VidZee] Error: TMDB ID (tmdbId) is required.');
29 | return [];
30 | }
31 |
32 | if (!mediaType || (mediaType !== 'movie' && mediaType !== 'tv')) {
33 | console.error('[VidZee] Error: mediaType is required and must be either "movie" or "tv".');
34 | return [];
35 | }
36 |
37 | if (mediaType === 'tv') {
38 | if (!seasonNum) {
39 | console.error('[VidZee] Error: Season (seasonNum) is required for TV shows.');
40 | return [];
41 | }
42 | if (!episodeNum) {
43 | console.error('[VidZee] Error: Episode (episodeNum) is required for TV shows.');
44 | return [];
45 | }
46 | }
47 |
48 | const servers = [3, 4, 5];
49 |
50 | const streamPromises = servers.map(async (sr) => {
51 | let targetApiUrl = `https://player.vidzee.wtf/api/server?id=${tmdbId}&sr=${sr}`;
52 |
53 | if (mediaType === 'tv') {
54 | targetApiUrl += `&ss=${seasonNum}&ep=${episodeNum}`;
55 | }
56 |
57 | let finalApiUrl;
58 | let headers = {
59 | 'Referer': 'https://core.vidzee.wtf/' // Default for proxy method
60 | };
61 | let timeout = 7000; // Reduced timeout
62 |
63 | const proxyBaseUrl = process.env.VIDZEE_PROXY_URL || process.env.SHOWBOX_PROXY_URL_VALUE;
64 | if (proxyBaseUrl) {
65 | finalApiUrl = proxyBaseUrl + encodeURIComponent(targetApiUrl);
66 | } else {
67 | finalApiUrl = targetApiUrl;
68 | }
69 |
70 | console.log(`[VidZee] Fetching from server ${sr}: ${targetApiUrl}`);
71 |
72 | try {
73 | const response = await axios.get(finalApiUrl, {
74 | headers: headers,
75 | timeout: timeout
76 | });
77 |
78 | const responseData = response.data;
79 |
80 | if (!responseData || typeof responseData !== 'object') {
81 | console.error(`[VidZee S${sr}] Error: Invalid response data from API.`);
82 | return [];
83 | }
84 |
85 | if (responseData.tracks) {
86 | delete responseData.tracks;
87 | }
88 |
89 | let apiSources = [];
90 | if (responseData.url && Array.isArray(responseData.url)) {
91 | apiSources = responseData.url;
92 | } else if (responseData.link && typeof responseData.link === 'string') {
93 | apiSources = [responseData];
94 | }
95 |
96 | if (!apiSources || apiSources.length === 0) {
97 | console.log(`[VidZee S${sr}] No stream sources found in API response.`);
98 | return [];
99 | }
100 |
101 | const streams = apiSources.map(sourceItem => {
102 | // Prefer sourceItem.name as label, fallback to sourceItem.type, then 'VidZee Stream'
103 | const label = sourceItem.name || sourceItem.type || 'VidZee';
104 | // Ensure quality has 'p' if it's a resolution, or keep it as is
105 | const quality = String(label).match(/^\d+$/) ? `${label}p` : label;
106 | const language = sourceItem.language || sourceItem.lang;
107 |
108 | return {
109 | title: `VidZee S${sr} - ${quality}`,
110 | url: sourceItem.link, // Use sourceItem.link for the URL
111 | quality: quality,
112 | language: language,
113 | provider: "VidZee",
114 | size: "Unknown size",
115 | behaviorHints: {
116 | notWebReady: true,
117 | headers: {
118 | 'Referer': 'https://core.vidzee.wtf/'
119 | }
120 | }
121 | };
122 | }).filter(stream => stream.url);
123 |
124 | console.log(`[VidZee S${sr}] Successfully extracted ${streams.length} streams.`);
125 | return streams;
126 |
127 | } catch (error) {
128 | if (error.response) {
129 | console.error(`[VidZee S${sr}] Error fetching: ${error.response.status} ${error.response.statusText}`);
130 | } else if (error.request) {
131 | console.error(`[VidZee S${sr}] Error fetching: No response received.`);
132 | } else {
133 | console.error(`[VidZee S${sr}] Error fetching:`, error.message);
134 | }
135 | return [];
136 | }
137 | });
138 |
139 | const allStreamsNested = await Promise.all(streamPromises);
140 | const allStreams = allStreamsNested.flat();
141 |
142 | console.log(`[VidZee] Found a total of ${allStreams.length} streams from servers ${servers.join(', ')}.`);
143 | return allStreams;
144 | };
145 |
146 | module.exports = { getVidZeeStreams };
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | [![Contributors][contributors-shield]][contributors-url]
6 | [![Forks][forks-shield]][forks-url]
7 | [![Stargazers][stars-shield]][stars-url]
8 | [![Issues][issues-shield]][issues-url]
9 | [![MIT License][license-shield]][license-url]
10 |
11 |
12 |
13 |
31 |
32 |
33 |
34 | Table of Contents
35 |
36 | -
37 | About The Project
38 |
42 |
43 | - Public Instance
44 | -
45 | Getting Started
46 |
51 |
52 | - Usage Notes
53 | - Contributing
54 | - Support
55 | - License
56 | - Contact
57 | - Acknowledgments
58 |
59 |
60 |
61 |
62 | ## About The Project
63 |
64 | Nuvio Streams is a powerful Stremio addon that provides direct HTTP streaming links for movies and TV shows from multiple online providers. Unlike torrent-based solutions, this addon focuses on delivering reliable, direct streams without P2P requirements.
65 |
66 | **Perfect for users who:**
67 | * Prefer direct HTTP streaming over debrid services
68 | * Want customizable provider and quality settings
69 | * Need reliable streaming without torrents/P2P
70 | * Are willing to configure personal cookies for optimal performance
71 |
72 | (back to top)
73 |
74 | ### Key Features
75 |
76 | * **🌐 Multiple Sources** - Aggregates direct HTTP streams without P2P
77 | * **Personal Cookie Support** - Get your own quota and access to 4K/HDR content
78 | * **🎯 Quality Filtering** - Set minimum quality requirements
79 | * **🔒 No P2P/Torrents** - Only direct HTTP streams
80 | * **🎬 Full Compatibility** - Supports TMDB & IMDb IDs
81 | * **Easy Configuration** - Web-based settings management
82 |
83 | (back to top)
84 |
85 | ### Built With
86 |
87 | * [![Node.js][Node.js]][Node-url]
88 | * [![Express.js][Express.js]][Express-url]
89 | * [![JavaScript][JavaScript]][JavaScript-url]
90 |
91 | (back to top)
92 |
93 |
94 | ## Public Instance
95 |
96 | **🌍 Current Public Instance:** [https://nuviostreams.hayd.uk](https://nuviostreams.hayd.uk)
97 |
98 | * 💡 For the most reliable experience, consider self-hosting your own instance
99 |
100 | (back to top)
101 |
102 |
103 | ## Getting Started
104 |
105 | Self-hosting provides the best experience with full access and personalized performance. For detailed setup and configuration instructions, please refer to our documentation.
106 |
107 | **[View the Self-Hosting Guide](https://github.com/tapframe/NuvioStreamsAddon/blob/master/DOCUMENTATION.md)**
108 |
109 | (back to top)
110 |
111 |
112 | ## Usage Notes
113 |
114 | ### Troubleshooting
115 |
116 | If some links are missing, try a refresh or a second attempt (caching may help). If a site is blocked in your region, consider using a proxy and configure proxy URLs in your `.env` file.
117 |
118 | (back to top)
119 |
120 |
121 |
122 | (back to top)
123 |
124 |
125 | ## Contributing
126 |
127 | Contributions make the open source community amazing! Any contributions are **greatly appreciated**.
128 |
129 | 1. Fork the Project
130 | 2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`)
131 | 3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`)
132 | 4. Push to the Branch (`git push origin feature/AmazingFeature`)
133 | 5. Open a Pull Request
134 |
135 | ### Ways to Contribute
136 |
137 | * 🔧 **Code Contributions** - Improve core features or add new ones
138 | * 🐛 **Bug Reports** - Help identify and fix issues
139 | * 💡 **Feature Requests** - Suggest improvements
140 | * **Documentation** - Improve or translate docs
141 | * 🧪 **Testing** - Test on different platforms
142 |
143 | (back to top)
144 |
145 |
146 | ## Support
147 |
148 | If you find Nuvio Streams useful, consider supporting development:
149 |
150 | * **[Ko-Fi](https://ko-fi.com/tapframe)** - Help with server costs
151 | * **GitHub Star** - Show your support
152 | * **Share** - Tell others about the project
153 |
154 | (back to top)
155 |
156 |
157 | ## License
158 |
159 | Distributed under the MIT License. See `LICENSE` for more information.
160 |
161 | (back to top)
162 |
163 |
164 | ## Contact
165 |
166 | **Project Links:**
167 | * GitHub: [https://github.com/tapframe](https://github.com/tapframe)
168 | * Issues: [https://github.com/tapframe/NuvioStreamsAddon/issues](https://github.com/tapframe/NuvioStreamsAddon/issues)
169 | * Public Instance: [https://nuviostreams.hayd.uk](https://nuviostreams.hayd.uk)
170 |
171 | (back to top)
172 |
173 |
174 | ## Acknowledgments
175 |
176 | * [TMDB](https://www.themoviedb.org/) - Movie/TV metadata
177 | * [Stremio](https://www.stremio.com/) - Streaming platform
178 | * [ScraperAPI](https://www.scraperapi.com/) - Anti-scraping solution
179 | * [Netlify](https://www.netlify.com/) - Proxy hosting
180 | * Community contributors and testers
181 |
182 | **Disclaimer:** This addon scrapes third-party websites. Users are responsible for compliance with terms of service and local laws. For educational and personal use only.
183 |
184 | (back to top)
185 |
186 |
187 | [contributors-shield]: https://img.shields.io/github/contributors/tapframe/NuvioStreamsAddon.svg?style=for-the-badge
188 | [contributors-url]: https://github.com/tapframe/NuvioStreamsAddon/graphs/contributors
189 | [forks-shield]: https://img.shields.io/github/forks/tapframe/NuvioStreamsAddon.svg?style=for-the-badge
190 | [forks-url]: https://github.com/tapframe/NuvioStreamsAddon/network/members
191 | [stars-shield]: https://img.shields.io/github/stars/tapframe/NuvioStreamsAddon.svg?style=for-the-badge
192 | [stars-url]: https://github.com/tapframe/NuvioStreamsAddon/stargazers
193 | [issues-shield]: https://img.shields.io/github/issues/tapframe/NuvioStreamsAddon.svg?style=for-the-badge
194 | [issues-url]: https://github.com/tapframe/NuvioStreamsAddon/issues
195 | [license-shield]: https://img.shields.io/github/license/tapframe/NuvioStreamsAddon.svg?style=for-the-badge
196 | [license-url]: https://github.com/tapframe/NuvioStreamsAddon/blob/master/LICENSE
197 |
198 | [Node.js]: https://img.shields.io/badge/Node.js-43853D?style=for-the-badge&logo=node.js&logoColor=white
199 | [Node-url]: https://nodejs.org/
200 | [Express.js]: https://img.shields.io/badge/Express.js-404D59?style=for-the-badge&logo=express&logoColor=white
201 | [Express-url]: https://expressjs.com/
202 | [JavaScript]: https://img.shields.io/badge/JavaScript-F7DF1E?style=for-the-badge&logo=javascript&logoColor=black
203 | [JavaScript-url]: https://developer.mozilla.org/en-US/docs/Web/JavaScript
204 |
--------------------------------------------------------------------------------
/utils/redisCache.js:
--------------------------------------------------------------------------------
1 | require('dotenv').config();
2 | const Redis = require('ioredis');
3 | const fs = require('fs').promises;
4 | const path = require('path');
5 |
6 | // Redis Cache Utility for NuvioStreams Providers
7 | class RedisCache {
8 | constructor(providerName = 'Generic') {
9 | this.providerName = providerName;
10 | this.redisClient = null;
11 | this.redisKeepAliveInterval = null;
12 | this.initializeRedis();
13 | }
14 |
15 | initializeRedis() {
16 | if (process.env.USE_REDIS_CACHE === 'true') {
17 | try {
18 | console.log(`[${this.providerName} Cache] Initializing Redis. REDIS_URL from env: ${process.env.REDIS_URL ? 'exists and has value' : 'MISSING or empty'}`);
19 | if (!process.env.REDIS_URL) {
20 | throw new Error(`REDIS_URL environment variable is not set or is empty for ${this.providerName} Redis.`);
21 | }
22 |
23 | // Check if this is a local Redis instance or remote
24 | const isLocal = process.env.REDIS_URL.includes('localhost') || process.env.REDIS_URL.includes('127.0.0.1');
25 |
26 | this.redisClient = new Redis(process.env.REDIS_URL, {
27 | maxRetriesPerRequest: 5,
28 | retryStrategy(times) {
29 | const delay = Math.min(times * 500, 5000);
30 | return delay;
31 | },
32 | reconnectOnError: function(err) {
33 | const targetError = 'READONLY';
34 | if (err.message.includes(targetError)) {
35 | return true;
36 | }
37 | return false;
38 | },
39 | // TLS is optional - only use if explicitly specified with rediss:// protocol
40 | tls: process.env.REDIS_URL.startsWith('rediss://') ? {} : undefined,
41 | enableOfflineQueue: true,
42 | enableReadyCheck: true,
43 | autoResubscribe: true,
44 | autoResendUnfulfilledCommands: true,
45 | lazyConnect: false
46 | });
47 |
48 | this.redisClient.on('connect', () => {
49 | console.log(`[${this.providerName} Cache] Successfully connected to Redis server.`);
50 |
51 | // Redis Keep-Alive for managed services like Upstash
52 | if (this.redisKeepAliveInterval) {
53 | clearInterval(this.redisKeepAliveInterval);
54 | }
55 | this.redisKeepAliveInterval = setInterval(async () => {
56 | try {
57 | await this.redisClient.ping();
58 | } catch (pingError) {
59 | console.warn(`[${this.providerName} Cache] Redis keep-alive ping failed: ${pingError.message}`);
60 | }
61 | }, 4 * 60 * 1000); // 4 minutes
62 | });
63 |
64 | this.redisClient.on('error', (err) => {
65 | console.error(`[${this.providerName} Cache] Redis connection error: ${err.message}`);
66 | });
67 |
68 | this.redisClient.on('close', () => {
69 | console.log(`[${this.providerName} Cache] Redis connection closed.`);
70 | if (this.redisKeepAliveInterval) {
71 | clearInterval(this.redisKeepAliveInterval);
72 | this.redisKeepAliveInterval = null;
73 | }
74 | });
75 |
76 | } catch (initError) {
77 | console.error(`[${this.providerName} Cache] Failed to initialize Redis client: ${initError.message}`);
78 | this.redisClient = null;
79 | }
80 | } else {
81 | console.log(`[${this.providerName} Cache] Redis cache is disabled (USE_REDIS_CACHE is not 'true'). Using file system cache only.`);
82 | }
83 | }
84 |
85 | async getFromCache(cacheKey, subDir = '', cacheDir = null) {
86 | if (process.env.DISABLE_CACHE === 'true') {
87 | console.log(`[${this.providerName} Cache] CACHE DISABLED: Skipping read for ${path.join(subDir, cacheKey)}`);
88 | return null;
89 | }
90 |
91 | const fullCacheKey = subDir ? `${this.providerName.toLowerCase()}:${subDir}:${cacheKey}` : `${this.providerName.toLowerCase()}:${cacheKey}`;
92 |
93 | // Try to get from Redis first
94 | if (this.redisClient && this.redisClient.status === 'ready') {
95 | try {
96 | const redisData = await this.redisClient.get(fullCacheKey);
97 | if (redisData !== null) {
98 | console.log(`[${this.providerName} Cache] REDIS CACHE HIT for: ${fullCacheKey}`);
99 | try {
100 | return JSON.parse(redisData);
101 | } catch (e) {
102 | return redisData;
103 | }
104 | }
105 | console.log(`[${this.providerName} Cache] REDIS CACHE MISS for: ${fullCacheKey}`);
106 | } catch (redisError) {
107 | console.warn(`[${this.providerName} Cache] REDIS CACHE READ ERROR for ${fullCacheKey}: ${redisError.message}. Falling back to file system cache.`);
108 | }
109 | } else if (this.redisClient) {
110 | console.log(`[${this.providerName} Cache] Redis client not ready (status: ${this.redisClient.status}). Skipping Redis read for ${fullCacheKey}, trying file system.`);
111 | }
112 |
113 | // Fallback to file system cache
114 | if (cacheDir) {
115 | const cachePath = path.join(cacheDir, subDir, `${cacheKey}.json`);
116 | try {
117 | const fileData = await fs.readFile(cachePath, 'utf-8');
118 | console.log(`[${this.providerName} Cache] FILE SYSTEM CACHE HIT for: ${path.join(subDir, cacheKey)}`);
119 |
120 | // If Redis is available, populate Redis for next time (permanent cache)
121 | if (this.redisClient && this.redisClient.status === 'ready') {
122 | try {
123 | await this.redisClient.set(fullCacheKey, fileData);
124 | console.log(`[${this.providerName} Cache] Populated REDIS CACHE from FILE SYSTEM for: ${fullCacheKey} (PERMANENT - no expiration)`);
125 | } catch (redisSetError) {
126 | console.warn(`[${this.providerName} Cache] REDIS CACHE SET ERROR (after file read) for ${fullCacheKey}: ${redisSetError.message}`);
127 | }
128 | }
129 |
130 | try {
131 | return JSON.parse(fileData);
132 | } catch (e) {
133 | return fileData;
134 | }
135 | } catch (error) {
136 | if (error.code !== 'ENOENT') {
137 | console.warn(`[${this.providerName} Cache] FILE SYSTEM CACHE READ ERROR for ${cacheKey}: ${error.message}`);
138 | } else {
139 | console.log(`[${this.providerName} Cache] FILE SYSTEM CACHE MISS for: ${path.join(subDir, cacheKey)}`);
140 | }
141 | return null;
142 | }
143 | }
144 |
145 | return null;
146 | }
147 |
148 | async saveToCache(cacheKey, content, subDir = '', cacheDir = null, ttlSeconds = null) {
149 | if (process.env.DISABLE_CACHE === 'true') {
150 | console.log(`[${this.providerName} Cache] CACHE DISABLED: Skipping write for ${path.join(subDir, cacheKey)}`);
151 | return;
152 | }
153 |
154 | const dataToSave = typeof content === 'string' ? content : JSON.stringify(content, null, 2);
155 | const fullCacheKey = subDir ? `${this.providerName.toLowerCase()}:${subDir}:${cacheKey}` : `${this.providerName.toLowerCase()}:${cacheKey}`;
156 |
157 | // Attempt to save to Redis first with optional TTL
158 | if (this.redisClient && this.redisClient.status === 'ready') {
159 | try {
160 | if (ttlSeconds) {
161 | await this.redisClient.setex(fullCacheKey, ttlSeconds, dataToSave);
162 | console.log(`[${this.providerName} Cache] SAVED TO REDIS CACHE: ${fullCacheKey} (TTL: ${ttlSeconds}s)`);
163 | } else {
164 | await this.redisClient.set(fullCacheKey, dataToSave);
165 | console.log(`[${this.providerName} Cache] SAVED TO REDIS CACHE: ${fullCacheKey} (PERMANENT - no expiration)`);
166 | }
167 | } catch (redisError) {
168 | console.warn(`[${this.providerName} Cache] REDIS CACHE WRITE ERROR for ${fullCacheKey}: ${redisError.message}. Proceeding with file system cache.`);
169 | }
170 | } else if (this.redisClient) {
171 | console.log(`[${this.providerName} Cache] Redis client not ready (status: ${this.redisClient.status}). Skipping Redis write for ${fullCacheKey}.`);
172 | }
173 |
174 | // Always save to file system cache as a fallback
175 | if (cacheDir) {
176 | try {
177 | const fullSubDir = path.join(cacheDir, subDir);
178 | await this.ensureCacheDir(fullSubDir);
179 | const cachePath = path.join(fullSubDir, `${cacheKey}.json`);
180 | await fs.writeFile(cachePath, dataToSave, 'utf-8');
181 | console.log(`[${this.providerName} Cache] SAVED TO FILE SYSTEM CACHE: ${path.join(subDir, cacheKey)}`);
182 | } catch (error) {
183 | console.warn(`[${this.providerName} Cache] FILE SYSTEM CACHE WRITE ERROR for ${cacheKey}: ${error.message}`);
184 | }
185 | }
186 | }
187 |
188 | // TTL method removed - all cache entries are now permanent
189 | // getTTLForSubDir(subDir) {
190 | // // This method is no longer used as all cache entries are permanent
191 | // // Previously configured TTL based on data type but now all data persists indefinitely
192 | // }
193 |
194 | async ensureCacheDir(dirPath) {
195 | try {
196 | await fs.mkdir(dirPath, { recursive: true });
197 | } catch (error) {
198 | if (error.code !== 'EEXIST') {
199 | console.warn(`[${this.providerName} Cache] Warning: Could not create cache directory ${dirPath}: ${error.message}`);
200 | }
201 | }
202 | }
203 |
204 | // Cleanup method
205 | cleanup() {
206 | if (this.redisKeepAliveInterval) {
207 | clearInterval(this.redisKeepAliveInterval);
208 | this.redisKeepAliveInterval = null;
209 | }
210 | if (this.redisClient) {
211 | this.redisClient.disconnect();
212 | this.redisClient = null;
213 | }
214 | }
215 | }
216 |
217 | module.exports = RedisCache;
--------------------------------------------------------------------------------
/providers/soapertv.js:
--------------------------------------------------------------------------------
1 | const fetch = require('node-fetch'); // As per original script: npm install node-fetch@2
2 | const cheerio = require('cheerio'); // As per original script: npm install cheerio
3 | const { URLSearchParams } = require('url'); // For form data
4 |
5 | // Constants
6 | const PROXY_URL = process.env.SOAPERTV_PROXY_URL || process.env.SHOWBOX_PROXY_URL_VALUE;
7 | const BASE_URL = 'https://soaper.cc';
8 | const TMDB_API_KEY_SOAPERTV = "439c478a771f35c05022f9feabcca01c"; // Public TMDB API key used by this provider
9 |
10 | // Simple In-Memory Cache
11 | const soaperCache = {
12 | search: {},
13 | episodes: {}
14 | };
15 | const CACHE_TTL = 30 * 60 * 1000; // 30 minutes TTL for cache entries
16 |
17 | // Function to get from cache
18 | function getFromCache(type, key) {
19 | if (soaperCache[type] && soaperCache[type][key]) {
20 | const entry = soaperCache[type][key];
21 | if (Date.now() - entry.timestamp < CACHE_TTL) {
22 | console.log(`[Soaper TV Cache] HIT for ${type} - ${key}`);
23 | return entry.data;
24 | }
25 | console.log(`[Soaper TV Cache] STALE for ${type} - ${key}`);
26 | delete soaperCache[type][key]; // Remove stale entry
27 | }
28 | console.log(`[Soaper TV Cache] MISS for ${type} - ${key}`);
29 | return null;
30 | }
31 |
32 | // Function to save to cache
33 | function saveToCache(type, key, data) {
34 | if (!soaperCache[type]) soaperCache[type] = {};
35 | soaperCache[type][key] = {
36 | data: data,
37 | timestamp: Date.now()
38 | };
39 | console.log(`[Soaper TV Cache] SAVED for ${type} - ${key}`);
40 | }
41 |
42 | // Proxy wrapper for fetch
43 | async function proxiedFetchSoaper(url, options = {}, isFullUrlOverride = false) {
44 | const isHttpUrl = url.startsWith('http://') || url.startsWith('https://');
45 | const fullUrl = isHttpUrl || isFullUrlOverride ? url : `${BASE_URL}${url}`;
46 |
47 | let fetchUrl;
48 | if (PROXY_URL) {
49 | fetchUrl = `${PROXY_URL}${encodeURIComponent(fullUrl)}`;
50 | console.log(`[Soaper TV] Fetching: ${url} (via proxy: ${fetchUrl.substring(0,100)}...)`);
51 | } else {
52 | fetchUrl = fullUrl;
53 | console.log(`[Soaper TV] Fetching: ${url} (direct request)`);
54 | }
55 |
56 | try {
57 | const response = await fetch(fetchUrl, options);
58 |
59 | if (!response.ok) {
60 | let errorBody = '';
61 | try {
62 | errorBody = await response.text();
63 | } catch (e) { /* ignore */ }
64 | throw new Error(`Response not OK: ${response.status} ${response.statusText}. Body: ${errorBody.substring(0,200)}`);
65 | }
66 |
67 | const contentType = response.headers.get('content-type');
68 | if (contentType && contentType.includes('application/json')) {
69 | return response.json();
70 | }
71 | return response.text();
72 | } catch (error) {
73 | console.error(`[Soaper TV] Fetch error for ${url}:`, error.message);
74 | throw error;
75 | }
76 | }
77 |
78 | // Compare media to find matching result
79 | function compareMediaSoaper(media, title, year) {
80 | const normalizeString = (str) => String(str || '').toLowerCase().replace(/[^a-zA-Z0-9]/g, '');
81 | const normalizedMediaTitle = normalizeString(media.title);
82 | const normalizedResultTitle = normalizeString(title);
83 |
84 | if (normalizedMediaTitle !== normalizedResultTitle) {
85 | return false;
86 | }
87 |
88 | if (year && media.year && media.year !== year) {
89 | return false;
90 | }
91 |
92 | return true;
93 | }
94 |
95 | async function getSoaperTvStreams(tmdbId, mediaType = 'movie', season = '', episode = '') {
96 | console.log(`[Soaper TV] Attempting to fetch streams for TMDB ID: ${tmdbId}, Type: ${mediaType}${mediaType === 'tv' ? `, S:${season}E:${episode}` : ''}`);
97 | try {
98 | const tmdbUrl = mediaType === 'movie'
99 | ? `https://api.themoviedb.org/3/movie/${tmdbId}?api_key=${TMDB_API_KEY_SOAPERTV}`
100 | : `https://api.themoviedb.org/3/tv/${tmdbId}?api_key=${TMDB_API_KEY_SOAPERTV}`;
101 |
102 | console.log(`[Soaper TV] Fetching TMDB info from: ${tmdbUrl}`);
103 | const tmdbResponse = await fetch(tmdbUrl); // Direct fetch for TMDB API
104 | if (!tmdbResponse.ok) {
105 | const errorBody = await tmdbResponse.text();
106 | throw new Error(`TMDB API request failed: ${tmdbResponse.status} ${tmdbResponse.statusText}. Body: ${errorBody.substring(0,200)}`);
107 | }
108 | const tmdbData = await tmdbResponse.json();
109 |
110 | if (tmdbData.success === false) {
111 | throw new Error(`TMDB API error: ${tmdbData.status_message || 'Unknown TMDB error'}`);
112 | }
113 |
114 | const mediaInfo = {
115 | title: mediaType === 'movie' ? tmdbData.title : tmdbData.name,
116 | year: parseInt(mediaType === 'movie'
117 | ? (tmdbData.release_date || '').split('-')[0]
118 | : (tmdbData.first_air_date || '').split('-')[0], 10)
119 | };
120 |
121 | if (!mediaInfo.title) {
122 | console.error('[Soaper TV] Failed to get title from TMDB data:', tmdbData);
123 | throw new Error('Could not extract title from TMDB response.');
124 | }
125 | console.log(`[Soaper TV] TMDB Info: "${mediaInfo.title}" (${mediaInfo.year || 'N/A'})`);
126 |
127 | const searchCacheKey = mediaInfo.title.toLowerCase();
128 | let searchResults = getFromCache('search', searchCacheKey);
129 |
130 | if (!searchResults) {
131 | const searchUrl = `/search.html?keyword=${encodeURIComponent(mediaInfo.title)}`;
132 | const searchResultHtml = await proxiedFetchSoaper(searchUrl);
133 | const search$ = cheerio.load(searchResultHtml);
134 |
135 | searchResults = []; // Initialize to empty array before pushing
136 | search$('.thumbnail').each((_, element) => {
137 | const title = search$(element).find('h5 a').first().text().trim();
138 | const yearText = search$(element).find('.img-tip').first().text().trim();
139 | const url = search$(element).find('h5 a').first().attr('href');
140 |
141 | if (title && url) {
142 | searchResults.push({
143 | title,
144 | year: yearText ? parseInt(yearText, 10) : undefined,
145 | url
146 | });
147 | }
148 | });
149 | saveToCache('search', searchCacheKey, searchResults);
150 | } else {
151 | console.log(`[Soaper TV] Using cached search results for "${mediaInfo.title}".`);
152 | }
153 |
154 | console.log(`[Soaper TV] Found ${searchResults.length} search results for "${mediaInfo.title}".`);
155 |
156 | const matchingResult = searchResults.find(x => compareMediaSoaper(mediaInfo, x.title, x.year));
157 |
158 | if (!matchingResult) {
159 | console.log(`[Soaper TV] No matching content found on SoaperTV for "${mediaInfo.title}" (${mediaInfo.year || 'N/A'}).`);
160 | return [];
161 | }
162 |
163 | console.log(`[Soaper TV] Found matching SoaperTV content: "${matchingResult.title}" (${matchingResult.year || 'N/A'}) at ${matchingResult.url}`);
164 | let contentUrl = matchingResult.url;
165 |
166 | if (mediaType === 'tv') {
167 | console.log(`[Soaper TV] Finding Season ${season}, Episode ${episode} for TV show.`);
168 |
169 | const episodeCacheKey = `${contentUrl}-s${season}`.toLowerCase();
170 | let episodeLinks = getFromCache('episodes', episodeCacheKey);
171 |
172 | if (!episodeLinks) {
173 | const showPageHtml = await proxiedFetchSoaper(contentUrl);
174 | const showPage$ = cheerio.load(showPageHtml);
175 |
176 | const seasonBlock = showPage$('h4')
177 | .filter((_, el) => showPage$(el).text().trim().split(':')[0].trim().toLowerCase() === `season${season}`)
178 | .parent();
179 |
180 | if (seasonBlock.length === 0) {
181 | console.log(`[Soaper TV] Season ${season} not found on page.`);
182 | return [];
183 | }
184 |
185 | episodeLinks = []; // Initialize before pushing
186 | seasonBlock.find('a').each((_, el) => {
187 | const episodeNumText = showPage$(el).text().split('.')[0];
188 | const episodeUrl = showPage$(el).attr('href');
189 | if (episodeNumText && episodeUrl) {
190 | episodeLinks.push({
191 | num: parseInt(episodeNumText, 10),
192 | url: episodeUrl
193 | });
194 | }
195 | });
196 | saveToCache('episodes', episodeCacheKey, episodeLinks);
197 | } else {
198 | console.log(`[Soaper TV] Using cached episode links for Season ${season} of ${contentUrl}.`);
199 | }
200 |
201 | const targetEpisode = episodeLinks.find(ep => ep.num === parseInt(episode, 10));
202 |
203 | if (!targetEpisode) {
204 | console.log(`[Soaper TV] Episode ${episode} not found in Season ${season} (using ${episodeLinks.length} cached/parsed links).`);
205 | return [];
206 | }
207 |
208 | contentUrl = targetEpisode.url;
209 | console.log(`[Soaper TV] Found episode page (from cache/parse): ${contentUrl}`);
210 | }
211 |
212 | const contentPageHtml = await proxiedFetchSoaper(contentUrl);
213 | const contentPage$ = cheerio.load(contentPageHtml);
214 | const pass = contentPage$('#hId').attr('value');
215 |
216 | if (!pass) {
217 | console.error('[Soaper TV] Could not find pass value on content page.');
218 | return [];
219 | }
220 | console.log(`[Soaper TV] Found pass value: ${pass}`);
221 |
222 | const infoEndpoint = mediaType === 'tv' ? '/home/index/getEInfoAjax' : '/home/index/getMInfoAjax';
223 | const formData = new URLSearchParams();
224 | formData.append('pass', pass);
225 | formData.append('e2', '0'); // Default value from original script
226 | formData.append('server', '0'); // Default value from original script
227 |
228 | const headers = {
229 | 'referer': `${BASE_URL}${contentUrl}`, // Critical for the API call
230 | 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1',
231 | 'Viewport-Width': '375',
232 | 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', // Specify charset
233 | 'X-Requested-With': 'XMLHttpRequest' // Often needed for AJAX endpoints
234 | };
235 |
236 | console.log(`[Soaper TV] Requesting stream info from ${infoEndpoint} with pass ${pass}.`);
237 | const streamInfoResponse = await proxiedFetchSoaper(infoEndpoint, {
238 | method: 'POST',
239 | body: formData.toString(),
240 | headers: headers
241 | });
242 |
243 | let streamInfo;
244 | if (typeof streamInfoResponse === 'string') {
245 | try {
246 | streamInfo = JSON.parse(streamInfoResponse);
247 | } catch (e) {
248 | console.error('[Soaper TV] Failed to parse stream info JSON:', streamInfoResponse.substring(0, 500));
249 | return [];
250 | }
251 | } else {
252 | streamInfo = streamInfoResponse; // Assuming it's already JSON
253 | }
254 |
255 | if (!streamInfo || !streamInfo.val || typeof streamInfo.val !== 'string') {
256 | console.error('[Soaper TV] No valid stream URL (val) found in response:', streamInfo);
257 | return [];
258 | }
259 |
260 | const streamPath = streamInfo.val;
261 | // Ensure streamPath doesn't already start with BASE_URL or http
262 | const finalStreamUrl = streamPath.startsWith('http') ? streamPath : (streamPath.startsWith('/') ? `${BASE_URL}${streamPath}` : `${BASE_URL}/${streamPath}`);
263 |
264 | console.log(`[Soaper TV] Found stream source: ${finalStreamUrl}`);
265 |
266 | const proxiedStreamUrl = `${PROXY_URL}${encodeURIComponent(finalStreamUrl)}`;
267 |
268 | return [{
269 | url: proxiedStreamUrl,
270 | quality: 'Auto Quality',
271 | provider: 'Soaper TV',
272 | title: `${mediaInfo.title}${mediaType === 'tv' ? ` S${String(season).padStart(2, '0')}E${String(episode).padStart(2, '0')}` : ''} - Soaper TV`,
273 | name: `Soaper TV - Auto`, // Shorter name for Stremio UI
274 | behaviorHints: {
275 | notWebReady: true
276 | },
277 | 源: 'SoaperTV', // Using Chinese character for "source" as seen in other provider
278 | codecs: [], // SoaperTV does not provide detailed codec info easily
279 | size: 'N/A'
280 | }];
281 |
282 | } catch (error) {
283 | console.error(`[Soaper TV] Error in getSoaperTvStreams for TMDB ID ${tmdbId}:`, error.message, error.stack);
284 | return [];
285 | }
286 | }
287 |
288 | module.exports = { getSoaperTvStreams };
--------------------------------------------------------------------------------
/providers/vixsrc.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Vixsrc streaming provider integration for Stremio
3 | * React Native compatible version - Standalone (no external dependencies)
4 | * Converted to Promise-based syntax for sandbox compatibility
5 | */
6 |
7 | // Constants
8 | const TMDB_API_KEY = "68e094699525b18a70bab2f86b1fa706";
9 | const BASE_URL = 'https://vixsrc.to';
10 |
11 | // Helper function to make HTTP requests with default headers
12 | function makeRequest(url, options = {}) {
13 | const defaultHeaders = {
14 | 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
15 | 'Accept': 'application/json,*/*',
16 | 'Accept-Language': 'en-US,en;q=0.5',
17 | 'Accept-Encoding': 'gzip, deflate',
18 | 'Connection': 'keep-alive',
19 | ...options.headers
20 | };
21 |
22 | return fetch(url, {
23 | method: options.method || 'GET',
24 | headers: defaultHeaders,
25 | ...options
26 | })
27 | .then(response => {
28 | if (!response.ok) {
29 | throw new Error(`HTTP ${response.status}: ${response.statusText}`);
30 | }
31 | return response;
32 | })
33 | .catch(error => {
34 | console.error(`[Vixsrc] Request failed for ${url}: ${error.message}`);
35 | throw error;
36 | });
37 | }
38 |
39 | // Helper function to get TMDB info
40 | function getTmdbInfo(tmdbId, mediaType) {
41 | const url = `https://api.themoviedb.org/3/${mediaType === 'tv' ? 'tv' : 'movie'}/${tmdbId}?api_key=${TMDB_API_KEY}`;
42 |
43 | return makeRequest(url)
44 | .then(response => response.json())
45 | .then(data => {
46 | const title = mediaType === 'tv' ? data.name : data.title;
47 | const year = mediaType === 'tv' ? data.first_air_date?.substring(0, 4) : data.release_date?.substring(0, 4);
48 |
49 | if (!title) {
50 | throw new Error('Could not extract title from TMDB response');
51 | }
52 |
53 | console.log(`[Vixsrc] TMDB Info: "${title}" (${year})`);
54 | return { title, year, data };
55 | });
56 | }
57 |
58 | // Helper function to parse M3U8 playlist
59 | function parseM3U8Playlist(content, baseUrl) {
60 | const streams = [];
61 | const audioTracks = [];
62 | const lines = content.split('\n');
63 |
64 | let currentStream = null;
65 |
66 | for (let i = 0; i < lines.length; i++) {
67 | const line = lines[i].trim();
68 |
69 | // Parse video streams
70 | if (line.startsWith('#EXT-X-STREAM-INF:')) {
71 | // Parse stream info
72 | const bandwidthMatch = line.match(/BANDWIDTH=(\d+)/);
73 | const resolutionMatch = line.match(/RESOLUTION=(\d+x\d+)/);
74 | const nameMatch = line.match(/NAME="([^"]+)"/) || line.match(/NAME=([^,]+)/);
75 |
76 | if (bandwidthMatch) {
77 | currentStream = {
78 | bandwidth: parseInt(bandwidthMatch[1]),
79 | resolution: resolutionMatch ? resolutionMatch[1] : 'Unknown',
80 | quality: nameMatch ? nameMatch[1] : getQualityFromResolution(resolutionMatch ? resolutionMatch[1] : 'Unknown'),
81 | url: ''
82 | };
83 | }
84 | }
85 | // Parse audio tracks
86 | else if (line.startsWith('#EXT-X-MEDIA:')) {
87 | const typeMatch = line.match(/TYPE=([^,]+)/);
88 | const nameMatch = line.match(/NAME="([^"]+)"/);
89 | const groupIdMatch = line.match(/GROUP-ID="([^"]+)"/);
90 | const languageMatch = line.match(/LANGUAGE="([^"]+)"/);
91 | const uriMatch = line.match(/URI="([^"]+)"/);
92 |
93 | if (typeMatch && typeMatch[1] === 'AUDIO') {
94 | const audioTrack = {
95 | type: 'audio',
96 | name: nameMatch ? nameMatch[1] : 'Unknown Audio',
97 | groupId: groupIdMatch ? groupIdMatch[1] : 'unknown',
98 | language: languageMatch ? languageMatch[1] : 'unknown',
99 | url: uriMatch ? resolveUrl(uriMatch[1], baseUrl) : null
100 | };
101 | audioTracks.push(audioTrack);
102 | }
103 | }
104 | // Handle URLs for video streams
105 | else if (line.startsWith('http') && currentStream) {
106 | // This is the URL for the current video stream
107 | currentStream.url = line.startsWith('http') ? line : resolveUrl(line, baseUrl);
108 | streams.push(currentStream);
109 | currentStream = null;
110 | }
111 | }
112 |
113 | console.log(`[Vixsrc] Found ${audioTracks.length} audio tracks:`);
114 | audioTracks.forEach((track, index) => {
115 | console.log(` ${index + 1}. ${track.name} (${track.language}) - ${track.url ? 'Available' : 'No URL'}`);
116 | });
117 |
118 | return { streams, audioTracks };
119 | }
120 |
121 | // Helper function to get quality from resolution
122 | function getQualityFromResolution(resolution) {
123 | if (resolution.includes('1920x1080') || resolution.includes('1080')) {
124 | return '1080p';
125 | } else if (resolution.includes('1280x720') || resolution.includes('720')) {
126 | return '720p';
127 | } else if (resolution.includes('854x480') || resolution.includes('640x480') || resolution.includes('480')) {
128 | return '480p';
129 | } else if (resolution.includes('640x360') || resolution.includes('360')) {
130 | return '360p';
131 | } else {
132 | return resolution;
133 | }
134 | }
135 |
136 | // Helper function to resolve URLs
137 | function resolveUrl(url, baseUrl) {
138 | if (url.startsWith('http')) {
139 | return url;
140 | }
141 |
142 | // Handle relative URLs
143 | const baseUrlObj = new URL(baseUrl);
144 | if (url.startsWith('/')) {
145 | return `${baseUrlObj.protocol}//${baseUrlObj.host}${url}`;
146 | } else {
147 | const basePath = baseUrlObj.pathname.substring(0, baseUrlObj.pathname.lastIndexOf('/') + 1);
148 | return `${baseUrlObj.protocol}//${baseUrlObj.host}${basePath}${url}`;
149 | }
150 | }
151 |
152 | // Helper function to extract stream URL from Vixsrc page
153 | function extractStreamFromPage(url, contentType, contentId, seasonNum, episodeNum) {
154 | let vixsrcUrl;
155 | let subtitleApiUrl;
156 |
157 | if (contentType === 'movie') {
158 | vixsrcUrl = `${BASE_URL}/movie/${contentId}`;
159 | subtitleApiUrl = `https://sub.wyzie.ru/search?id=${contentId}`;
160 | } else {
161 | vixsrcUrl = `${BASE_URL}/tv/${contentId}/${seasonNum}/${episodeNum}`;
162 | subtitleApiUrl = `https://sub.wyzie.ru/search?id=${contentId}&season=${seasonNum}&episode=${episodeNum}`;
163 | }
164 |
165 | console.log(`[Vixsrc] Fetching: ${vixsrcUrl}`);
166 |
167 | return makeRequest(vixsrcUrl, {
168 | headers: {
169 | 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'
170 | }
171 | })
172 | .then(response => response.text())
173 | .then(html => {
174 | console.log(`[Vixsrc] HTML length: ${html.length} characters`);
175 |
176 | let masterPlaylistUrl = null;
177 |
178 | // Method 1: Look for window.masterPlaylist (primary method)
179 | if (html.includes('window.masterPlaylist')) {
180 | console.log('[Vixsrc] Found window.masterPlaylist');
181 |
182 | const urlMatch = html.match(/url:\s*['"]([^'"]+)['"]/);
183 | const tokenMatch = html.match(/['"]?token['"]?\s*:\s*['"]([^'"]+)['"]/);
184 | const expiresMatch = html.match(/['"]?expires['"]?\s*:\s*['"]([^'"]+)['"]/);
185 |
186 | if (urlMatch && tokenMatch && expiresMatch) {
187 | const baseUrl = urlMatch[1];
188 | const token = tokenMatch[1];
189 | const expires = expiresMatch[1];
190 |
191 | console.log('[Vixsrc] Extracted tokens:');
192 | console.log(` - Base URL: ${baseUrl}`);
193 | console.log(` - Token: ${token.substring(0, 20)}...`);
194 | console.log(` - Expires: ${expires}`);
195 |
196 | // Construct the master playlist URL
197 | if (baseUrl.includes('?b=1')) {
198 | masterPlaylistUrl = `${baseUrl}&token=${token}&expires=${expires}&h=1&lang=en`;
199 | } else {
200 | masterPlaylistUrl = `${baseUrl}?token=${token}&expires=${expires}&h=1&lang=en`;
201 | }
202 |
203 | console.log(`[Vixsrc] Constructed master playlist URL: ${masterPlaylistUrl}`);
204 | }
205 | }
206 |
207 | // Method 2: Look for direct .m3u8 URLs
208 | if (!masterPlaylistUrl) {
209 | const m3u8Match = html.match(/(https?:\/\/[^'"\s]+\.m3u8[^'"\s]*)/);
210 | if (m3u8Match) {
211 | masterPlaylistUrl = m3u8Match[1];
212 | console.log('[Vixsrc] Found direct .m3u8 URL:', masterPlaylistUrl);
213 | }
214 | }
215 |
216 | // Method 3: Look for stream URLs in script tags
217 | if (!masterPlaylistUrl) {
218 | const scriptMatches = html.match(/