Премьера:<\/span>\s*([\s\S]*?)<\/div>/i
);
const added = html.match(
/]*class=["'][^"']*movie__added[^"']*["'][^>]*>[\s\S]*?Добавлено<\/span>[\s\S]*?([\s\S]*?)<\/span>/i
);
return _stripTags((premiere && premiere[1]) || (added && added[1]) || "Unknown");
}
function _episodeKey(season, episode) {
return `${season || 1}:${episode || 0}`;
}
function _episodeTitle(season, episode) {
if (season && season > 1) return `S${season}E${episode}`;
return `Episode ${episode}`;
}
function _voiceName(item) {
const studio = String(item?.voiceStudio || "").trim();
const type = String(item?.voiceType || "").trim();
if (studio && type) return `${studio} · ${type}`;
if (studio) return studio;
if (type) return type;
return "Unknown voiceover";
}
function _sourceUrl(sources, key) {
return _cleanUrl(sources?.[key] || "");
}
function _buildQualityUrls(videoJson) {
const sources = videoJson?.sources || {};
const url2160 = _sourceUrl(sources, "mpeg4kUrl");
const url1440 = _sourceUrl(sources, "mpeg2kUrl") || _sourceUrl(sources, "mpegQhdUrl");
const url1080 = _sourceUrl(sources, "mpegFullHdUrl");
const url720 = _sourceUrl(sources, "mpegHighUrl");
const url480 = _sourceUrl(sources, "mpegMediumUrl");
const url360 = _sourceUrl(sources, "mpegLowUrl");
const url240 = _sourceUrl(sources, "mpegLowestUrl");
const url144 = _sourceUrl(sources, "mpegTinyUrl");
return {
url2160,
url1440,
url1080,
url720,
url480,
url360,
url240,
url144
};
}
function _pickBestMp4(qualityUrls) {
const ordered = [
{ quality: 2160, url: qualityUrls.url2160 },
{ quality: 1440, url: qualityUrls.url1440 },
{ quality: 1080, url: qualityUrls.url1080 },
{ quality: 720, url: qualityUrls.url720 },
{ quality: 480, url: qualityUrls.url480 },
{ quality: 360, url: qualityUrls.url360 },
{ quality: 240, url: qualityUrls.url240 },
{ quality: 144, url: qualityUrls.url144 }
];
for (const item of ordered) {
if (item.url) return item;
}
return null;
}
function _hasAnyQuality(qualityUrls) {
return !!(
qualityUrls.url2160 ||
qualityUrls.url1440 ||
qualityUrls.url1080 ||
qualityUrls.url720 ||
qualityUrls.url480 ||
qualityUrls.url360 ||
qualityUrls.url240 ||
qualityUrls.url144
);
}
function _buildVoiceoverStream(voiceName, videoJson) {
const qualityUrls = _buildQualityUrls(videoJson);
if (!_hasAnyQuality(qualityUrls)) {
return null;
}
const best = _pickBestMp4(qualityUrls);
if (!best?.url) return null;
const headers = _streamHeaders();
return {
title: voiceName || "Voiceover",
streamUrl: best.url,
// Main Sora quality-picker fields.
url1080: qualityUrls.url1080,
url720: qualityUrls.url720,
url480: qualityUrls.url480,
// Extra qualities. Harmless if Sora ignores them.
url2160: qualityUrls.url2160,
url1440: qualityUrls.url1440,
url360: qualityUrls.url360,
url240: qualityUrls.url240,
url144: qualityUrls.url144,
headers
};
}
// ------------------------------------------------------------
// Search
// ------------------------------------------------------------
async function searchResults(keyword) {
try {
const query = String(keyword || "").trim();
if (!query) return JSON.stringify([]);
const html = await _postSearch(query);
const results = _parseSearchResults(html);
for (const item of results) {
item._score = _scoreTitle(item.title, query);
}
results.sort((a, b) => a._score - b._score);
return JSON.stringify(results.map(({ _score, ...rest }) => rest));
} catch (_) {
return JSON.stringify([]);
}
}
// ------------------------------------------------------------
// Details
// ------------------------------------------------------------
async function extractDetails(href) {
try {
const animeUrl = _extractAnimeUrl(href);
if (!animeUrl) return JSON.stringify([]);
const html = await _getAnimePage(animeUrl);
const titleId = _extractTitleId(html);
const pub = _extractPublisherId(html);
const aggr = _extractAggregator(html);
return JSON.stringify([{
description: _detailsDescription(html),
aliases: _detailsAliases(html, titleId, pub, aggr) || "AniDub",
airdate: _airdate(html)
}]);
} catch (_) {
return JSON.stringify([]);
}
}
// ------------------------------------------------------------
// Episodes
// Unique episodes only.
// Store all voiceovers in href payload.
// ------------------------------------------------------------
async function extractEpisodes(href) {
try {
const animeUrl = _extractAnimeUrl(href);
if (!animeUrl) return JSON.stringify([]);
const html = await _getAnimePage(animeUrl);
const titleId = _extractTitleId(html);
const pub = _extractPublisherId(html);
const aggr = _extractAggregator(html);
if (!titleId) return JSON.stringify([]);
const playlist = await _getPlaylist(titleId, pub, aggr, animeUrl);
const items = Array.isArray(playlist?.items) ? playlist.items : [];
const byEpisode = new Map();
for (const item of items) {
const vkId = String(item?.vkId || "").trim();
if (!vkId) continue;
const season = Number.isFinite(item?.season) ? item.season : 1;
const episode = Number.isFinite(item?.episode) ? item.episode : 0;
if (!episode) continue;
const key = _episodeKey(season, episode);
if (!byEpisode.has(key)) {
byEpisode.set(key, {
season,
episode,
options: []
});
}
const ep = byEpisode.get(key);
ep.options.push({
vkId,
cvhId: String(item?.cvhId || ""),
voiceStudio: String(item?.voiceStudio || "").trim(),
voiceType: String(item?.voiceType || "").trim(),
voiceName: _voiceName(item),
name: String(item?.name || "").trim(),
season,
episode
});
}
const out = Array.from(byEpisode.values())
.sort((a, b) => {
if (a.season !== b.season) return a.season - b.season;
return a.episode - b.episode;
})
.map(ep => {
ep.options.sort((a, b) => _voiceRank(a.voiceName) - _voiceRank(b.voiceName));
return {
href: _packEpisode({
animeUrl,
titleId,
pub,
aggr,
season: ep.season,
episode: ep.episode,
options: ep.options
}),
number: ep.episode,
title: _episodeTitle(ep.season, ep.episode)
};
});
return JSON.stringify(out);
} catch (_) {
return JSON.stringify([]);
}
}
// ------------------------------------------------------------
// Stream
// Correct voiceover + quality picker:
// - one stream per voiceover
// - each stream carries url1080/url720/url480/etc.
// - HLS skipped.
// ------------------------------------------------------------
async function extractStreamUrl(href) {
try {
const payload = _unpackEpisode(href);
const options = Array.isArray(payload?.options) ? payload.options : [];
if (!options.length) {
return JSON.stringify({
streams: [],
subtitle: DEFAULT_SUBTITLE
});
}
options.sort((a, b) => _voiceRank(a.voiceName) - _voiceRank(b.voiceName));
const streams = [];
const seen = new Set();
for (const opt of options) {
const vkId = String(opt?.vkId || "").trim();
if (!vkId || seen.has(vkId)) continue;
seen.add(vkId);
try {
const videoJson = await _getVideo(vkId, payload?.animeUrl || BASE_URL + "/");
const stream = _buildVoiceoverStream(
String(opt?.voiceName || "Voiceover").trim(),
videoJson
);
if (stream) streams.push(stream);
} catch (_) {}
}
return JSON.stringify({
streams,
subtitle: DEFAULT_SUBTITLE
});
} catch (_) {
return JSON.stringify({
streams: [],
subtitle: DEFAULT_SUBTITLE
});
}
}
function _defaultExport() {
return {
searchResults,
extractDetails,
extractEpisodes,
extractStreamUrl
};
}
try {
globalThis.default = _defaultExport;
} catch (_) {}
try {
this.default = _defaultExport;
} catch (_) {}
try {
globalThis.module = globalThis.module || {};
globalThis.module.exports = { default: _defaultExport };
} catch (_) {}