]*\bsection-content\b/i);
const section = sectionIndex >= 0 ? text.slice(sectionIndex, sectionIndex + 1600) : text;
const encrypted = attrFrom(section, /]*\btype=(["'])hidden\1)[^>]*\bvalue=(["'])(.*?)\2[^>]*>/i, 3) ||
attrFrom(section, /]*\bvalue=(["'])(.*?)\1[^>]*>/i, 2);
const mainTag = tagById(text, "main");
const listTag = tagByClass(text, "play-list");
const videoId = attrFrom(mainTag, /\bdata-id=(["'])(.*?)\1/i, 2) || mainVideoId();
const listId = attrFrom(listTag, /\bdata-id=(["'])(.*?)\1/i, 2);
if (!encrypted || !listId || !videoId) {
log("episode parts missing", pageUrl, !!encrypted, listId, videoId);
return null;
}
return {
encrypted: htmlDecode(encrypted),
listId: htmlDecode(listId),
videoId: htmlDecode(videoId)
};
}
function attrFrom(text, pattern, group) {
const match = String(text || "").match(pattern);
return cleanText(match && match[group || 1]);
}
function tagById(html, id) {
const pattern = new RegExp("<[^>]+\\bid=[\"']" + escapeRegex(id) + "[\"'][^>]*>", "i");
const match = String(html || "").match(pattern);
return match ? match[0] : "";
}
function tagByClass(html, className) {
const pattern = new RegExp("<[^>]+\\bclass=[\"'][^\"']*\\b" + escapeRegex(className) + "\\b[^\"']*[\"'][^>]*>", "i");
const match = String(html || "").match(pattern);
return match ? match[0] : "";
}
function htmlDecode(value) {
const text = String(value || "");
if (text.indexOf("&") < 0) return text;
const box = document.createElement("textarea");
box.innerHTML = text;
return box.value;
}
function escapeRegex(value) {
return String(value || "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function decryptEpisodeParts(parts) {
if (!parts || !parts.encrypted || !parts.listId || !parts.videoId) return Promise.resolve("");
const requestId = "fm_ymvid_media_" + Date.now() + "_" + Math.random().toString(36).slice(2);
return new Promise((resolve) => {
const timer = setTimeout(() => {
if (state.mediaResolvers[requestId]) {
delete state.mediaResolvers[requestId];
resolve("");
}
}, 5000);
state.mediaResolvers[requestId] = function (url) {
clearTimeout(timer);
resolve(url || "");
};
try {
window.dispatchEvent(new CustomEvent(CONFIG.mediaRequestEvent, {
detail: {
requestId: requestId,
encrypted: parts.encrypted,
listId: parts.listId,
videoId: parts.videoId
}
}));
} catch (e) {
clearTimeout(timer);
delete state.mediaResolvers[requestId];
resolve("");
}
});
}
function mainVideoId() {
const main = document.getElementById("main");
return cleanText(main && main.getAttribute("data-id"));
}
function resourceTitle() {
const title = document.querySelector("article .media-info h1,.media-info h1,h1");
return cleanText(title && title.textContent) || cleanText(document.title.replace(/\s*\|\s*粤漫之家\s*$/i, ""));
}
function posterUrl() {
const image = document.querySelector("article .media-thumb img,.media-thumb img,.grid-content img");
return image ? absoluteUrl(image.getAttribute("src") || image.src) : "";
}
function updatePanelBusy(busy) {
const panel = document.getElementById(CONFIG.panelId);
if (!panel) return;
const button = panel.querySelector("." + CONFIG.primaryClass);
if (button) button.textContent = busy ? "处理中" : "App播放";
}
function updatePanelStatus(message) {
const panel = document.getElementById(CONFIG.panelId);
if (!panel) return;
const status = panel.querySelector("." + CONFIG.statusClass);
if (status && message) status.textContent = message;
}
function waitForMediaUrl(timeout, options) {
const deadline = Date.now() + timeout;
return new Promise((resolve) => {
const tick = function () {
requestPageMediaUrl();
const mediaUrl = currentMediaUrl(options);
if (mediaUrl || Date.now() >= deadline) {
resolve(mediaUrl);
return;
}
setTimeout(tick, 120);
};
tick();
});
}
function currentMediaUrl(options) {
const allowCached = !options || options.allowCached !== false;
const computed = computeMediaUrl();
if (computed) {
state.lastMediaUrl = computed;
return computed;
}
if (allowCached && state.lastMediaUrl) return state.lastMediaUrl;
const video = document.querySelector("#player video,video");
if (video && video.currentSrc) return playableMediaUrl(video.currentSrc);
if (video && video.src) return playableMediaUrl(video.src);
return "";
}
function resetMediaCacheIfEpisodeChanged() {
const key = [normalizePath(location.href), encryptedPlayerValue(), playListId(), mainVideoId()].join("|");
if (!key || key === state.lastEpisodeKey) return;
state.lastEpisodeKey = key;
state.lastMediaUrl = "";
}
function computeMediaUrl() {
const encrypted = encryptedPlayerValue();
const listId = playListId();
const main = document.getElementById("main");
const videoId = main && main.getAttribute("data-id");
if (!encrypted || !listId || !videoId || typeof window.decryptByAES !== "function") return "";
let decrypted = "";
try {
decrypted = window.decryptByAES(encrypted);
} catch (e) {
log("decrypt failed", e && e.message || e);
}
if (!decrypted) return "";
const marker = "?t=";
const index = decrypted.indexOf(marker);
if (index < 0) return "";
const base = decrypted.slice(0, index);
const token = decrypted.slice(index + marker.length);
const listParts = String(listId || "").split("-");
const seriesId = listParts[0] || "";
const route = listParts[1] || "0";
const url = route === "0" || !seriesId ? base + "?t=" + token + "&vId=" + videoId : base + "/" + seriesId + "?t=" + token + "&vId=" + videoId;
return playableMediaUrl(url);
}
function encryptedPlayerValue() {
const input = document.querySelector(".section-content > input[type='hidden'],.section-content > input");
return input ? cleanText(input.value) : "";
}
function playListId() {
const list = document.querySelector(".play-list");
return list ? cleanText(list.getAttribute("data-id")) : "";
}
function renderNativeEpisodes() {
const episodes = collectEpisodes();
if (!episodes.length) {
removeNativeEpisodes();
return;
}
let container = document.getElementById(CONFIG.episodeId);
if (!container) {
container = document.createElement("section");
container.id = CONFIG.episodeId;
const panel = document.getElementById(CONFIG.panelId);
const playerSection = document.querySelector(".player-section") || document.getElementById("player");
if (panel && panel.parentNode) panel.parentNode.insertBefore(container, panel.nextSibling);
else if (playerSection) playerSection.appendChild(container);
else return;
}
const signature = episodes.map((item) => [item.href, item.label, item.active ? "1" : "0"].join("#")).join("|");
if (container.dataset.fmYmvidSignature !== signature) {
container.dataset.fmYmvidSignature = signature;
const current = episodes.find((item) => item.active);
container.innerHTML = [
'',
'剧集',
'' + escapeHtml(current ? "当前 " + current.label : "") + '',
'
',
''
].join("");
}
document.body.classList.add("fm-ymvid-has-native-episodes");
const links = container.querySelectorAll("a[href]");
for (let i = 0; i < links.length; i++) {
bindEpisodeLink(links[i]);
}
const active = container.querySelector(".fm-ymvid-active");
if (active && active.scrollIntoView && !active.dataset.fmYmvidSeen) {
active.dataset.fmYmvidSeen = "1";
setTimeout(() => active.scrollIntoView({ block: "nearest", inline: "center" }), 180);
}
}
function removeNativeEpisodes() {
const container = document.getElementById(CONFIG.episodeId);
if (container && container.parentNode) container.parentNode.removeChild(container);
if (document.body) document.body.classList.remove("fm-ymvid-has-native-episodes");
}
function collectEpisodes() {
const links = document.querySelectorAll(".play-list .item a[href]");
const seen = {};
const episodes = [];
for (let i = 0; i < links.length; i++) {
const link = links[i];
const href = absoluteUrl(link.getAttribute("href"));
if (!href || seen[href]) continue;
seen[href] = true;
const em = link.querySelector("em");
const rawLabel = cleanText(em && em.textContent || link.textContent);
const label = rawLabel || String(i + 1).padStart(2, "0");
const item = link.closest && link.closest(".item");
const active = !!(item && item.classList && item.classList.contains("active")) || normalizePath(href) === normalizePath(location.href);
episodes.push({ href: href, label: label, active: active });
}
return episodes;
}
function normalizePath(url) {
try {
const parsed = new URL(url, location.href);
return parsed.pathname.replace(/\/+$/, "");
} catch (e) {
return String(url || "").replace(/\/+$/, "");
}
}
function escapeHtml(value) {
return String(value || "").replace(/[&<>"']/g, function (char) {
return ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[char];
});
}
function escapeAttr(value) {
return escapeHtml(value).replace(/`/g, "`");
}
function enhanceEpisodeList() {
const links = document.querySelectorAll(".play-list .item a[href]");
for (let i = 0; i < links.length; i++) {
bindEpisodeLink(links[i]);
}
const active = document.querySelector(".play-list .item.active a");
if (active && active.scrollIntoView && !active.dataset.fmYmvidSeen) {
active.dataset.fmYmvidSeen = "1";
setTimeout(() => active.scrollIntoView({ block: "nearest", inline: "center" }), 180);
}
}
function bindEpisodeLink(link) {
if (!link || link.dataset.fmYmvidEpisode === "1") return;
link.dataset.fmYmvidEpisode = "1";
link.setAttribute("tabindex", "0");
link.addEventListener("click", function (event) {
handleEpisodeClick(link, event);
});
link.addEventListener("keydown", function (event) {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
handleEpisodeClick(link, event);
}
});
}
function handleEpisodeClick(link, event) {
if (!canNativePlay()) return false;
if (event && (event.defaultPrevented || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || event.button > 0)) return false;
const episode = episodeFromLink(link);
if (!episode) return false;
if (event) {
event.preventDefault();
event.stopPropagation();
}
nativePlay(episode);
return true;
}
function episodeFromLink(link) {
const href = absoluteUrl(link && link.getAttribute("href"));
if (!href) return null;
const path = normalizePath(href);
const fromList = collectEpisodes().find((item) => normalizePath(item.href) === path);
if (fromList) return fromList;
return {
href: href,
label: cleanText(link && (link.querySelector("em") && link.querySelector("em").textContent || link.textContent))
};
}
function canNativePlay() {
return !!(window.fm || window.fongmiBridge || window.fongmiClient);
}
function enhanceCards() {
const cards = document.querySelectorAll(".grid-content,.feature-post-box,.swiper-slide,.item-row,.aside-body li");
for (let i = 0; i < cards.length; i++) enhanceCard(cards[i]);
const direct = document.querySelectorAll(CONFIG.focusSelector);
for (let i = 0; i < direct.length; i++) {
if (direct[i].dataset.fmYmvidFocus === "1") continue;
direct[i].dataset.fmYmvidFocus = "1";
direct[i].addEventListener("focus", focusCurrent, true);
}
}
function enhanceCard(card) {
if (!card || card.dataset.fmYmvidCard === "1") return;
const link = card.querySelector("a[href]");
if (!link) return;
card.dataset.fmYmvidCard = "1";
card.setAttribute("tabindex", "0");
card.setAttribute("role", "link");
card.addEventListener("click", function (event) {
if (event.target && event.target.closest && event.target.closest("a,button,input,textarea,select")) return;
link.click();
});
card.addEventListener("keydown", function (event) {
if (state.inputEditing) return;
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
link.click();
}
});
card.addEventListener("focus", focusCurrent, true);
card.addEventListener("blur", function () {
card.classList.remove(CONFIG.focusClass);
}, true);
}
function focusCurrent(event) {
if (!isTv()) return;
const target = event.currentTarget || event.target;
if (!target || !target.classList) return;
clearTimeout(state.focusRaf);
target.classList.add(CONFIG.focusClass);
state.focusRaf = setTimeout(() => {
try {
target.scrollIntoView({ block: "nearest", inline: "nearest" });
} catch (e) {
// ignore
}
}, 40);
}
function handleFocusIn(event) {
const tag = event.target && event.target.tagName;
state.inputEditing = /^(INPUT|TEXTAREA|SELECT)$/i.test(tag || "");
}
function handleFocusOut(event) {
const target = event.target;
if (target && target.classList) target.classList.remove(CONFIG.focusClass);
const tag = target && target.tagName;
if (/^(INPUT|TEXTAREA|SELECT)$/i.test(tag || "")) state.inputEditing = false;
}
function toast(message) {
try {
if (window.fm && fm.ext && fm.ext.toast) return fm.ext.toast(message);
} catch (e) {
// ignore
}
return Promise.resolve();
}
})();