Update index.js
Browse files
index.js
CHANGED
|
@@ -1,202 +1,352 @@
|
|
|
|
|
|
|
|
| 1 |
const express = require('express');
|
| 2 |
-
const fetch
|
| 3 |
|
| 4 |
-
const app
|
| 5 |
const PORT = process.env.PORT || 7860;
|
| 6 |
|
|
|
|
| 7 |
app.use((req, res, next) => {
|
| 8 |
-
res.header('Access-Control-Allow-Origin',
|
| 9 |
res.header('Access-Control-Allow-Headers', '*');
|
| 10 |
res.header('Access-Control-Allow-Methods', 'GET, OPTIONS');
|
| 11 |
if (req.method === 'OPTIONS') return res.sendStatus(200);
|
| 12 |
next();
|
| 13 |
});
|
| 14 |
|
|
|
|
|
|
|
| 15 |
const UPSTREAM_URLS = [
|
| 16 |
-
'https://torrentio.strem.fun',
|
| 17 |
-
'https://
|
| 18 |
];
|
| 19 |
|
| 20 |
-
const
|
| 21 |
-
|
| 22 |
-
const
|
|
|
|
|
|
|
| 23 |
const MAX_SIZE_GB_SERIES = 1.5;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
|
| 25 |
const SOURCE_CONFIG = [
|
| 26 |
-
{ name: 'yts',
|
| 27 |
-
{ name: 'yify',
|
| 28 |
-
{ name: '
|
| 29 |
-
{ name: '
|
| 30 |
-
{ name: '
|
| 31 |
-
{ name: '
|
| 32 |
-
{ name: '
|
| 33 |
-
{ name: '
|
| 34 |
-
{ name: '
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
];
|
| 40 |
|
| 41 |
const QUALITY_RANK = {
|
| 42 |
-
'
|
| 43 |
-
'
|
| 44 |
-
'
|
| 45 |
-
'webrip': 4,
|
| 46 |
-
'576p':
|
| 47 |
-
'480p':
|
| 48 |
-
'bluray': 7
|
| 49 |
};
|
| 50 |
|
| 51 |
-
|
| 52 |
|
| 53 |
function getQuality(title = '') {
|
| 54 |
-
const
|
| 55 |
-
if (
|
| 56 |
-
if (
|
| 57 |
-
if (
|
| 58 |
-
if (
|
| 59 |
-
if (
|
| 60 |
-
if (
|
| 61 |
-
if (
|
|
|
|
| 62 |
return null;
|
| 63 |
}
|
| 64 |
|
| 65 |
function getSeeders(stream) {
|
| 66 |
if (stream.behaviorHints?.seeders) return stream.behaviorHints.seeders;
|
| 67 |
-
const
|
| 68 |
-
return
|
| 69 |
}
|
| 70 |
|
| 71 |
-
function
|
| 72 |
const title = stream.title || '';
|
| 73 |
-
const
|
| 74 |
-
|
| 75 |
-
if (!
|
| 76 |
-
|
| 77 |
-
const
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
if (unit === 'GB') return value;
|
| 81 |
-
if (unit === 'MB') return value / 1024;
|
| 82 |
-
return null;
|
| 83 |
}
|
| 84 |
|
| 85 |
function detectSource(title = '') {
|
| 86 |
const lower = title.toLowerCase();
|
| 87 |
for (const src of SOURCE_CONFIG) {
|
| 88 |
-
if (lower.includes(src.name)) return src
|
| 89 |
}
|
| 90 |
return null;
|
| 91 |
}
|
| 92 |
|
| 93 |
-
function
|
| 94 |
-
const
|
| 95 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
}
|
| 97 |
|
| 98 |
-
|
| 99 |
-
|
|
|
|
| 100 |
if (!stream.url && !stream.infoHash) return false;
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
|
|
|
| 105 |
if (getSeeders(stream) < MIN_SEEDERS) return false;
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
const maxSize = type === 'series' ? MAX_SIZE_GB_SERIES : MAX_SIZE_GB_MOVIE;
|
| 110 |
-
const sizeGB = getSizeInGB(stream);
|
| 111 |
if (sizeGB !== null && sizeGB > maxSize) return false;
|
| 112 |
-
|
| 113 |
return true;
|
| 114 |
}
|
| 115 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
async function fetchUpstream(type, id) {
|
| 117 |
-
for (const
|
| 118 |
try {
|
| 119 |
const controller = new AbortController();
|
| 120 |
-
const
|
| 121 |
-
const
|
| 122 |
-
|
|
|
|
| 123 |
if (res.ok) {
|
| 124 |
const data = await res.json();
|
| 125 |
-
if (data.streams?.length)
|
|
|
|
|
|
|
|
|
|
| 126 |
}
|
| 127 |
-
} catch (e) {
|
|
|
|
|
|
|
| 128 |
}
|
| 129 |
return [];
|
| 130 |
}
|
| 131 |
|
| 132 |
-
function
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
const
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
const sourceB = detectSource(b.title);
|
| 146 |
-
const orderA = getSourceOrder(sourceA);
|
| 147 |
-
const orderB = getSourceOrder(sourceB);
|
| 148 |
-
if (orderA !== orderB) return orderA - orderB;
|
| 149 |
-
return getSeeders(b) - getSeeders(a);
|
| 150 |
-
});
|
| 151 |
}
|
| 152 |
-
|
| 153 |
-
const result = [];
|
| 154 |
-
const sortedQualities = Object.keys(byQuality).sort((a, b) => {
|
| 155 |
-
const rankA = QUALITY_RANK[a] || 999;
|
| 156 |
-
const rankB = QUALITY_RANK[b] || 999;
|
| 157 |
-
return rankA - rankB;
|
| 158 |
-
});
|
| 159 |
-
|
| 160 |
-
sortedQualities.forEach(quality => {
|
| 161 |
-
const topLinks = byQuality[quality].slice(0, LINKS_PER_QUALITY);
|
| 162 |
-
result.push(...topLinks);
|
| 163 |
-
});
|
| 164 |
-
|
| 165 |
-
return result;
|
| 166 |
}
|
| 167 |
|
|
|
|
|
|
|
| 168 |
app.get('/manifest.json', (req, res) => {
|
| 169 |
res.json({
|
| 170 |
-
id:
|
| 171 |
-
name:
|
| 172 |
-
description: 'Stream Everything
|
| 173 |
-
version:
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
|
|
|
| 178 |
});
|
| 179 |
});
|
| 180 |
|
| 181 |
app.get('/stream/:type/:id.json', async (req, res) => {
|
| 182 |
const { type, id } = req.params;
|
| 183 |
try {
|
| 184 |
-
const
|
| 185 |
-
const sorted
|
| 186 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 187 |
} catch (e) {
|
|
|
|
| 188 |
res.json({ streams: [] });
|
| 189 |
}
|
| 190 |
});
|
| 191 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 192 |
app.get('/debug/:type/:id.json', async (req, res) => {
|
| 193 |
const { type, id } = req.params;
|
| 194 |
try {
|
| 195 |
const streams = await fetchUpstream(type, id);
|
| 196 |
-
res.json({ streams });
|
| 197 |
} catch (e) {
|
| 198 |
res.status(500).json({ error: e.message });
|
| 199 |
}
|
| 200 |
});
|
| 201 |
|
| 202 |
-
|
|
|
|
|
|
|
|
|
| 1 |
+
'use strict';
|
| 2 |
+
|
| 3 |
const express = require('express');
|
| 4 |
+
const fetch = require('node-fetch');
|
| 5 |
|
| 6 |
+
const app = express();
|
| 7 |
const PORT = process.env.PORT || 7860;
|
| 8 |
|
| 9 |
+
// βββ CORS ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 10 |
app.use((req, res, next) => {
|
| 11 |
+
res.header('Access-Control-Allow-Origin', '*');
|
| 12 |
res.header('Access-Control-Allow-Headers', '*');
|
| 13 |
res.header('Access-Control-Allow-Methods', 'GET, OPTIONS');
|
| 14 |
if (req.method === 'OPTIONS') return res.sendStatus(200);
|
| 15 |
next();
|
| 16 |
});
|
| 17 |
|
| 18 |
+
// βββ CONFIG ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 19 |
+
|
| 20 |
const UPSTREAM_URLS = [
|
| 21 |
+
'https://torrentio.strem.fun/providers=eztv,thepiratebay,nyaasi|qualityfilter=scr,cam,threed,unknown',
|
| 22 |
+
'https://vanseleem-gsplus.hf.space/',
|
| 23 |
];
|
| 24 |
|
| 25 |
+
const SUBTITLES_URL = 'https://vanseleem-vansubs.hf.space';
|
| 26 |
+
|
| 27 |
+
const MIN_SEEDERS = 2;
|
| 28 |
+
const BLOCK_SIZELESS = true;
|
| 29 |
+
const MAX_SIZE_GB_MOVIE = 4;
|
| 30 |
const MAX_SIZE_GB_SERIES = 1.5;
|
| 31 |
+
const LINKS_PER_QUALITY = 5;
|
| 32 |
+
const FETCH_TIMEOUT_MS = 12_000;
|
| 33 |
+
|
| 34 |
+
const ALLOWED_QUALITIES = ['4k', '2160p', '1080p', '720p', '576p', '480p'];
|
| 35 |
|
| 36 |
const SOURCE_CONFIG = [
|
| 37 |
+
{ name: 'yts', display: 'YTS', order: 1 },
|
| 38 |
+
{ name: 'yify', display: 'YTS', order: 1 },
|
| 39 |
+
{ name: 'knaben', display: 'Knaben', order: 2 },
|
| 40 |
+
{ name: 'eztv', display: 'EZTV', order: 3 },
|
| 41 |
+
{ name: 'nyaasi', display: 'Nyaa', order: 4 },
|
| 42 |
+
{ name: 'thepiratebay', display: 'TPB', order: 5 },
|
| 43 |
+
{ name: 'piratebay', display: 'TPB', order: 5 },
|
| 44 |
+
{ name: 'the piratebay',display: 'TPB', order: 5 },
|
| 45 |
+
{ name: 'tpb', display: 'TPB', order: 5 },
|
| 46 |
+
];
|
| 47 |
+
|
| 48 |
+
const BLOCKED_PROVIDERS = [
|
| 49 |
+
'rarbg', '1337x', 'kickasstorrents', 'torrentgalaxy', 'magnetdl',
|
| 50 |
+
'horriblesubs', 'tokyotosho', 'anidex', 'rutor', 'rutracker',
|
| 51 |
+
'comando', 'bludv', 'micoleaodublado',
|
| 52 |
+
'torrent9', 'ilcorsaronero',
|
| 53 |
+
'mejortorrent', 'wolfmax4k', 'cinecalidad', 'besttorrents',
|
| 54 |
];
|
| 55 |
|
| 56 |
const QUALITY_RANK = {
|
| 57 |
+
'4k': 1, '2160p': 1,
|
| 58 |
+
'1080p': 2,
|
| 59 |
+
'720p': 3,
|
| 60 |
+
'webdl': 4, 'webrip': 4,
|
| 61 |
+
'576p': 5,
|
| 62 |
+
'480p': 6,
|
| 63 |
+
'bluray': 7,
|
| 64 |
};
|
| 65 |
|
| 66 |
+
// βββ STREAM HELPERS ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 67 |
|
| 68 |
function getQuality(title = '') {
|
| 69 |
+
const t = title.toLowerCase();
|
| 70 |
+
if (t.includes('4k') || t.includes('2160p')) return '4k';
|
| 71 |
+
if (t.includes('1080p')) return '1080p';
|
| 72 |
+
if (t.includes('720p')) return '720p';
|
| 73 |
+
if (t.includes('576p')) return '576p';
|
| 74 |
+
if (t.includes('480p')) return '480p';
|
| 75 |
+
if (t.includes('webrip')) return 'webrip';
|
| 76 |
+
if (t.includes('webdl') || t.includes('web-dl')) return 'webdl';
|
| 77 |
+
if (t.includes('bluray')) return 'bluray';
|
| 78 |
return null;
|
| 79 |
}
|
| 80 |
|
| 81 |
function getSeeders(stream) {
|
| 82 |
if (stream.behaviorHints?.seeders) return stream.behaviorHints.seeders;
|
| 83 |
+
const m = (stream.title || '').match(/π€\s*(\d+)/);
|
| 84 |
+
return m ? parseInt(m[1], 10) : 0;
|
| 85 |
}
|
| 86 |
|
| 87 |
+
function getSizeGB(stream) {
|
| 88 |
const title = stream.title || '';
|
| 89 |
+
const m = title.match(/πΎ\s*([\d.]+)\s*(GB|MB)/i) ||
|
| 90 |
+
title.match(/([\d.]+)\s*(GB|MB)/i);
|
| 91 |
+
if (!m) return null;
|
| 92 |
+
const val = parseFloat(m[1]);
|
| 93 |
+
const unit = m[2].toUpperCase();
|
| 94 |
+
return unit === 'GB' ? val : val / 1024;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
}
|
| 96 |
|
| 97 |
function detectSource(title = '') {
|
| 98 |
const lower = title.toLowerCase();
|
| 99 |
for (const src of SOURCE_CONFIG) {
|
| 100 |
+
if (lower.includes(src.name)) return src;
|
| 101 |
}
|
| 102 |
return null;
|
| 103 |
}
|
| 104 |
|
| 105 |
+
function isBlocked(stream) {
|
| 106 |
+
const haystack = (
|
| 107 |
+
(stream.behaviorHints?.bingeGroup || '') + ' ' +
|
| 108 |
+
(stream.title || '') + ' ' +
|
| 109 |
+
(stream.name || '')
|
| 110 |
+
).toLowerCase();
|
| 111 |
+
return BLOCKED_PROVIDERS.some(p => haystack.includes(p));
|
| 112 |
}
|
| 113 |
|
| 114 |
+
// βββ FILTER & SORT βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 115 |
+
|
| 116 |
+
function filterStream(stream, type) {
|
| 117 |
if (!stream.url && !stream.infoHash) return false;
|
| 118 |
+
if (isBlocked(stream)) return false;
|
| 119 |
+
|
| 120 |
+
const quality = getQuality(stream.title);
|
| 121 |
+
if (!quality || !ALLOWED_QUALITIES.includes(quality)) return false;
|
| 122 |
+
|
| 123 |
if (getSeeders(stream) < MIN_SEEDERS) return false;
|
| 124 |
+
|
| 125 |
+
const sizeGB = getSizeGB(stream);
|
| 126 |
+
if (BLOCK_SIZELESS && sizeGB === null) return false;
|
| 127 |
const maxSize = type === 'series' ? MAX_SIZE_GB_SERIES : MAX_SIZE_GB_MOVIE;
|
|
|
|
| 128 |
if (sizeGB !== null && sizeGB > maxSize) return false;
|
| 129 |
+
|
| 130 |
return true;
|
| 131 |
}
|
| 132 |
|
| 133 |
+
function sortAndGroup(streams, type) {
|
| 134 |
+
const filtered = streams.filter(s => filterStream(s, type));
|
| 135 |
+
|
| 136 |
+
// Deduplicate by infoHash then by title+size fingerprint
|
| 137 |
+
const seen = new Set();
|
| 138 |
+
const unique = filtered.filter(s => {
|
| 139 |
+
const key = s.infoHash
|
| 140 |
+
? s.infoHash.toLowerCase()
|
| 141 |
+
: `${(s.title || '').trim().toLowerCase()}::${getSizeGB(s)}`;
|
| 142 |
+
if (seen.has(key)) return false;
|
| 143 |
+
seen.add(key);
|
| 144 |
+
return true;
|
| 145 |
+
});
|
| 146 |
+
|
| 147 |
+
// Group by quality bucket
|
| 148 |
+
const byQuality = {};
|
| 149 |
+
unique.forEach(s => {
|
| 150 |
+
const q = getQuality(s.title);
|
| 151 |
+
if (!byQuality[q]) byQuality[q] = [];
|
| 152 |
+
byQuality[q].push(s);
|
| 153 |
+
});
|
| 154 |
+
|
| 155 |
+
// Within each bucket: priority source order, then seeders desc
|
| 156 |
+
for (const q in byQuality) {
|
| 157 |
+
byQuality[q].sort((a, b) => {
|
| 158 |
+
const srcA = detectSource(a.title);
|
| 159 |
+
const srcB = detectSource(b.title);
|
| 160 |
+
const ordA = srcA ? srcA.order : 999;
|
| 161 |
+
const ordB = srcB ? srcB.order : 999;
|
| 162 |
+
if (ordA !== ordB) return ordA - ordB;
|
| 163 |
+
return getSeeders(b) - getSeeders(a);
|
| 164 |
+
});
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
// Flatten: best quality first, capped at LINKS_PER_QUALITY per bucket
|
| 168 |
+
const sortedQualities = Object.keys(byQuality).sort(
|
| 169 |
+
(a, b) => (QUALITY_RANK[a] || 999) - (QUALITY_RANK[b] || 999)
|
| 170 |
+
);
|
| 171 |
+
|
| 172 |
+
const result = [];
|
| 173 |
+
sortedQualities.forEach(q => {
|
| 174 |
+
result.push(...byQuality[q].slice(0, LINKS_PER_QUALITY));
|
| 175 |
+
});
|
| 176 |
+
|
| 177 |
+
return result;
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
// βββ TITLE BUILDER βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 181 |
+
|
| 182 |
+
function formatBytes(gb) {
|
| 183 |
+
if (gb === null) return null;
|
| 184 |
+
return gb >= 1 ? `${gb.toFixed(2)} GB` : `${(gb * 1024).toFixed(0)} MB`;
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
function extractMovieName(title = '') {
|
| 188 |
+
return title
|
| 189 |
+
.replace(/\b(4k|2160p|1080p?|720p?|576p|480p?|360p?|240p?)\b.*/i, '')
|
| 190 |
+
.replace(/\b(bluray|webrip|webdl|web-dl|hdtv|dvdrip)\b.*/i, '')
|
| 191 |
+
.replace(/[\.\-_]+/g, ' ')
|
| 192 |
+
.replace(/\s*\(\d{4}\)\s*$/, '')
|
| 193 |
+
.trim();
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
function extractYear(title = '') {
|
| 197 |
+
const m = title.match(/\b(19\d{2}|20\d{2})\b/);
|
| 198 |
+
return m ? m[1] : '';
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
function getAudio(title = '') {
|
| 202 |
+
if (/DDP?\s*Atmos/i.test(title)) return 'Atmos';
|
| 203 |
+
if (/DDP|EAC3/i.test(title)) return 'DD+';
|
| 204 |
+
if (/DTS[-\s]?HD/i.test(title)) return 'DTS-HD';
|
| 205 |
+
if (/DTS/i.test(title)) return 'DTS';
|
| 206 |
+
if (/TrueHD/i.test(title)) return 'TrueHD';
|
| 207 |
+
if (/AAC/i.test(title)) return 'AAC';
|
| 208 |
+
if (/AC3|Dolby/i.test(title)) return 'DD';
|
| 209 |
+
return '';
|
| 210 |
+
}
|
| 211 |
+
|
| 212 |
+
function getLanguages(title = '') {
|
| 213 |
+
const map = {
|
| 214 |
+
english: 'EN', french: 'FR', spanish: 'ES', german: 'DE',
|
| 215 |
+
italian: 'IT', portuguese: 'PT', russian: 'RU', japanese: 'JA',
|
| 216 |
+
korean: 'KO', chinese: 'ZH', hindi: 'HI', arabic: 'AR',
|
| 217 |
+
};
|
| 218 |
+
const lower = title.toLowerCase();
|
| 219 |
+
return Object.entries(map).filter(([k]) => lower.includes(k)).map(([, v]) => v);
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
function buildTitle(stream) {
|
| 223 |
+
const raw = stream._rawTitle || stream.title || '';
|
| 224 |
+
const name = extractMovieName(raw);
|
| 225 |
+
const year = extractYear(raw);
|
| 226 |
+
const disp = year ? `${name} (${year})` : name;
|
| 227 |
+
|
| 228 |
+
const seeders = getSeeders(stream);
|
| 229 |
+
const sizeGB = getSizeGB(stream);
|
| 230 |
+
const src = detectSource(raw);
|
| 231 |
+
const srcLabel = src ? src.display : 'Unknown';
|
| 232 |
+
const audio = getAudio(raw);
|
| 233 |
+
const langs = getLanguages(raw);
|
| 234 |
+
|
| 235 |
+
const lines = [
|
| 236 |
+
`π ${disp}`,
|
| 237 |
+
`π± ${seeders}`,
|
| 238 |
+
sizeGB !== null ? `πΎ ${formatBytes(sizeGB)}` : 'β οΈ Size unknown',
|
| 239 |
+
`π
${srcLabel}`,
|
| 240 |
+
];
|
| 241 |
+
|
| 242 |
+
let l5 = '';
|
| 243 |
+
if (langs.length) l5 += `π¬π§ ${langs.join(' β’ ')}`;
|
| 244 |
+
if (audio) l5 += (l5 ? ' ' : '') + `π ${audio}`;
|
| 245 |
+
if (l5) lines.push(l5);
|
| 246 |
+
|
| 247 |
+
return lines.join('\n');
|
| 248 |
+
}
|
| 249 |
+
|
| 250 |
+
// βββ UPSTREAM FETCH ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 251 |
+
|
| 252 |
async function fetchUpstream(type, id) {
|
| 253 |
+
for (const base of UPSTREAM_URLS) {
|
| 254 |
try {
|
| 255 |
const controller = new AbortController();
|
| 256 |
+
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
| 257 |
+
const url = `${base}/stream/${type}/${id}.json`;
|
| 258 |
+
const res = await fetch(url, { signal: controller.signal });
|
| 259 |
+
clearTimeout(timer);
|
| 260 |
if (res.ok) {
|
| 261 |
const data = await res.json();
|
| 262 |
+
if (data.streams?.length) {
|
| 263 |
+
console.log(`[upstream] ${data.streams.length} streams from ${url}`);
|
| 264 |
+
return data.streams;
|
| 265 |
+
}
|
| 266 |
}
|
| 267 |
+
} catch (e) {
|
| 268 |
+
console.warn(`[upstream] failed: ${e.message}`);
|
| 269 |
+
}
|
| 270 |
}
|
| 271 |
return [];
|
| 272 |
}
|
| 273 |
|
| 274 |
+
async function fetchSubtitles(type, id) {
|
| 275 |
+
try {
|
| 276 |
+
const controller = new AbortController();
|
| 277 |
+
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
| 278 |
+
const url = `${SUBTITLES_URL}/subtitles/${type}/${id}.json`;
|
| 279 |
+
const res = await fetch(url, { signal: controller.signal });
|
| 280 |
+
clearTimeout(timer);
|
| 281 |
+
if (res.ok) {
|
| 282 |
+
const data = await res.json();
|
| 283 |
+
return data.subtitles || [];
|
| 284 |
+
}
|
| 285 |
+
} catch (e) {
|
| 286 |
+
console.warn(`[subtitles] failed: ${e.message}`);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 287 |
}
|
| 288 |
+
return [];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 289 |
}
|
| 290 |
|
| 291 |
+
// βββ ROUTES ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 292 |
+
|
| 293 |
app.get('/manifest.json', (req, res) => {
|
| 294 |
res.json({
|
| 295 |
+
id: 'community.ghostreamplus',
|
| 296 |
+
name: 'Ghostream+ π¬',
|
| 297 |
+
description: 'Stream Everything you want\nYTS Β· Knaben\nMade with π€ by Van',
|
| 298 |
+
version: '7.0.5',
|
| 299 |
+
logo: 'https://i.imgur.com/wEYbVqL.png',
|
| 300 |
+
resources: ['stream', 'subtitles'],
|
| 301 |
+
types: ['movie', 'series'],
|
| 302 |
+
idPrefixes: ['tt'],
|
| 303 |
+
catalogs: [],
|
| 304 |
});
|
| 305 |
});
|
| 306 |
|
| 307 |
app.get('/stream/:type/:id.json', async (req, res) => {
|
| 308 |
const { type, id } = req.params;
|
| 309 |
try {
|
| 310 |
+
const raw = await fetchUpstream(type, id);
|
| 311 |
+
const sorted = sortAndGroup(raw, type);
|
| 312 |
+
|
| 313 |
+
// Apply unified display title
|
| 314 |
+
const titled = sorted.map(s => {
|
| 315 |
+
const clone = Object.assign({}, s);
|
| 316 |
+
clone._rawTitle = s.title || s.name || '';
|
| 317 |
+
clone.title = buildTitle(clone);
|
| 318 |
+
return clone;
|
| 319 |
+
});
|
| 320 |
+
|
| 321 |
+
console.log(`[handler] ${type}/${id} β ${titled.length} final streams`);
|
| 322 |
+
res.json({ streams: titled });
|
| 323 |
} catch (e) {
|
| 324 |
+
console.error('[handler] error:', e);
|
| 325 |
res.json({ streams: [] });
|
| 326 |
}
|
| 327 |
});
|
| 328 |
|
| 329 |
+
app.get('/subtitles/:type/:id.json', async (req, res) => {
|
| 330 |
+
const { type, id } = req.params;
|
| 331 |
+
try {
|
| 332 |
+
const subs = await fetchSubtitles(type, id);
|
| 333 |
+
res.json({ subtitles: subs });
|
| 334 |
+
} catch (e) {
|
| 335 |
+
res.json({ subtitles: [] });
|
| 336 |
+
}
|
| 337 |
+
});
|
| 338 |
+
|
| 339 |
+
// Debug: raw upstream response before any filtering
|
| 340 |
app.get('/debug/:type/:id.json', async (req, res) => {
|
| 341 |
const { type, id } = req.params;
|
| 342 |
try {
|
| 343 |
const streams = await fetchUpstream(type, id);
|
| 344 |
+
res.json({ count: streams.length, streams });
|
| 345 |
} catch (e) {
|
| 346 |
res.status(500).json({ error: e.message });
|
| 347 |
}
|
| 348 |
});
|
| 349 |
|
| 350 |
+
// βββ START βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 351 |
+
|
| 352 |
+
app.listen(PORT, () => console.log(`Ghostream+ π¬ running on http://0.0.0.0:${PORT}`));
|