Update index.js
Browse files
index.js
CHANGED
|
@@ -1,200 +1,135 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
if (!byQuality[quality]) byQuality[quality] = [];
|
| 137 |
-
byQuality[quality].push(stream);
|
| 138 |
-
});
|
| 139 |
-
|
| 140 |
-
for (const quality in byQuality) {
|
| 141 |
-
byQuality[quality].sort((a, b) => {
|
| 142 |
-
const sourceA = detectSource(a.title);
|
| 143 |
-
const sourceB = detectSource(b.title);
|
| 144 |
-
const orderA = getSourceOrder(sourceA);
|
| 145 |
-
const orderB = getSourceOrder(sourceB);
|
| 146 |
-
if (orderA !== orderB) return orderA - orderB;
|
| 147 |
-
return getSeeders(b) - getSeeders(a);
|
| 148 |
-
});
|
| 149 |
-
}
|
| 150 |
-
|
| 151 |
-
const result = [];
|
| 152 |
-
const sortedQualities = Object.keys(byQuality).sort((a, b) => {
|
| 153 |
-
const rankA = QUALITY_RANK[a] || 999;
|
| 154 |
-
const rankB = QUALITY_RANK[b] || 999;
|
| 155 |
-
return rankA - rankB;
|
| 156 |
-
});
|
| 157 |
-
|
| 158 |
-
sortedQualities.forEach(quality => {
|
| 159 |
-
const topLinks = byQuality[quality].slice(0, LINKS_PER_QUALITY);
|
| 160 |
-
result.push(...topLinks);
|
| 161 |
-
});
|
| 162 |
-
|
| 163 |
-
return result;
|
| 164 |
-
}
|
| 165 |
-
|
| 166 |
-
app.get('/manifest.json', (req, res) => {
|
| 167 |
-
res.json({
|
| 168 |
-
id: 'org.ghostream.platinum',
|
| 169 |
-
name: 'Ghostream 🚀',
|
| 170 |
-
description: 'Stream Everything You Want\nMade With 🤍 By Van',
|
| 171 |
-
version: '7.3.0',
|
| 172 |
-
resources: ['stream'],
|
| 173 |
-
types: ['movie', 'series'],
|
| 174 |
-
idPrefixes: ['tt'],
|
| 175 |
-
catalogs: []
|
| 176 |
-
});
|
| 177 |
-
});
|
| 178 |
-
|
| 179 |
-
app.get('/stream/:type/:id.json', async (req, res) => {
|
| 180 |
-
const { type, id } = req.params;
|
| 181 |
-
try {
|
| 182 |
-
const streams = await fetchUpstream(type, id);
|
| 183 |
-
const sorted = sortAndGroup(streams);
|
| 184 |
-
res.json({ streams: sorted });
|
| 185 |
-
} catch (e) {
|
| 186 |
-
res.json({ streams: [] });
|
| 187 |
-
}
|
| 188 |
-
});
|
| 189 |
-
|
| 190 |
-
app.get('/debug/:type/:id.json', async (req, res) => {
|
| 191 |
-
const { type, id } = req.params;
|
| 192 |
-
try {
|
| 193 |
-
const streams = await fetchUpstream(type, id);
|
| 194 |
-
res.json({ streams });
|
| 195 |
-
} catch (e) {
|
| 196 |
-
res.status(500).json({ error: e.message });
|
| 197 |
-
}
|
| 198 |
-
});
|
| 199 |
-
|
| 200 |
-
app.listen(PORT, () => console.log(`Ghostream 🚀 on ${PORT}`));
|
|
|
|
| 1 |
+
from fastapi import FastAPI, Request
|
| 2 |
+
from fastapi.responses import JSONResponse, FileResponse
|
| 3 |
+
import httpx
|
| 4 |
+
import re
|
| 5 |
+
import os
|
| 6 |
+
from typing import List, Dict, Any
|
| 7 |
+
|
| 8 |
+
app = FastAPI()
|
| 9 |
+
|
| 10 |
+
UPSTREAM_URLS = [
|
| 11 |
+
"https://torrentio.strem.fun",
|
| 12 |
+
"https://thepiratebay-plus.strem.fun",
|
| 13 |
+
]
|
| 14 |
+
|
| 15 |
+
# Quality order: highest first. Exclude 4K, screeners, cam, 3D.
|
| 16 |
+
QUALITY_ORDER = [
|
| 17 |
+
"1080p", "720p", "576p", "480p", "360p", "240p",
|
| 18 |
+
"1080", "720", "480", "360",
|
| 19 |
+
"hdrip", "webrip", "brrip", "hdtv", "bluray", "dvdrip",
|
| 20 |
+
# all others will be appended at the end (lowest)
|
| 21 |
+
]
|
| 22 |
+
|
| 23 |
+
# Blacklist keywords (case‑insensitive)
|
| 24 |
+
EXCLUDE_KEYWORDS = ["4k", "2160p", "screenr", "screener", "cam", "3d"]
|
| 25 |
+
|
| 26 |
+
MAX_MOVIE_SIZE_GB = 4
|
| 27 |
+
MAX_SERIES_SIZE_GB = 1.5
|
| 28 |
+
LINKS_PER_QUALITY = 5
|
| 29 |
+
|
| 30 |
+
def parse_size_to_gb(size_str: str) -> float:
|
| 31 |
+
"""Convert size string like '2.5 GB' or '800 MB' to GB float."""
|
| 32 |
+
if not size_str:
|
| 33 |
+
return None
|
| 34 |
+
match = re.match(r"([\d.]+)\s*(GB|MB|TB)", size_str, re.IGNORECASE)
|
| 35 |
+
if not match:
|
| 36 |
+
return None
|
| 37 |
+
value = float(match.group(1))
|
| 38 |
+
unit = match.group(2).upper()
|
| 39 |
+
if unit == "TB":
|
| 40 |
+
return value * 1024
|
| 41 |
+
elif unit == "GB":
|
| 42 |
+
return value
|
| 43 |
+
elif unit == "MB":
|
| 44 |
+
return value / 1024
|
| 45 |
+
return None
|
| 46 |
+
|
| 47 |
+
def extract_quality(name: str) -> str:
|
| 48 |
+
"""Extract a quality label like '1080p' from a stream name."""
|
| 49 |
+
# Try pattern like 1080p, 720p, etc.
|
| 50 |
+
m = re.search(r"\b(\d{3,4}p)\b", name)
|
| 51 |
+
if m:
|
| 52 |
+
return m.group(1)
|
| 53 |
+
# Fallback: look for known labels
|
| 54 |
+
name_lower = name.lower()
|
| 55 |
+
for q in QUALITY_ORDER:
|
| 56 |
+
if q in name_lower:
|
| 57 |
+
return q
|
| 58 |
+
# If nothing found, treat as 'other'
|
| 59 |
+
return "other"
|
| 60 |
+
|
| 61 |
+
def quality_sort_key(quality: str) -> int:
|
| 62 |
+
"""Return a sortable number for the quality."""
|
| 63 |
+
try:
|
| 64 |
+
return QUALITY_ORDER.index(quality)
|
| 65 |
+
except ValueError:
|
| 66 |
+
return len(QUALITY_ORDER) # push unknown to the end
|
| 67 |
+
|
| 68 |
+
def filter_and_sort(streams: List[Dict], is_series: bool) -> List[Dict]:
|
| 69 |
+
"""Apply size, quality exclusion, per‑quality limit, and sorting."""
|
| 70 |
+
max_size_gb = MAX_SERIES_SIZE_GB if is_series else MAX_MOVIE_SIZE_GB
|
| 71 |
+
filtered = []
|
| 72 |
+
|
| 73 |
+
for s in streams:
|
| 74 |
+
# Size filter
|
| 75 |
+
size_str = s.get("title", "").split("\n")[0] # typical format: '📦 1.2 GB' at start
|
| 76 |
+
size_match = re.search(r"💾\s*([\d.]+)\s*(GB|MB|TB)", s.get("title", ""), re.IGNORECASE)
|
| 77 |
+
if not size_match:
|
| 78 |
+
# try other common pattern: 'size: X GB'
|
| 79 |
+
size_match = re.search(r"size:\s*([\d.]+)\s*(GB|MB|TB)", s.get("title", ""), re.IGNORECASE)
|
| 80 |
+
size_gb = parse_size_to_gb(size_match.group(0) if size_match else None)
|
| 81 |
+
|
| 82 |
+
if size_gb is not None and size_gb > max_size_gb:
|
| 83 |
+
continue # too big
|
| 84 |
+
|
| 85 |
+
# Quality exclusion
|
| 86 |
+
quality = extract_quality(s.get("title") or s.get("name", ""))
|
| 87 |
+
if any(excl in (s.get("title") + (s.get("name") or "")).lower() for excl in EXCLUDE_KEYWORDS):
|
| 88 |
+
continue # blacklisted
|
| 89 |
+
|
| 90 |
+
s["_quality"] = quality
|
| 91 |
+
filtered.append(s)
|
| 92 |
+
|
| 93 |
+
# Group by quality, keep only LINKS_PER_QUALITY
|
| 94 |
+
groups = {}
|
| 95 |
+
for s in filtered:
|
| 96 |
+
q = s["_quality"]
|
| 97 |
+
groups.setdefault(q, []).append(s)
|
| 98 |
+
limited = []
|
| 99 |
+
for q, streams_list in groups.items():
|
| 100 |
+
limited.extend(streams_list[:LINKS_PER_QUALITY])
|
| 101 |
+
|
| 102 |
+
# Sort by quality (1080p first, etc.)
|
| 103 |
+
limited.sort(key=lambda x: quality_sort_key(x["_quality"]))
|
| 104 |
+
|
| 105 |
+
# Remove the temporary quality field
|
| 106 |
+
for s in limited:
|
| 107 |
+
s.pop("_quality", None)
|
| 108 |
+
return limited
|
| 109 |
+
|
| 110 |
+
@app.get("/manifest.json")
|
| 111 |
+
async def manifest():
|
| 112 |
+
return FileResponse("manifest.json")
|
| 113 |
+
|
| 114 |
+
@app.get("/stream/{type}/{id}.json")
|
| 115 |
+
async def stream(type: str, id: str, request: Request):
|
| 116 |
+
params = dict(request.query_params)
|
| 117 |
+
merged = []
|
| 118 |
+
async with httpx.AsyncClient(timeout=15) as client:
|
| 119 |
+
for base_url in UPSTREAM_URLS:
|
| 120 |
+
url = f"{base_url}/stream/{type}/{id}.json"
|
| 121 |
+
try:
|
| 122 |
+
resp = await client.get(url, params=params)
|
| 123 |
+
if resp.status_code == 200:
|
| 124 |
+
data = resp.json()
|
| 125 |
+
merged.extend(data.get("streams", []))
|
| 126 |
+
except Exception:
|
| 127 |
+
pass # upstream fails silently
|
| 128 |
+
|
| 129 |
+
is_series = (type == "series")
|
| 130 |
+
final_streams = filter_and_sort(merged, is_series)
|
| 131 |
+
return JSONResponse({"streams": final_streams})
|
| 132 |
+
|
| 133 |
+
if __name__ == "__main__":
|
| 134 |
+
import uvicorn
|
| 135 |
+
uvicorn.run(app, host="0.0.0.0", port=int(os.environ.get("PORT", 7860)))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|