removal
Fetch and Save Remote Content / fetch (push) Successful in 35s
Sync Versions to index.json / sync-versions (push) Failing after 49s

This commit is contained in:
aka paul
2026-05-08 18:24:05 +02:00
parent e324070a32
commit 7cced4311b
14 changed files with 308 additions and 308 deletions
@@ -1,309 +1,309 @@
// AnimeVost for Sora (AsyncJS) // AnimeVost for Sora (AsyncJS)
// Author: emp0ry // Author: emp0ry
// Version: 1.0.2 // Version: 1.0.2
const API_BASE = "https://api.animevost.org/v1/"; const API_BASE = "https://api.animevost.org/v1/";
const FORM_CT = "application/x-www-form-urlencoded; charset=UTF-8"; const FORM_CT = "application/x-www-form-urlencoded; charset=UTF-8";
const SITE_BASE = "https://animevost.org"; const SITE_BASE = "https://animevost.org";
const DEFAULT_SUBTITLE = "https://none.com"; const DEFAULT_SUBTITLE = "https://none.com";
// --- utils --- // --- utils ---
function encodeForm(fields) { function encodeForm(fields) {
return Object.keys(fields) return Object.keys(fields)
.map(k => `${encodeURIComponent(k)}=${encodeURIComponent(fields[k])}`) .map(k => `${encodeURIComponent(k)}=${encodeURIComponent(fields[k])}`)
.join("&"); .join("&");
} }
async function postForm(url, fields) { async function postForm(url, fields) {
const bodyStr = encodeForm(fields); const bodyStr = encodeForm(fields);
try { try {
const resA = await fetchv2(url, { "Content-Type": FORM_CT }, "POST", bodyStr); const resA = await fetchv2(url, { "Content-Type": FORM_CT }, "POST", bodyStr);
if (resA && typeof resA.text === "function") return resA; if (resA && typeof resA.text === "function") return resA;
} catch (_) {} } catch (_) {}
return await fetchv2(url, { return await fetchv2(url, {
method: "POST", method: "POST",
headers: { "Content-Type": FORM_CT }, headers: { "Content-Type": FORM_CT },
body: bodyStr body: bodyStr
}); });
} }
async function parseJsonSafe(res) { async function parseJsonSafe(res) {
const txt = await res.text(); const txt = await res.text();
try { try {
return JSON.parse(txt); return JSON.parse(txt);
} catch (_) { } catch (_) {
return JSON.parse(txt.replace(/^\uFEFF/, "").trim()); return JSON.parse(txt.replace(/^\uFEFF/, "").trim());
} }
} }
function cleanTitle(raw) { function cleanTitle(raw) {
if (!raw || typeof raw !== "string") return "Unknown title"; if (!raw || typeof raw !== "string") return "Unknown title";
let t = raw.split(" /")[0]; let t = raw.split(" /")[0];
return t.replace(/\s*\[.*?\]\s*$/g, "").trim() || "Unknown title"; return t.replace(/\s*\[.*?\]\s*$/g, "").trim() || "Unknown title";
} }
function htmlToText(html) { function htmlToText(html) {
if (!html || typeof html !== "string") return ""; if (!html || typeof html !== "string") return "";
return html return html
.replace(/<br\s*\/?>/gi, "\n") .replace(/<br\s*\/?>/gi, "\n")
.replace(/<\/p>/gi, "\n") .replace(/<\/p>/gi, "\n")
.replace(/<[^>]+>/g, "") .replace(/<[^>]+>/g, "")
.replace(/\n{3,}/g, "\n\n") .replace(/\n{3,}/g, "\n\n")
.trim(); .trim();
} }
function cleanUrl(url) { function cleanUrl(url) {
const s = String(url || "").trim(); const s = String(url || "").trim();
return s || null; return s || null;
} }
function _safeJsonParse(value, fallback) { function _safeJsonParse(value, fallback) {
try { try {
return JSON.parse(value); return JSON.parse(value);
} catch (_) { } catch (_) {
return fallback; return fallback;
} }
} }
// Pack payload into href to avoid relying on global state. // Pack payload into href to avoid relying on global state.
function makeHrefFromPayload(obj) { function makeHrefFromPayload(obj) {
return `animevost://payload/${encodeURIComponent(JSON.stringify(obj || {}))}`; return `animevost://payload/${encodeURIComponent(JSON.stringify(obj || {}))}`;
} }
function readPayloadFromHref(href) { function readPayloadFromHref(href) {
const m = String(href || "").match(/^animevost:\/\/payload\/(.+)$/); const m = String(href || "").match(/^animevost:\/\/payload\/(.+)$/);
if (!m) return null; if (!m) return null;
try { try {
return JSON.parse(decodeURIComponent(m[1])); return JSON.parse(decodeURIComponent(m[1]));
} catch (_) { } catch (_) {
return null; return null;
} }
} }
function parseIdFromAny(hrefOrId) { function parseIdFromAny(hrefOrId) {
const p = readPayloadFromHref(hrefOrId); const p = readPayloadFromHref(hrefOrId);
if (p && p.id) return parseInt(p.id, 10); if (p && p.id) return parseInt(p.id, 10);
const m1 = String(hrefOrId || "").match(/^animevost:\/\/release\/(\d+)$/); const m1 = String(hrefOrId || "").match(/^animevost:\/\/release\/(\d+)$/);
if (m1) return parseInt(m1[1], 10); if (m1) return parseInt(m1[1], 10);
const m2 = String(hrefOrId || "").match(/[?&]id=(\d+)/); const m2 = String(hrefOrId || "").match(/[?&]id=(\d+)/);
if (m2) return parseInt(m2[1], 10); if (m2) return parseInt(m2[1], 10);
if (/^\d+$/.test(String(hrefOrId || ""))) { if (/^\d+$/.test(String(hrefOrId || ""))) {
return parseInt(hrefOrId, 10); return parseInt(hrefOrId, 10);
} }
return null; return null;
} }
// Same packing style as AniLiberty / SameBand. // Same packing style as AniLiberty / SameBand.
function _packEpisode(payload) { function _packEpisode(payload) {
return "animevost:" + encodeURIComponent(JSON.stringify(payload || {})); return "animevost:" + encodeURIComponent(JSON.stringify(payload || {}));
} }
function _unpackEpisode(href) { function _unpackEpisode(href) {
const raw = String(href || ""); const raw = String(href || "");
if (!raw.startsWith("animevost:")) return null; if (!raw.startsWith("animevost:")) return null;
return _safeJsonParse(decodeURIComponent(raw.slice("animevost:".length)), null); return _safeJsonParse(decodeURIComponent(raw.slice("animevost:".length)), null);
} }
function _streamHeaders() { function _streamHeaders() {
return { return {
"User-Agent": "Mozilla/5.0", "User-Agent": "Mozilla/5.0",
"Referer": SITE_BASE + "/" "Referer": SITE_BASE + "/"
}; };
} }
// --- search: POST /search --- // --- search: POST /search ---
async function searchResults(keyword) { async function searchResults(keyword) {
try { try {
let res = await postForm(API_BASE + "search", { name: String(keyword) }); let res = await postForm(API_BASE + "search", { name: String(keyword) });
let json = await parseJsonSafe(res); let json = await parseJsonSafe(res);
if (json?.error || !Array.isArray(json?.data) || json.data.length === 0) { if (json?.error || !Array.isArray(json?.data) || json.data.length === 0) {
res = await postForm(API_BASE + "search", { name: `"${String(keyword)}"` }); res = await postForm(API_BASE + "search", { name: `"${String(keyword)}"` });
json = await parseJsonSafe(res); json = await parseJsonSafe(res);
} }
if (json?.error) { if (json?.error) {
return JSON.stringify([]); return JSON.stringify([]);
} }
const list = Array.isArray(json?.data) ? json.data : []; const list = Array.isArray(json?.data) ? json.data : [];
if (!list.length) { if (!list.length) {
return JSON.stringify([]); return JSON.stringify([]);
} }
const tiles = list.map(item => { const tiles = list.map(item => {
const payload = { const payload = {
id: item.id, id: item.id,
title: cleanTitle(item.title), title: cleanTitle(item.title),
description: htmlToText(item.description || ""), description: htmlToText(item.description || ""),
year: item.year || "", year: item.year || "",
type: item.type || "", type: item.type || "",
image: item.urlImagePreview || "" image: item.urlImagePreview || ""
}; };
return { return {
title: payload.title, title: payload.title,
image: payload.image, image: payload.image,
href: makeHrefFromPayload(payload) href: makeHrefFromPayload(payload)
}; };
}); });
return JSON.stringify(tiles); return JSON.stringify(tiles);
} catch (_) { } catch (_) {
return JSON.stringify([]); return JSON.stringify([]);
} }
} }
// --- details: from payload --- // --- details: from payload ---
async function extractDetails(href) { async function extractDetails(href) {
try { try {
const p = readPayloadFromHref(href); const p = readPayloadFromHref(href);
const out = [{ const out = [{
description: p?.description || "No description available.", description: p?.description || "No description available.",
aliases: `Type: ${p?.type || "Unknown"}`, aliases: `Type: ${p?.type || "Unknown"}`,
airdate: p?.year ? String(p.year) : "Unknown" airdate: p?.year ? String(p.year) : "Unknown"
}]; }];
return JSON.stringify(out); return JSON.stringify(out);
} catch (_) { } catch (_) {
return JSON.stringify([]); return JSON.stringify([]);
} }
} }
// --- episodes: POST /playlist --- // --- episodes: POST /playlist ---
async function extractEpisodes(href) { async function extractEpisodes(href) {
try { try {
const p = readPayloadFromHref(href); const p = readPayloadFromHref(href);
const id = p?.id ?? parseIdFromAny(href); const id = p?.id ?? parseIdFromAny(href);
if (!id) { if (!id) {
return JSON.stringify([]); return JSON.stringify([]);
} }
const res = await postForm(API_BASE + "playlist", { id: String(id) }); const res = await postForm(API_BASE + "playlist", { id: String(id) });
const arr = await parseJsonSafe(res); const arr = await parseJsonSafe(res);
if (!Array.isArray(arr)) { if (!Array.isArray(arr)) {
return JSON.stringify([]); return JSON.stringify([]);
} }
const out = arr.map((ep, idx) => { const out = arr.map((ep, idx) => {
const name = ep?.name || ""; const name = ep?.name || "";
const m = name.match(/(\d+)/); const m = name.match(/(\d+)/);
const num = m ? parseInt(m[1], 10) : (idx + 1); const num = m ? parseInt(m[1], 10) : (idx + 1);
// AnimeVost usually provides only hd/std. // AnimeVost usually provides only hd/std.
// Match Sora picker format used in AniLiberty/SameBand: // Match Sora picker format used in AniLiberty/SameBand:
// hd -> 720p // hd -> 720p
// std -> 480p // std -> 480p
const url1080 = null; const url1080 = null;
const url720 = cleanUrl(ep?.hd); const url720 = cleanUrl(ep?.hd);
const url480 = cleanUrl(ep?.std); const url480 = cleanUrl(ep?.std);
const fallback = url720 || url480; const fallback = url720 || url480;
if (!fallback) return null; if (!fallback) return null;
return { return {
href: _packEpisode({ href: _packEpisode({
url1080, url1080,
url720, url720,
url480, url480,
fallback fallback
}), }),
number: num, number: num,
title: name || `Episode ${num}`, title: name || `Episode ${num}`,
image: ep?.preview || "" image: ep?.preview || ""
}; };
}).filter(Boolean); }).filter(Boolean);
return JSON.stringify(out); return JSON.stringify(out);
} catch (_) { } catch (_) {
return JSON.stringify([]); return JSON.stringify([]);
} }
} }
// --- stream: returns quality picker JSON --- // --- stream: returns quality picker JSON ---
async function extractStreamUrl(href) { async function extractStreamUrl(href) {
try { try {
const payload = _unpackEpisode(href); const payload = _unpackEpisode(href);
// Backward compatibility for old AnimeVost episode href format. // Backward compatibility for old AnimeVost episode href format.
if (!payload) { if (!payload) {
return href; return href;
} }
const url1080 = cleanUrl(payload.url1080); const url1080 = cleanUrl(payload.url1080);
const url720 = cleanUrl(payload.url720); const url720 = cleanUrl(payload.url720);
const url480 = cleanUrl(payload.url480); const url480 = cleanUrl(payload.url480);
const fallback = cleanUrl(payload.fallback); const fallback = cleanUrl(payload.fallback);
const headers = _streamHeaders(); const headers = _streamHeaders();
const streams = []; const streams = [];
if (url1080) { if (url1080) {
streams.push({ streams.push({
title: "1080p", title: "1080p",
streamUrl: url1080, streamUrl: url1080,
url1080, url1080,
url720, url720,
url480, url480,
headers headers
}); });
} }
if (url720) { if (url720) {
streams.push({ streams.push({
title: "720p", title: "720p",
streamUrl: url720, streamUrl: url720,
url1080, url1080,
url720, url720,
url480, url480,
headers headers
}); });
} }
if (url480) { if (url480) {
streams.push({ streams.push({
title: "480p", title: "480p",
streamUrl: url480, streamUrl: url480,
url1080, url1080,
url720, url720,
url480, url480,
headers headers
}); });
} }
if (!streams.length && fallback) { if (!streams.length && fallback) {
streams.push({ streams.push({
title: "720p", title: "720p",
streamUrl: fallback, streamUrl: fallback,
url1080: null, url1080: null,
url720: fallback, url720: fallback,
url480: null, url480: null,
headers headers
}); });
} }
return JSON.stringify({ return JSON.stringify({
streams, streams,
subtitle: DEFAULT_SUBTITLE subtitle: DEFAULT_SUBTITLE
}); });
} catch (_) { } catch (_) {
return JSON.stringify({ return JSON.stringify({
streams: [], streams: [],
subtitle: DEFAULT_SUBTITLE subtitle: DEFAULT_SUBTITLE
}); });
} }
} }