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(//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( "]*" + 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 = /]*class=["'][^"']*shortstory[^"']*["'][^>]*>[\s\S]*?<\/article>/gi; const blocks = src.match(articleRegex) || []; for (const block of blocks) { const posterTag = (block.match(/]*class=["'][^"']*poster[^"']*["'][^>]*>/i) || [""])[0]; const imageLinkTag = (block.match(/]*class=["'][^"']*image[^"']*["'][^>]*>/i) || [""])[0]; const infoTitleMatch = block.match(/]*class=["'][^"']*info-title[^"']*["'][^>]*>([\s\S]*?)<\/div>/i); const imgTag = (block.match(/]*>/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( /]*class=["'][^"']*player-content[^"']*["'][^>]*>[\s\S]*?]*src=["']([^"']+)["']/i ); if (inPlayer && inPlayer[1]) return inPlayer[1]; const anyIframe = src.match(/]*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( /]*class=["'][^"']*description[^"']*["'][^>]*>[\s\S]*?]*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(/]*>/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" }); } }