feat(miruro): attempt to retrieve quality from stream playlist

This commit is contained in:
janisslsm committed 2026-07-19 20:23:17 +03:00
1 parent 6e86adb0e6
commit 27a87191c3
7 files changed
+928 -42

No files matched your search

+80 -15
View File
@@ -2,10 +2,11 @@
* Resolve TMDB media to AniList, then retrieve Miruro episode sources.
*/
import { BASE_URL, fetchJson, HEADERS, secureGet } from './http.js';
import { BASE_URL, fetchJson, fetchText, HEADERS, secureGet } from './http.js';
const ANIMAP_URL = 'https://animap.id/api/map';
const ANILIST_URL = 'https://graphql.anilist.co';
const HLS_QUALITY_TIMEOUT_MS = 4000;
// Miruro's visible native-player providers and their preferred subtitle mode.
const PROVIDERS = [
@@ -270,15 +271,71 @@ function getOrigin(url) {
return match ? match[1] : BASE_URL;
}
function getQuality(stream) {
const value = String(stream.quality || stream.server || 'HD');
function getDeclaredQuality(stream) {
const value = `${stream.quality || ''} ${stream.resolution || ''} ${stream.server || ''}`;
const resolution = value.match(/(2160|1440|1080|720|480|360)p?/i);
if (resolution) {
return `${resolution[1]}p`;
}
return /4k/i.test(value) ? '4K' : 'HD';
return /4k/i.test(value) ? '4K' : null;
}
function getHlsQuality(manifest) {
const heights = [];
const pattern = /\bRESOLUTION\s*=\s*\d+\s*x\s*(\d+)\b/gi;
let match;
while ((match = pattern.exec(manifest)) !== null) {
const height = Number(match[1]);
if (height > 0) {
heights.push(height);
}
}
return heights.length > 0 ? `${Math.max(...heights)}p` : null;
}
function isHlsStream(stream) {
const type = String(stream && stream.type || '').toLowerCase();
return type === 'hls' || type === 'm3u8' || type === 'application/x-mpegurl';
}
async function resolveQuality(stream, headers) {
const declaredQuality = getDeclaredQuality(stream);
if (declaredQuality) {
return declaredQuality;
}
if (isHlsStream(stream)) {
let timeoutId;
try {
const manifest = await Promise.race([
fetchText(stream.url, { headers }),
new Promise((resolve) => {
timeoutId = setTimeout(() => resolve(null), HLS_QUALITY_TIMEOUT_MS);
})
]);
const hlsQuality = typeof manifest === 'string' ? getHlsQuality(manifest) : null;
if (hlsQuality) {
return hlsQuality;
}
} catch (_) {
// A stream can still be playable when its manifest rejects this
// metadata request, so retain it with an unknown HD quality.
} finally {
if (timeoutId != null) {
clearTimeout(timeoutId);
}
}
}
return 'HD';
}
function isPlayableStream(stream) {
@@ -291,14 +348,26 @@ function isPlayableStream(stream) {
);
}
function normalizeStreams(sourcePayload, provider, variant) {
async function normalizeStreams(sourcePayload, provider, variant) {
const subtitleCount = Array.isArray(sourcePayload.subtitles) ? sourcePayload.subtitles.length : 0;
const language = variant === 'dub' ? 'DUB' : 'SUB';
return (sourcePayload.streams || []).filter(isPlayableStream).map((stream) => {
const referer = stream.referer || BASE_URL;
const server = stream.server || getQuality(stream);
return Promise.all((sourcePayload.streams || []).filter(isPlayableStream).map(async (stream) => {
const streamHeaders = stream.headers && typeof stream.headers === 'object'
? stream.headers
: {};
const referer = stream.referer || streamHeaders.Referer || streamHeaders.referer || BASE_URL;
const subtitleLabel = subtitleCount > 0 && variant !== 'dub' ? ' · CC' : '';
const headers = {
...streamHeaders,
'Referer': referer,
'Origin': getOrigin(referer),
'User-Agent': HEADERS['User-Agent']
};
const quality = await resolveQuality(stream, headers);
const server = !stream.server || /^(?:HD|auto)$/i.test(stream.server)
? quality
: stream.server;
return {
// Nuvio's source picker prominently displays `name`, so include the
@@ -306,14 +375,10 @@ function normalizeStreams(sourcePayload, provider, variant) {
name: `Miruro · ${provider.label} · ${language}`,
title: `[${language}] ${provider.label} · ${server}${subtitleLabel}`,
url: stream.url,
quality: getQuality(stream),
headers: {
'Referer': referer,
'Origin': getOrigin(referer),
'User-Agent': HEADERS['User-Agent']
}
quality,
headers
};
});
}));
}
async function getProviderStreams(provider, providerPayload, anilistId, episodeNumber) {