diff --git a/aniliberty/aniliberty.js b/aniliberty/aniliberty.js index 99bd9c9..62b190e 100644 --- a/aniliberty/aniliberty.js +++ b/aniliberty/aniliberty.js @@ -1,14 +1,18 @@ -// AniLiberty module for Sora +// AniLiberty module for Sora (AsyncJS) // Author: emp0ry -// Version: 1.0.0 -// Description: -// Streams anime directly from AniLiberty / AniLibria API v1 -// Endpoints used: -// • /app/status -// • /app/search/releases?query= -// • /anime/releases/{id} -// -// Supports 1080p / 720p / 480p HLS playback +// Version: 1.0.3 +// Changes: add skip markers (opening/ending) and average_duration_of_episode -> "Duration: Xm". + +const IMAGE_HOST = "https://anilibria.top"; + +function fullImg(path) { + if (!path) return `${IMAGE_HOST}/favicon.ico`; + return path.startsWith("http") ? path : `${IMAGE_HOST}${path}`; +} + +function pickBestHls(ep) { + return ep?.hls_1080 || ep?.hls_720 || ep?.hls_480 || null; +} // ------------------------------------------------------------ // Detect working API domain @@ -19,7 +23,6 @@ async function checkApiStatus() { "https://aniliberty.top/api/v1/", "https://anilibria.wtf/api/v1/" ]; - for (const base of domains) { try { const res = await fetchv2(base + "app/status"); @@ -31,102 +34,136 @@ async function checkApiStatus() { } // ------------------------------------------------------------ -// Search anime releases +// Search -> JSON string // ------------------------------------------------------------ async function searchResults(keyword) { - const base = await checkApiStatus(); - const url = `${base}app/search/releases?query=${encodeURIComponent(keyword)}&include=id,name,poster,year,description`; - try { - const response = await fetchv2(url); - const data = await response.json(); - if (!Array.isArray(data) || !data.length) - return [{ - title: `No results from AniLiberty (${base})`, - image: "https://anilibria.top/favicon.ico", - href: "https://anilibria.top" - }]; + const base = await checkApiStatus(); + const url = `${base}app/search/releases?query=${encodeURIComponent(keyword)}&include=id,name.main,poster.src`; - return data.map(item => { - const title = item.name?.main || item.name?.english || "Unknown title"; - const year = item.year ? ` (${item.year})` : ""; - const img = item.poster?.optimized?.preview - ? `https://anilibria.top${item.poster.optimized.preview}` - : `https://anilibria.top${item.poster?.preview || "/favicon.ico"}`; - return { - title: title + year, - image: img, - href: `${base}anime/releases/${item.id}` - }; - }); - } catch (err) { - console.log("AniLiberty search error:", err); - return [{ - title: "AniLiberty: Error during search", - image: "https://anilibria.top/favicon.ico", - href: "https://anilibria.top" - }]; + 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), + // Pass a details endpoint as href (will be reused by details/episodes) + 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) { + out.push({ + title: "No results from AniLiberty", + image: `${IMAGE_HOST}/favicon.ico`, + href: IMAGE_HOST + }); + } + return JSON.stringify(out); + } catch (e) { + console.log("AniLiberty search error:", e); + return JSON.stringify([{ title: "Error", image: `${IMAGE_HOST}/favicon.ico`, href: IMAGE_HOST }]); } } // ------------------------------------------------------------ -// Extract anime details +// Details -> JSON string (Duration: Xm in aliases) // ------------------------------------------------------------ async function extractDetails(url) { try { - const response = await fetchv2(url); - const data = await response.json(); - return [{ - description: data.description || "No description available.", - aliases: data.name?.english || "N/A", - airdate: data.year ? String(data.year) : "Unknown" + 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" }]; - } catch (err) { - console.log("AniLiberty details error:", err); - return [{ + return JSON.stringify(out); + } catch (e) { + console.log("AniLiberty details error:", e); + return JSON.stringify([{ description: "Error loading details", - aliases: "N/A", - airdate: "N/A" - }]; + aliases: "Duration: Unknown", + airdate: "Unknown" + }]); } } // ------------------------------------------------------------ -// Extract episode list (with HLS sources) +// Episodes -> JSON string (with opening/ending skips) // ------------------------------------------------------------ async function extractEpisodes(url) { try { - const response = await fetchv2(url); - const data = await response.json(); - const eps = data.episodes || []; - if (!eps.length) return []; + const res = await fetchv2(url); + const data = await res.json(); - return eps.map(ep => ({ - number: ep.ordinal || ep.sort_order || 0, - image: ep.preview?.optimized?.preview - ? `https://anilibria.top${ep.preview.optimized.preview}` - : "https://anilibria.top/favicon.ico", - href: - ep.hls_1080 || - ep.hls_720 || - ep.hls_480 || - null - })).filter(e => e.href); - } catch (err) { - console.log("AniLiberty episodes error:", err); - return []; + const seriesPoster = fullImg(data?.poster?.src); + const eps = Array.isArray(data?.episodes) ? data.episodes : []; + + const out = eps.map((ep, idx) => { + const href = pickBestHls(ep); + if (!href) 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) || 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, + 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) { + console.log("AniLiberty episodes error:", e); + return JSON.stringify([]); } } // ------------------------------------------------------------ -// Extract direct stream URL (HLS) +// Stream -> RAW URL string // ------------------------------------------------------------ async function extractStreamUrl(url) { try { - // Direct HLS links are already provided; just return the same URL - return url; - } catch (err) { - console.log("AniLiberty stream error:", err); + return url; // direct HLS + } catch (e) { + console.log("AniLiberty stream error:", e); return null; } }