256 lines
7.1 KiB
JavaScript
256 lines
7.1 KiB
JavaScript
// AniLiberty module for Sora (AsyncJS)
|
|
// Author: emp0ry
|
|
// Version: 1.0.2
|
|
|
|
const DEFAULT_IMAGE_HOST = "https://aniliberty.top";
|
|
|
|
function originFromApi(urlOrBase) {
|
|
if (!urlOrBase) return DEFAULT_IMAGE_HOST;
|
|
const raw = String(urlOrBase);
|
|
const m = raw.match(/^(https?:\/\/[^/]+)\/api\/v1\/?/i);
|
|
if (m && m[1]) return m[1];
|
|
const m2 = raw.match(/^(https?:\/\/[^/]+)/i);
|
|
return (m2 && m2[1]) ? m2[1] : DEFAULT_IMAGE_HOST;
|
|
}
|
|
|
|
function fullImg(path, host) {
|
|
if (!path) return;
|
|
if (path.startsWith("http")) return path;
|
|
const base = host || DEFAULT_IMAGE_HOST;
|
|
return `${base}${path}`;
|
|
}
|
|
|
|
function pickBestHls(ep) {
|
|
return ep?.hls_1080 || ep?.hls_720 || ep?.hls_480 || null;
|
|
}
|
|
|
|
function _packEpisode(payload) {
|
|
return "aniliberty:" + encodeURIComponent(JSON.stringify(payload || {}));
|
|
}
|
|
|
|
function _unpackEpisode(href) {
|
|
const s = String(href || "");
|
|
if (!s.startsWith("aniliberty:")) return null;
|
|
return _safeJsonParse(decodeURIComponent(s.slice("aniliberty:".length)), null);
|
|
}
|
|
|
|
function _safeJsonParse(s, fallback) {
|
|
try {
|
|
return JSON.parse(s);
|
|
} catch (_) {
|
|
return fallback;
|
|
}
|
|
}
|
|
|
|
// ------------------------------------------------------------
|
|
// Detect working API domain
|
|
// ------------------------------------------------------------
|
|
async function checkApiStatus() {
|
|
const domains = [
|
|
"https://aniliberty.top/api/v1/",
|
|
"https://anilibria.top/api/v1/",
|
|
"https://anilibria.wtf/api/v1/"
|
|
];
|
|
for (const base of domains) {
|
|
try {
|
|
const res = await fetchv2(base + "app/status");
|
|
const data = await res.json();
|
|
if (data?.is_alive || data?.result === "ok") return base;
|
|
} catch (_) {}
|
|
}
|
|
return "https://aniliberty.top/api/v1/";
|
|
}
|
|
|
|
// ------------------------------------------------------------
|
|
// Search -> JSON string
|
|
// ------------------------------------------------------------
|
|
async function searchResults(keyword) {
|
|
try {
|
|
const base = await checkApiStatus();
|
|
const origin = originFromApi(base);
|
|
const url = `${base}app/search/releases?query=${encodeURIComponent(keyword)}&include=id,name.main,poster.src`;
|
|
|
|
const res = await fetchv2(url);
|
|
const data = await res.json();
|
|
|
|
const out = (Array.isArray(data) ? data : []).map(it => ({
|
|
title: it?.name?.main || "Unknown title",
|
|
image: fullImg(it?.poster?.src, origin),
|
|
href:
|
|
`${base}anime/releases/${it.id}?` +
|
|
[
|
|
"include=name.main,poster.src,description,average_duration_of_episode",
|
|
"episodes.ordinal,episodes.name,episodes.duration",
|
|
"episodes.preview.src",
|
|
"episodes.opening.start,episodes.opening.stop",
|
|
"episodes.ending.start,episodes.ending.stop",
|
|
"episodes.hls_1080,episodes.hls_720,episodes.hls_480"
|
|
].join(",")
|
|
}));
|
|
|
|
if (!out.length) {
|
|
return JSON.stringify([]);;
|
|
}
|
|
return JSON.stringify(out);
|
|
} catch (e) {
|
|
return JSON.stringify([]);;
|
|
}
|
|
}
|
|
|
|
// ------------------------------------------------------------
|
|
// Details -> JSON string (Duration: Xm in aliases)
|
|
// ------------------------------------------------------------
|
|
async function extractDetails(url) {
|
|
try {
|
|
const res = await fetchv2(url);
|
|
const data = await res.json();
|
|
|
|
const description = data?.description || "No description available.";
|
|
const mins = Number.isFinite(data?.average_duration_of_episode)
|
|
? `${data.average_duration_of_episode}m`
|
|
: "Unknown";
|
|
|
|
const out = [{
|
|
description,
|
|
aliases: `Duration: ${mins}`,
|
|
airdate: "Unknown"
|
|
}];
|
|
return JSON.stringify(out);
|
|
} catch (e) {
|
|
return JSON.stringify([]);;
|
|
}
|
|
}
|
|
|
|
// ------------------------------------------------------------
|
|
// Episodes -> JSON string (with opening/ending skips)
|
|
// ------------------------------------------------------------
|
|
async function extractEpisodes(url) {
|
|
try {
|
|
const res = await fetchv2(url);
|
|
const data = await res.json();
|
|
|
|
const origin = originFromApi(url);
|
|
|
|
const seriesPoster = fullImg(data?.poster?.src, origin);
|
|
const eps = Array.isArray(data?.episodes) ? data.episodes : [];
|
|
|
|
const out = eps.map((ep, idx) => {
|
|
const url1080 = ep?.hls_1080 || null;
|
|
const url720 = ep?.hls_720 || null;
|
|
const url480 = ep?.hls_480 || null;
|
|
const best = url1080 || url720 || url480;
|
|
if (!best) return null;
|
|
|
|
const num = Number.isFinite(ep?.ordinal)
|
|
? ep.ordinal
|
|
: (Number.isFinite(ep?.sort_order) ? ep.sort_order : (idx + 1));
|
|
|
|
const title = ep?.name ? String(ep.name) : `Episode ${num}`;
|
|
const image = fullImg(ep?.preview?.src, origin) || seriesPoster;
|
|
|
|
// Build skip blocks only if numbers present
|
|
const opening = (ep?.opening && Number.isFinite(ep.opening.start) && Number.isFinite(ep.opening.stop))
|
|
? { start: ep.opening.start, stop: ep.opening.stop }
|
|
: undefined;
|
|
|
|
const ending = (ep?.ending && Number.isFinite(ep.ending.start) && Number.isFinite(ep.ending.stop))
|
|
? { start: ep.ending.start, stop: ep.ending.stop }
|
|
: undefined;
|
|
|
|
const entry = {
|
|
href: _packEpisode({
|
|
url1080,
|
|
url720,
|
|
url480,
|
|
fallback: best
|
|
}),
|
|
number: num,
|
|
title,
|
|
image
|
|
};
|
|
|
|
// Attach only when available
|
|
if (opening) entry.opening = opening;
|
|
if (ending) entry.ending = ending;
|
|
if (Number.isFinite(ep?.duration)) entry.duration = ep.duration; // seconds (optional)
|
|
|
|
return entry;
|
|
}).filter(Boolean);
|
|
|
|
return JSON.stringify(out);
|
|
} catch (e) {
|
|
return JSON.stringify([]);
|
|
}
|
|
}
|
|
|
|
// ------------------------------------------------------------
|
|
// Stream -> RAW URL string
|
|
// ------------------------------------------------------------
|
|
async function extractStreamUrl(url) {
|
|
try {
|
|
const payload = _unpackEpisode(url);
|
|
if (!payload) {
|
|
return url; // backward compatibility for old episode href format
|
|
}
|
|
|
|
const url1080 = (payload.url1080 || "").toString().trim() || null;
|
|
const url720 = (payload.url720 || "").toString().trim() || null;
|
|
const url480 = (payload.url480 || "").toString().trim() || null;
|
|
const fallback = (payload.fallback || "").toString().trim() || null;
|
|
|
|
const streams = [];
|
|
|
|
if (url1080) {
|
|
streams.push({
|
|
title: "1080p",
|
|
streamUrl: url1080,
|
|
url1080,
|
|
url720,
|
|
url480,
|
|
headers: {
|
|
"User-Agent": "Mozilla/5.0",
|
|
"Referer": DEFAULT_IMAGE_HOST + "/"
|
|
}
|
|
});
|
|
}
|
|
|
|
if (url720) {
|
|
streams.push({
|
|
title: "720p",
|
|
streamUrl: url720,
|
|
url1080,
|
|
url720,
|
|
url480,
|
|
headers: {
|
|
"User-Agent": "Mozilla/5.0",
|
|
"Referer": DEFAULT_IMAGE_HOST + "/"
|
|
}
|
|
});
|
|
}
|
|
|
|
if (url480) {
|
|
streams.push({
|
|
title: "480p",
|
|
streamUrl: url480,
|
|
url1080,
|
|
url720,
|
|
url480,
|
|
headers: {
|
|
"User-Agent": "Mozilla/5.0",
|
|
"Referer": DEFAULT_IMAGE_HOST + "/"
|
|
}
|
|
});
|
|
}
|
|
|
|
if (!streams.length) {
|
|
return fallback;
|
|
}
|
|
|
|
return JSON.stringify({
|
|
streams,
|
|
subtitle: "https://none.com"
|
|
});
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
} |