removal
This commit is contained in:
@@ -0,0 +1,467 @@
|
||||
const BASE_URL = "https://sameband.studio";
|
||||
const SEARCH_URL = BASE_URL + "/";
|
||||
|
||||
function _ua() {
|
||||
return "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36";
|
||||
}
|
||||
|
||||
function _safeJsonParse(value, fallback) {
|
||||
try {
|
||||
return JSON.parse(value);
|
||||
} catch (_) {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
function _htmlDecode(value) {
|
||||
const s = String(value || "");
|
||||
if (!s) return "";
|
||||
|
||||
const named = {
|
||||
"&": "&",
|
||||
"<": "<",
|
||||
">": ">",
|
||||
""": '"',
|
||||
"'": "'",
|
||||
"'": "'",
|
||||
" ": " ",
|
||||
"«": "<<",
|
||||
"»": ">>"
|
||||
};
|
||||
|
||||
const withNamed = s.replace(/&(amp|lt|gt|quot|#39|apos|nbsp|laquo|raquo);/g, m => named[m] || m);
|
||||
const withDec = withNamed.replace(/&#(\d+);/g, (_, d) => {
|
||||
const code = parseInt(d, 10);
|
||||
return Number.isFinite(code) ? String.fromCharCode(code) : "";
|
||||
});
|
||||
|
||||
return withDec.replace(/&#x([0-9a-f]+);/gi, (_, h) => {
|
||||
const code = parseInt(h, 16);
|
||||
return Number.isFinite(code) ? String.fromCharCode(code) : "";
|
||||
});
|
||||
}
|
||||
|
||||
function _stripTags(value) {
|
||||
return _htmlDecode(String(value || "")
|
||||
.replace(/<br\s*\/?>/gi, "\n")
|
||||
.replace(/<\/p>/gi, "\n")
|
||||
.replace(/<[^>]+>/g, " ")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim());
|
||||
}
|
||||
|
||||
function _absUrl(url, base) {
|
||||
const raw = String(url || "").trim();
|
||||
if (!raw) return "";
|
||||
if (raw.startsWith("http://") || raw.startsWith("https://")) return raw;
|
||||
if (raw.startsWith("//")) return "https:" + raw;
|
||||
|
||||
const root = String(base || BASE_URL).replace(/\/+$/, "");
|
||||
return root + "/" + raw.replace(/^\/+/, "");
|
||||
}
|
||||
|
||||
function _attr(tag, name) {
|
||||
const block = String(tag || "");
|
||||
if (!block) return "";
|
||||
|
||||
const quoted = new RegExp(name + "\\s*=\\s*(['\"])(.*?)\\1", "i").exec(block);
|
||||
if (quoted && quoted[2]) return quoted[2];
|
||||
|
||||
const plain = new RegExp(name + "\\s*=\\s*([^\\s>]+)", "i").exec(block);
|
||||
return plain && plain[1] ? plain[1] : "";
|
||||
}
|
||||
|
||||
function _extractMeta(html, attrName, attrValue) {
|
||||
const src = String(html || "");
|
||||
const re = new RegExp(
|
||||
"<meta[^>]*" + attrName + "=['\"]" + attrValue + "['\"][^>]*content=['\"]([^'\"]+)['\"][^>]*>",
|
||||
"i"
|
||||
);
|
||||
const m = src.match(re);
|
||||
return m && m[1] ? _htmlDecode(m[1]).trim() : "";
|
||||
}
|
||||
|
||||
function _packEpisode(payload) {
|
||||
return "sameband:" + encodeURIComponent(JSON.stringify(payload || {}));
|
||||
}
|
||||
|
||||
function _unpackEpisode(href) {
|
||||
const raw = String(href || "");
|
||||
if (!raw.startsWith("sameband:")) return null;
|
||||
return _safeJsonParse(decodeURIComponent(raw.slice("sameband:".length)), null);
|
||||
}
|
||||
|
||||
function _searchHeaders() {
|
||||
return {
|
||||
"User-Agent": _ua(),
|
||||
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
||||
"Accept-Language": "ru-RU,ru;q=0.9,en;q=0.8",
|
||||
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
|
||||
"Origin": BASE_URL,
|
||||
"Referer": SEARCH_URL
|
||||
};
|
||||
}
|
||||
|
||||
function _htmlHeaders(referer) {
|
||||
return {
|
||||
"User-Agent": _ua(),
|
||||
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
||||
"Accept-Language": "ru-RU,ru;q=0.9,en;q=0.8",
|
||||
"Referer": referer || SEARCH_URL,
|
||||
"Origin": BASE_URL
|
||||
};
|
||||
}
|
||||
|
||||
async function _postSearch(query) {
|
||||
const body =
|
||||
"story=" + encodeURIComponent(String(query || "")) +
|
||||
"&do=search&subaction=search";
|
||||
|
||||
try {
|
||||
const r = await fetchv2(SEARCH_URL, _searchHeaders(), "POST", body);
|
||||
const txt = await r.text();
|
||||
if (txt && txt.includes("class=\"poster\"")) return txt;
|
||||
} catch (_) {}
|
||||
|
||||
const fallback = await fetchv2(BASE_URL + "/index.php?do=search", _searchHeaders(), "POST", body);
|
||||
return fallback.text();
|
||||
}
|
||||
|
||||
function _parseSearchResults(html) {
|
||||
const src = String(html || "");
|
||||
const out = [];
|
||||
const seen = new Set();
|
||||
|
||||
const articleRegex = /<article[^>]*class=["'][^"']*shortstory[^"']*["'][^>]*>[\s\S]*?<\/article>/gi;
|
||||
const blocks = src.match(articleRegex) || [];
|
||||
|
||||
for (const block of blocks) {
|
||||
const posterTag = (block.match(/<div[^>]*class=["'][^"']*poster[^"']*["'][^>]*>/i) || [""])[0];
|
||||
const imageLinkTag = (block.match(/<a[^>]*class=["'][^"']*image[^"']*["'][^>]*>/i) || [""])[0];
|
||||
const infoTitleMatch = block.match(/<div[^>]*class=["'][^"']*info-title[^"']*["'][^>]*>([\s\S]*?)<\/div>/i);
|
||||
const imgTag = (block.match(/<img[^>]*>/i) || [""])[0];
|
||||
|
||||
const href = _absUrl(_attr(imageLinkTag, "href"), BASE_URL);
|
||||
const titleRaw = _attr(posterTag, "title") || (infoTitleMatch ? infoTitleMatch[1] : "");
|
||||
const title = _stripTags(titleRaw);
|
||||
const image = _absUrl(_attr(imgTag, "src") || _attr(imgTag, "data-src"), BASE_URL);
|
||||
|
||||
if (!href || !title || seen.has(href)) continue;
|
||||
seen.add(href);
|
||||
|
||||
out.push({
|
||||
title,
|
||||
image,
|
||||
href
|
||||
});
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
function _extractAnimeUrl(input) {
|
||||
const raw = String(input || "").trim();
|
||||
if (!raw) return "";
|
||||
|
||||
const packed = _unpackEpisode(raw);
|
||||
if (packed && packed.animeUrl) return _absUrl(packed.animeUrl, BASE_URL);
|
||||
|
||||
return _absUrl(raw, BASE_URL);
|
||||
}
|
||||
|
||||
function _bestYearCandidate(html) {
|
||||
const src = String(html || "");
|
||||
const years = [];
|
||||
const regex = /\b(19\d{2}|20\d{2})\b/g;
|
||||
let m;
|
||||
|
||||
while ((m = regex.exec(src)) !== null) {
|
||||
const year = parseInt(m[1], 10);
|
||||
if (year >= 1950 && year <= (new Date().getFullYear() + 1)) {
|
||||
years.push(year);
|
||||
}
|
||||
}
|
||||
|
||||
if (!years.length) return "Unknown";
|
||||
return String(Math.min.apply(null, years));
|
||||
}
|
||||
|
||||
function _extractIframeSrc(html) {
|
||||
const src = String(html || "");
|
||||
|
||||
const inPlayer = src.match(
|
||||
/<div[^>]*class=["'][^"']*player-content[^"']*["'][^>]*>[\s\S]*?<iframe[^>]*src=["']([^"']+)["']/i
|
||||
);
|
||||
if (inPlayer && inPlayer[1]) return inPlayer[1];
|
||||
|
||||
const anyIframe = src.match(/<iframe[^>]*src=["']([^"']+)["']/i);
|
||||
return anyIframe && anyIframe[1] ? anyIframe[1] : "";
|
||||
}
|
||||
|
||||
function _buildListCandidates(iframeSrc) {
|
||||
const iframeAbs = _absUrl(iframeSrc, BASE_URL);
|
||||
const candidates = [];
|
||||
|
||||
const add = (u) => {
|
||||
const v = String(u || "").trim();
|
||||
if (!v) return;
|
||||
if (!candidates.includes(v)) candidates.push(v);
|
||||
};
|
||||
|
||||
const playMatch = iframeAbs.match(/\/v\/play\/([^/?#]+)\.html/i);
|
||||
if (playMatch && playMatch[1]) {
|
||||
const fileName = decodeURIComponent(playMatch[1]);
|
||||
const withUnderscores = `${BASE_URL}/v/list/${fileName}_list.txt`;
|
||||
const withSpaces = `${BASE_URL}/v/list/${fileName.replace(/_/g, " ")}_list.txt`;
|
||||
|
||||
add(withUnderscores);
|
||||
add(encodeURI(withUnderscores));
|
||||
add(withSpaces);
|
||||
add(encodeURI(withSpaces));
|
||||
}
|
||||
|
||||
return candidates;
|
||||
}
|
||||
|
||||
async function _fetchPlaylistArray(candidates, referer) {
|
||||
for (const candidate of candidates) {
|
||||
try {
|
||||
const r = await fetchv2(candidate, _htmlHeaders(referer || SEARCH_URL));
|
||||
const txt = await r.text();
|
||||
const parsed = _safeJsonParse(txt, null);
|
||||
if (Array.isArray(parsed) && parsed.length) {
|
||||
return parsed;
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
function _extractEpisodeTitle(titleHtml, fallbackNum) {
|
||||
const clean = _stripTags(titleHtml || "").replace(/\b\d{1,2}:\d{2}\b/g, "").trim();
|
||||
return clean || `Episode ${fallbackNum}`;
|
||||
}
|
||||
|
||||
function _extractEpisodeNumber(label, fallbackNum) {
|
||||
const m = String(label || "").match(/(\d{1,4})/);
|
||||
if (!m || !m[1]) return fallbackNum;
|
||||
|
||||
const n = parseInt(m[1], 10);
|
||||
return Number.isFinite(n) ? n : fallbackNum;
|
||||
}
|
||||
|
||||
function _parseFileVariants(fileField) {
|
||||
const raw = String(fileField || "").trim();
|
||||
if (!raw) return [];
|
||||
|
||||
const out = [];
|
||||
const parts = raw.split(",");
|
||||
|
||||
for (const part of parts) {
|
||||
const item = String(part || "").trim();
|
||||
if (!item) continue;
|
||||
|
||||
const m = item.match(/^\[([^\]]+)\](.+)$/);
|
||||
const qualityLabel = m && m[1] ? String(m[1]).trim() : "";
|
||||
const path = m && m[2] ? String(m[2]).trim() : item;
|
||||
|
||||
const abs = _absUrl(path, BASE_URL);
|
||||
if (!abs) continue;
|
||||
|
||||
const q = parseInt(qualityLabel.replace(/[^\d]/g, ""), 10) || 0;
|
||||
out.push({
|
||||
quality: q,
|
||||
label: qualityLabel,
|
||||
url: encodeURI(abs)
|
||||
});
|
||||
}
|
||||
|
||||
out.sort((a, b) => b.quality - a.quality);
|
||||
return out;
|
||||
}
|
||||
|
||||
async function searchResults(keyword) {
|
||||
try {
|
||||
const query = String(keyword || "").trim();
|
||||
if (!query) return JSON.stringify([]);
|
||||
|
||||
const html = await _postSearch(query);
|
||||
const results = _parseSearchResults(html);
|
||||
return JSON.stringify(results);
|
||||
} catch (_) {
|
||||
return JSON.stringify([]);
|
||||
}
|
||||
}
|
||||
|
||||
async function extractDetails(url) {
|
||||
try {
|
||||
const animeUrl = _extractAnimeUrl(url);
|
||||
if (!animeUrl) return JSON.stringify([]);
|
||||
|
||||
const r = await fetchv2(animeUrl, _htmlHeaders(SEARCH_URL));
|
||||
const html = await r.text();
|
||||
|
||||
const descMatch = html.match(
|
||||
/<div[^>]*class=["'][^"']*description[^"']*["'][^>]*>[\s\S]*?<div[^>]*class=["'][^"']*limiter[^"']*["'][^>]*>([\s\S]*?)<\/div>/i
|
||||
);
|
||||
|
||||
const description = _stripTags(
|
||||
(descMatch && descMatch[1]) || _extractMeta(html, "name", "description") || "No description available"
|
||||
) || "No description available";
|
||||
|
||||
let aliases = _extractMeta(html, "property", "og:title") || "";
|
||||
aliases = aliases.replace(/\s*(?:>|\u00BB)+\s*SameBand\s*$/i, "").trim();
|
||||
|
||||
return JSON.stringify([
|
||||
{
|
||||
description,
|
||||
aliases: aliases || "SameBand",
|
||||
airdate: _bestYearCandidate(html)
|
||||
}
|
||||
]);
|
||||
} catch (_) {
|
||||
return JSON.stringify([]);
|
||||
}
|
||||
}
|
||||
|
||||
async function extractEpisodes(url) {
|
||||
try {
|
||||
const animeUrl = _extractAnimeUrl(url);
|
||||
if (!animeUrl) return JSON.stringify([]);
|
||||
|
||||
const page = await fetchv2(animeUrl, _htmlHeaders(SEARCH_URL));
|
||||
const html = await page.text();
|
||||
|
||||
const iframeSrc = _extractIframeSrc(html);
|
||||
if (!iframeSrc) return JSON.stringify([]);
|
||||
|
||||
const candidates = _buildListCandidates(iframeSrc);
|
||||
const playlist = await _fetchPlaylistArray(candidates, animeUrl);
|
||||
if (!playlist.length) return JSON.stringify([]);
|
||||
|
||||
const episodes = playlist.map((item, index) => {
|
||||
const titleHtml = String(item?.title || "");
|
||||
const title = _extractEpisodeTitle(titleHtml, index + 1);
|
||||
const number = _extractEpisodeNumber(title, index + 1);
|
||||
|
||||
const imgTag = (titleHtml.match(/<img[^>]*>/i) || [""])[0];
|
||||
const image = _absUrl(_attr(imgTag, "src"), BASE_URL);
|
||||
|
||||
const payload = {
|
||||
animeUrl,
|
||||
file: String(item?.file || ""),
|
||||
thumbnails: String(item?.thumbnails || ""),
|
||||
title,
|
||||
image
|
||||
};
|
||||
|
||||
const out = {
|
||||
href: _packEpisode(payload),
|
||||
number,
|
||||
title
|
||||
};
|
||||
|
||||
if (image) out.image = image;
|
||||
return out;
|
||||
});
|
||||
|
||||
episodes.sort((a, b) => Number(a.number) - Number(b.number));
|
||||
return JSON.stringify(episodes);
|
||||
} catch (_) {
|
||||
return JSON.stringify([]);
|
||||
}
|
||||
}
|
||||
|
||||
async function extractStreamUrl(href) {
|
||||
try {
|
||||
const payload = _unpackEpisode(href);
|
||||
const fileField = payload?.file ? String(payload.file) : "";
|
||||
if (!fileField) {
|
||||
return JSON.stringify({ streams: [], subtitle: "https://none.com" });
|
||||
}
|
||||
|
||||
const variants = _parseFileVariants(fileField);
|
||||
if (!variants.length) {
|
||||
return JSON.stringify({ streams: [], subtitle: "https://none.com" });
|
||||
}
|
||||
|
||||
const byQuality = {
|
||||
1080: "",
|
||||
720: "",
|
||||
480: ""
|
||||
};
|
||||
|
||||
for (const v of variants) {
|
||||
if ((v.quality === 1080 || v.quality === 720 || v.quality === 480) && !byQuality[v.quality]) {
|
||||
byQuality[v.quality] = v.url;
|
||||
}
|
||||
}
|
||||
|
||||
const url1080 = byQuality[1080] || null;
|
||||
const url720 = byQuality[720] || null;
|
||||
const url480 = byQuality[480] || null;
|
||||
|
||||
const headers = {
|
||||
"User-Agent": _ua(),
|
||||
"Referer": payload?.animeUrl ? String(payload.animeUrl) : (BASE_URL + "/"),
|
||||
"Origin": BASE_URL
|
||||
};
|
||||
|
||||
const streams = [];
|
||||
|
||||
if (url1080) {
|
||||
streams.push({
|
||||
title: "1080p",
|
||||
streamUrl: url1080,
|
||||
url1080,
|
||||
url720,
|
||||
url480,
|
||||
headers
|
||||
});
|
||||
}
|
||||
|
||||
if (url720) {
|
||||
streams.push({
|
||||
title: "720p",
|
||||
streamUrl: url720,
|
||||
url1080,
|
||||
url720,
|
||||
url480,
|
||||
headers
|
||||
});
|
||||
}
|
||||
|
||||
if (url480) {
|
||||
streams.push({
|
||||
title: "480p",
|
||||
streamUrl: url480,
|
||||
url1080,
|
||||
url720,
|
||||
url480,
|
||||
headers
|
||||
});
|
||||
}
|
||||
|
||||
if (!streams.length) {
|
||||
const first = variants[0];
|
||||
if (first?.url) {
|
||||
streams.push({
|
||||
title: "1080p",
|
||||
streamUrl: first.url,
|
||||
url1080: first.url,
|
||||
url720: null,
|
||||
url480: null,
|
||||
headers
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return JSON.stringify({
|
||||
streams,
|
||||
subtitle: "https://none.com"
|
||||
});
|
||||
} catch (_) {
|
||||
return JSON.stringify({ streams: [], subtitle: "https://none.com" });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"sourceName": "SameBand",
|
||||
"iconUrl": "https://sameband.studio/templates/sameband/dleimages/favicon/touch-icon-ipad.png",
|
||||
"author": {
|
||||
"name": "emp0ry",
|
||||
"icon": "https://avatars.githubusercontent.com/u/64217088"
|
||||
},
|
||||
"version": "1.0.0",
|
||||
"language": "Russian",
|
||||
"streamType": "HLS",
|
||||
"quality": "1080p",
|
||||
"baseUrl": "https://sameband.studio/",
|
||||
"searchBaseUrl": "https://sameband.studio/",
|
||||
"scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/sameband/sameband.js",
|
||||
"asyncJS": true,
|
||||
"streamAsyncJS": true,
|
||||
"softsub": false,
|
||||
"type": "anime",
|
||||
"downloadSupport": false,
|
||||
"supportsMojuru": true,
|
||||
"supportsDartotsu": true,
|
||||
"supportsSora": true,
|
||||
"supportsLuna": true
|
||||
}
|
||||
Reference in New Issue
Block a user