diff --git a/animevost/animevost.js b/animevost/animevost.js new file mode 100644 index 0000000..dcb96d9 --- /dev/null +++ b/animevost/animevost.js @@ -0,0 +1,161 @@ +// AnimeVost for Sora (AsyncJS) +// Author: emp0ry +// Version: 1.0.0 + +const API_BASE = "https://api.animevost.org/v1/"; +const FORM_CT = "application/x-www-form-urlencoded; charset=UTF-8"; + +// --- utils --- +function encodeForm(fields) { + return Object.keys(fields) + .map(k => `${encodeURIComponent(k)}=${encodeURIComponent(fields[k])}`) + .join("&"); +} + +async function postForm(url, fields) { + const bodyStr = encodeForm(fields); + try { + const resA = await fetchv2(url, { "Content-Type": FORM_CT }, "POST", bodyStr); + if (resA && typeof resA.text === "function") return resA; + } catch (_) {} + return await fetchv2(url, { method: "POST", headers: { "Content-Type": FORM_CT }, body: bodyStr }); +} + +async function parseJsonSafe(res) { + const txt = await res.text(); + try { return JSON.parse(txt); } + catch { return JSON.parse(txt.replace(/^\uFEFF/, "").trim()); } +} + +function cleanTitle(raw) { + if (!raw || typeof raw !== "string") return "Unknown title"; + let t = raw.split(" /")[0]; + return t.replace(/\s*\[.*?\]\s*$/g, "").trim() || "Unknown title"; +} + +function htmlToText(html) { + if (!html || typeof html !== "string") return ""; + return html + .replace(//gi, "\n") + .replace(/<\/p>/gi, "\n") + .replace(/<[^>]+>/g, "") + .replace(/\n{3,}/g, "\n\n") + .trim(); +} + +// Pack payload into href to avoid relying on global state. +function makeHrefFromPayload(obj) { + return `animevost://payload/${encodeURIComponent(JSON.stringify(obj))}`; +} +function readPayloadFromHref(href) { + const m = String(href).match(/^animevost:\/\/payload\/(.+)$/); + if (!m) return null; + try { return JSON.parse(decodeURIComponent(m[1])); } catch { return null; } +} +function parseIdFromAny(hrefOrId) { + const p = readPayloadFromHref(hrefOrId); + if (p && p.id) return parseInt(p.id, 10); + const m1 = String(hrefOrId).match(/^animevost:\/\/release\/(\d+)$/); + if (m1) return parseInt(m1[1], 10); + const m2 = String(hrefOrId).match(/[?&]id=(\d+)/); + if (m2) return parseInt(m2[1], 10); + if (/^\d+$/.test(String(hrefOrId))) return parseInt(hrefOrId, 10); + return null; +} + +// --- search (POST /search) --- +async function searchResults(keyword) { + try { + let res = await postForm(API_BASE + "search", { name: String(keyword) }); + let json = await parseJsonSafe(res); + + if (json?.error || !Array.isArray(json?.data) || json.data.length === 0) { + res = await postForm(API_BASE + "search", { name: `"${String(keyword)}"` }); + json = await parseJsonSafe(res); + } + + if (json?.error) { + return JSON.stringify([]); + } + + const list = Array.isArray(json?.data) ? json.data : []; + if (!list.length) { + return JSON.stringify([]); + } + + const tiles = list.map(item => { + const payload = { + id: item.id, + title: cleanTitle(item.title), + description: htmlToText(item.description || ""), + year: item.year || "", + type: item.type || "", + image: item.urlImagePreview || "" + }; + return { + title: payload.title, + image: payload.image, + href: makeHrefFromPayload(payload) + }; + }); + + return JSON.stringify(tiles); + } catch (e) { + return JSON.stringify([]); + } +} + +// --- details (from payload) --- +async function extractDetails(href) { + try { + const p = readPayloadFromHref(href); + const out = [{ + description: p?.description || "No description available.", + aliases: `Type: ${p?.type || "Unknown"}`, + airdate: p?.year ? String(p.year) : "Unknown" + }]; + return JSON.stringify(out); + } catch (e) { + return JSON.stringify([]); + } +} + +// --- episodes (POST /playlist) --- +async function extractEpisodes(href) { + try { + const p = readPayloadFromHref(href); + const id = p?.id ?? parseIdFromAny(href); + if (!id) { + return JSON.stringify([]); + } + + const res = await postForm(API_BASE + "playlist", { id: String(id) }); + const arr = await parseJsonSafe(res); + + if (!Array.isArray(arr)) return JSON.stringify([]); + + const out = arr.map((ep, idx) => { + const name = ep?.name || ""; + const m = name.match(/(\d+)/); + const num = m ? parseInt(m[1], 10) : (idx + 1); + const link = ep?.hd || ep?.std || null; + if (!link) return null; + + return { + number: num, + title: name || `Episode ${num}`, + image: ep?.preview || "", + href: link + }; + }).filter(Boolean); + + return JSON.stringify(out); + } catch (e) { + return JSON.stringify([]); + } +} + +// --- stream --- +async function extractStreamUrl(url) { + try { return url; } catch { return null; } +} diff --git a/animevost/animevost.json b/animevost/animevost.json new file mode 100644 index 0000000..2f7cdf3 --- /dev/null +++ b/animevost/animevost.json @@ -0,0 +1,20 @@ +{ + "sourceName": "AnimeVost", + "iconUrl": "https://animevost.org/apple-touch-icon.png", + "author": { + "name": "emp0ry", + "icon": "https://avatars.githubusercontent.com/u/64217088" + }, + "version": "1.0.0", + "language": "Russian", + "streamType": "MP4", + "quality": "720p", + "baseUrl": "https://api.animevost.org/v1/", + "searchBaseUrl": "https://api.animevost.org/v1/search?name=%s", + "scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/animevost/animevost.js", + "asyncJS": true, + "streamAsyncJS": false, + "softsub": false, + "type": "anime", + "downloadSupport": true +} \ No newline at end of file