樱花动漫

https://www.yinhuadm.xyz

zpccool (13551) 2天前 下载:715

视频
樱花动漫 - 全 HTTP 解析,chapterContent 纯 HTTP 解密
二维码导入(APP尚未完成该功能)
// @name        樱花动漫
// @uuid        yinhuadongman
// @version     2.1.0
// @author      AI
// @url         https://www.yinhuadm.xyz
// @type        video
// @enabled     true
// @description 樱花动漫 - 全 HTTP 解析,chapterContent 纯 HTTP 解密

var BASE = "https://www.yinhuadm.xyz";
var PLAYER_BASE = "https://player.mcue.cc";
var MCUE_SALT = "lemon";

// ─── 工具函数 ───────────────────────────────────────────

function absUrl(href) {
  if (!href) return "";
  if (href.indexOf("http") === 0) return href;
  if (href.charAt(0) === "/") return BASE + href;
  return BASE + "/" + href;
}

async function httpGetRetry(url, headers) {
  var text = "";
  var lastErr = null;
  for (var i = 0; i < 3; i++) {
    try {
      text = await legado.http.get(url, headers);
      lastErr = null;
      break;
    } catch (e) {
      lastErr = e;
    }
  }
  if (lastErr) throw lastErr;
  return text;
}

function cleanTitle(title) {
  title = String(title || "").replace(/\s+/g, " ").trim();
  if (!title || title.length > 80) return "";
  if (/^(更新至第?\d+集|更新至\d+集|已?完结|全\d+集|完结|动漫|HD中字|HD|详情|立即播放|播放)$/.test(title)) return "";
  return title;
}

// 提取章节页 player_aaaa JSON(用括号计数避免截断)
function extractPlayerAaaa(html) {
  var idx = html.indexOf("player_aaaa");
  if (idx === -1) return null;
  var start = html.indexOf("{", idx);
  if (start === -1) return null;
  var depth = 0;
  for (var i = start; i < html.length; i++) {
    if (html[i] === "{") depth++;
    else if (html[i] === "}") {
      depth--;
      if (depth === 0) {
        try {
          return JSON.parse(html.substring(start, i + 1));
        } catch (e) {
          return null;
        }
      }
    }
  }
  return null;
}

function decodeJsString(value) {
  return String(value || "")
    .replace(/\\u([0-9a-fA-F]{4})/g, function (_, hex) {
      return String.fromCharCode(parseInt(hex, 16));
    })
    .replace(/\\\//g, "/")
    .replace(/\\"/g, '"')
    .replace(/\\\\/g, "\\");
}

function extractHtmlAttr(tag, name) {
  var quoted = new RegExp("\\s" + name + "\\s*=\\s*([\"'])(.*?)\\1", "i").exec(tag);
  if (quoted) return quoted[2];

  var plain = new RegExp("\\s" + name + "\\s*=\\s*([^\\s>]+)", "i").exec(tag);
  return plain ? plain[1] : "";
}

function extractNowMetaId(html, matchFn) {
  var metas = html.match(/<meta\b[^>]*>/gi) || [];
  for (var i = 0; i < metas.length; i++) {
    if (!matchFn(metas[i])) continue;
    var id = extractHtmlAttr(metas[i], "id");
    if (id.indexOf("now_") === 0) return id.substring(4);
  }
  return "";
}

function buildMcueSeed(html) {
  var order = extractNowMetaId(html, function (tag) {
    return /^utf-8$/i.test(extractHtmlAttr(tag, "charset"));
  });
  var text = extractNowMetaId(html, function (tag) {
    return /^viewport$/i.test(extractHtmlAttr(tag, "name"));
  });

  if (!order || !text || order.length !== text.length) return "";

  var items = [];
  for (var i = 0; i < order.length; i++) {
    items.push({ id: order.charAt(i), text: text.charAt(i) });
  }
  items.sort(function (a, b) {
    return Number(a.id) - Number(b.id);
  });

  var seed = "";
  for (var j = 0; j < items.length; j++) {
    seed += items[j].text;
  }
  return seed;
}

function extractMcueCipher(html) {
  var m = html.match(/\"url\"\s*:\s*\"([^\"]+)\"/);
  return m ? decodeJsString(m[1]) : "";
}

async function decryptMcuePlayerHtml(html) {
  var cipher = extractMcueCipher(html);
  var seed = buildMcueSeed(html);
  if (!cipher || !seed) {
    legado.log("[chapterContent] player parse failed cipher=" + !!cipher + " seed=" + !!seed);
    return "";
  }

  var md5 = await legado.md5(seed + MCUE_SALT);
  var iv = md5.substring(0, 16);
  var key = md5.substring(16);
  return await legado.aesDecrypt(cipher, key, iv, "CBC");
}

// ─── explore ────────────────────────────────────────────

var CATEGORY_URLS = {
  今日更新: "/label/new.html",
  热榜: "/label/hot.html",
  本周追番: "/label/week.html",
  新片上线: "/s/1.html",
};

async function explore(page, category) {
  legado.log("[explore] page=" + page + " category=" + category);

  if (category === "GETALL") {
    return Object.keys(CATEGORY_URLS);
  }

  var path = CATEGORY_URLS[category] || "/label/new.html";
  // 分页
  if (page > 1) {
    path = path.replace(".html", "/" + page + ".html");
  }
  var url = BASE + path;
  legado.log("[explore] url=" + url);

  var html = await httpGetRetry(url, { Referer: BASE });
  var doc = legado.dom.parse(html);

  var books = [];
  var seen = {};

  if (category === "热榜") {
    var cards = legado.dom.selectAll(doc, ".module-card-item");
    for (var c = 0; c < cards.length; c++) {
      var detailLink = legado.dom.select(cards[c], ".module-card-item-title a") || legado.dom.select(cards[c], 'a[href*="/v/"]');
      if (!detailLink) continue;
      var cardHref = legado.dom.attr(detailLink, "href") || "";
      if (cardHref.indexOf("/v/") === -1) continue;
      var cardUrl = absUrl(cardHref);
      if (seen[cardUrl]) continue;

      var cardTitle = cleanTitle(legado.dom.selectText(cards[c], ".module-card-item-title strong") || legado.dom.text(detailLink));
      if (!cardTitle) {
        var cardImg = legado.dom.select(cards[c], "img");
        cardTitle = cleanTitle(cardImg ? legado.dom.attr(cardImg, "alt") : "");
      }
      if (!cardTitle) continue;

      var cardImgEl = legado.dom.select(cards[c], "img");
      var cardCover = "";
      if (cardImgEl) {
        cardCover = legado.dom.attr(cardImgEl, "data-original") || legado.dom.attr(cardImgEl, "data-src") || legado.dom.attr(cardImgEl, "src") || "";
      }
      var cardNoteEl = legado.dom.select(cards[c], ".module-item-note");
      var cardLatest = cardNoteEl ? legado.dom.text(cardNoteEl).trim() : "";

      seen[cardUrl] = true;
      books.push({
        name: cardTitle,
        bookUrl: cardUrl,
        coverUrl: cardCover,
        latestChapter: cardLatest,
        kind: category,
        tocUrl: cardUrl,
      });
    }
  }

  if (books.length === 0) {
    var links = legado.dom.selectAll(doc, "a.module-poster-item");
    if (links.length === 0) {
      links = legado.dom.selectAll(doc, 'a[href*="/v/"]');
    }

    for (var i = 0; i < links.length; i++) {
      var href = legado.dom.attr(links[i], "href") || "";
      if (href.indexOf("/v/") === -1) continue;
      var fullUrl = absUrl(href);
      if (seen[fullUrl]) continue;

      var title = legado.dom.attr(links[i], "title") || "";
      if (!title) {
        var titleEl = legado.dom.select(links[i], ".module-poster-item-title");
        title = titleEl ? legado.dom.text(titleEl) : legado.dom.text(links[i]);
      }
      title = cleanTitle(title);
      if (!title) {
        var altImg = legado.dom.select(links[i], "img");
        title = cleanTitle(altImg ? legado.dom.attr(altImg, "alt") : "");
      }
      if (!title) continue;

      seen[fullUrl] = true;

      var imgEl = legado.dom.select(links[i], "img");
      var cover = "";
      if (imgEl) {
        cover = legado.dom.attr(imgEl, "data-original") || legado.dom.attr(imgEl, "data-src") || legado.dom.attr(imgEl, "src") || "";
      }

      var noteEl = legado.dom.select(links[i], ".module-item-note");
      var latestChapter = noteEl ? legado.dom.text(noteEl).trim() : "";

      books.push({
        name: title,
        bookUrl: fullUrl,
        coverUrl: cover,
        latestChapter: latestChapter,
        kind: category,
        tocUrl: fullUrl,
      });
    }
  }

  legado.dom.free(doc);
  legado.log("[explore] found=" + books.length);
  return books;
}

// ─── search ─────────────────────────────────────────────

async function search(keyword, page) {
  legado.log("[search] keyword=" + keyword + " page=" + page);

  var url = BASE + "/vch/" + encodeURIComponent(keyword) + ".html";
  if (page > 1) {
    url = BASE + "/vch/" + encodeURIComponent(keyword) + "/page/" + page + ".html";
  }
  legado.log("[search] url=" + url);

  var html = await httpGetRetry(url, { Referer: BASE });
  var doc = legado.dom.parse(html);

  var books = [];
  var seen = {};

  // 搜索结果通常是 .module-poster-item 或包含 /v/ 的链接
  var items = legado.dom.selectAll(doc, "a.module-poster-item");
  if (items.length === 0) {
    // 退化:取全部 /v/ 链接
    items = legado.dom.selectAll(doc, 'a[href*="/v/"]');
  }

  for (var i = 0; i < items.length; i++) {
    var href = legado.dom.attr(items[i], "href") || "";
    if (href.indexOf("/v/") === -1) continue;
    var fullUrl = absUrl(href);
    if (seen[fullUrl]) continue;

    var title = legado.dom.attr(items[i], "title") || "";
    if (!title) {
      var tEl = legado.dom.select(items[i], ".module-poster-item-title");
      title = tEl ? legado.dom.text(tEl) : "";
    }
    if (!title) title = legado.dom.attr(items[i], "title") || "";
    title = title.trim();
    if (!title || title.length > 80) continue;
    if (/^(更新至第?\d+集|已?完结|动漫|HD中字|HD|详情|立即播放)$/.test(title)) continue;

    seen[fullUrl] = true;

    var imgEl = legado.dom.select(items[i], "img");
    var cover = "";
    if (imgEl) {
      cover = legado.dom.attr(imgEl, "data-original") || legado.dom.attr(imgEl, "data-src") || legado.dom.attr(imgEl, "src") || "";
    }

    var noteEl = legado.dom.select(items[i], ".module-item-note");
    var latestChapter = noteEl ? legado.dom.text(noteEl).trim() : "";

    books.push({
      name: title,
      bookUrl: fullUrl,
      coverUrl: cover,
      latestChapter: latestChapter,
      kind: "",
      tocUrl: fullUrl,
    });
  }

  // 如果 .module-poster-item 没有找到,尝试另一种模式(多 href 重复但 title 不同)
  if (books.length === 0) {
    var links = legado.dom.selectAll(doc, 'a[href*="/v/"]');
    for (var j = 0; j < links.length; j++) {
      var lhref = legado.dom.attr(links[j], "href") || "";
      if (lhref.indexOf("/v/") === -1) continue;
      var lfull = absUrl(lhref);

      var ltitle = (legado.dom.attr(links[j], "title") || legado.dom.text(links[j])).trim();
      if (!ltitle || ltitle.length > 80) continue;
      if (/^(更新至第?\d+集|已?完结|动漫|HD中字|HD|详情|立即播放)$/.test(ltitle)) continue;
      if (seen[lfull + "|" + ltitle]) continue;
      seen[lfull + "|" + ltitle] = true;
      if (seen[lfull]) continue; // 已有该 URL 的条目

      seen[lfull] = true;

      var limgEl = legado.dom.select(links[j], "img");
      var lcover = "";
      if (limgEl) {
        lcover = legado.dom.attr(limgEl, "data-original") || legado.dom.attr(limgEl, "data-src") || legado.dom.attr(limgEl, "src") || "";
      }

      books.push({
        name: ltitle,
        bookUrl: lfull,
        coverUrl: lcover,
        latestChapter: "",
        kind: "",
        tocUrl: lfull,
      });
    }
  }

  legado.dom.free(doc);
  legado.log("[search] found=" + books.length);
  return books;
}

// ─── bookInfo ────────────────────────────────────────────

async function bookInfo(bookUrl) {
  legado.log("[bookInfo] url=" + bookUrl);

  var html = await httpGetRetry(bookUrl, { Referer: BASE });
  var doc = legado.dom.parse(html);

  var name = legado.dom.selectText(doc, "h1") || "";
  name = name.trim();

  // 封面:优先 data-original(懒加载真实地址)
  var coverEl = legado.dom.select(doc, ".module-item-cover img") || legado.dom.select(doc, ".module-item-pic img");
  var coverUrl = "";
  if (coverEl) {
    coverUrl = legado.dom.attr(coverEl, "data-original") || legado.dom.attr(coverEl, "data-src") || legado.dom.attr(coverEl, "src") || "";
  }

  // 简介
  var intro = legado.dom.selectText(doc, ".module-info-introduction") || legado.dom.selectText(doc, ".vod_content") || "";
  intro = intro.trim();

  // 最新话数
  var latestChapter = legado.dom.selectText(doc, ".module-item-note") || "";
  latestChapter = latestChapter.trim();

  // 分类 / 标签
  var kindEl = legado.dom.selectAll(doc, ".module-info-tag-link a");
  var kinds = [];
  for (var i = 0; i < kindEl.length; i++) {
    var ktext = legado.dom.text(kindEl[i]).trim();
    if (ktext) kinds.push(ktext);
  }

  legado.dom.free(doc);
  legado.log("[bookInfo] name=" + name + " cover=" + coverUrl.substring(0, 40));

  return {
    name: name,
    author: "",
    bookUrl: bookUrl,
    coverUrl: coverUrl,
    intro: intro,
    latestChapter: latestChapter,
    kind: kinds.join(" "),
    tocUrl: bookUrl,
  };
}

// ─── chapterList ─────────────────────────────────────────

async function chapterList(tocUrl) {
  legado.log("[chapterList] url=" + tocUrl);

  var html = await httpGetRetry(tocUrl, { Referer: BASE });
  var doc = legado.dom.parse(html);

  // 提取所有 /p/ 链接,按 URL pattern 分组
  var allLinks = legado.dom.selectAll(doc, 'a[href*="/p/"]');

  // 解析 /p/{vid}-{line}-{ep}.html
  var episodes = {}; // key: line, value: [{ep, name, url}]
  var seen = {};

  for (var i = 0; i < allLinks.length; i++) {
    var href = legado.dom.attr(allLinks[i], "href") || "";
    var m = href.match(/\/p\/(\d+)-(\d+)-(\d+)\.html/);
    if (!m) continue;
    var fullUrl = absUrl(href);
    if (seen[fullUrl]) continue;

    var epText = legado.dom.text(allLinks[i]).trim();
    // 过滤非集数文字(如"立即播放")
    if (!epText || epText.indexOf("第") === -1) {
      // 尝试从 title 属性
      epText = legado.dom.attr(allLinks[i], "title") || "";
      if (epText.indexOf("第") === -1) {
        // 自动生成集数名
        var epNum = parseInt(m[3]);
        epText = "第" + (epNum < 10 ? "0" + epNum : epNum) + "集";
      }
    }

    seen[fullUrl] = true;

    var lineNum = parseInt(m[2]);
    var epNum2 = parseInt(m[3]);
    if (!episodes[lineNum]) episodes[lineNum] = [];
    episodes[lineNum].push({ ep: epNum2, name: epText, url: fullUrl });
  }

  legado.dom.free(doc);

  // 取第一条线路(最小line号),排序后返回
  var lineNums = Object.keys(episodes).map(function (k) {
    return parseInt(k);
  });
  if (lineNums.length === 0) return [];

  lineNums.sort(function (a, b) {
    return a - b;
  });
  var firstLine = lineNums[0];
  var lineEps = episodes[firstLine];
  lineEps.sort(function (a, b) {
    return a.ep - b.ep;
  });

  var chapters = [];
  for (var k = 0; k < lineEps.length; k++) {
    var groupLabel = "线路" + firstLine;
    // 如果有多条线路,也加入其他线路(作为不同 group)
    chapters.push({
      name: lineEps[k].name,
      url: lineEps[k].url,
      group: groupLabel,
    });
  }

  // 如果有多条线路,补充其他线路章节
  for (var ln = 1; ln < lineNums.length; ln++) {
    var lineN = lineNums[ln];
    var lnEps = episodes[lineN];
    lnEps.sort(function (a, b) {
      return a.ep - b.ep;
    });
    for (var le = 0; le < lnEps.length; le++) {
      chapters.push({
        name: lnEps[le].name,
        url: lnEps[le].url,
        group: "线路" + lineN,
      });
    }
  }

  legado.log("[chapterList] total=" + chapters.length + " lines=" + lineNums.length);
  return chapters;
}

// ─── chapterContent ──────────────────────────────────────
// 流程(纯 HTTP):
//   1) HTTP 获取章节页 → 提取 player_aaaa.url(MCZY token)
//   2) 若 url 已是 http 开头 → 直接返回
//   3) HTTP 获取 player.mcue.cc 播放器页
//   4) 解析 now_* meta 生成 seed,MD5(seed + lemon) 后 AES-CBC 解密 config.url

async function chapterContent(chapterUrl) {
  legado.log("[chapterContent] url=" + chapterUrl);

  // Step 1: HTTP 获取章节页
  var html = await httpGetRetry(chapterUrl, {
    Referer: BASE,
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
  });

  var pd = extractPlayerAaaa(html);
  if (!pd) {
    legado.log("[chapterContent] player_aaaa not found");
    return "";
  }

  var mczyUrl = String(pd.url || "");
  var enc = String(pd.encrypt || "0");
  legado.log("[chapterContent] encrypt=" + enc + " from=" + pd.from);

  if (enc === "1") {
    mczyUrl = decodeURIComponent(mczyUrl);
  } else if (enc === "2") {
    // base64 解码后 unescape
    try {
      mczyUrl = decodeURIComponent(atob(mczyUrl));
    } catch (e) {}
  }

  // 已是直接链接
  if (mczyUrl.indexOf("http") === 0) {
    legado.log("[chapterContent] direct url=" + mczyUrl);
    return mczyUrl;
  }

  if (!mczyUrl) {
    legado.log("[chapterContent] empty url");
    return "";
  }

  legado.log("[chapterContent] MCZY=" + mczyUrl.substring(0, 40) + "...");

  // Step 2: HTTP 访问播放器页并复刻 setting.js 解密逻辑
  var playerPageUrl = PLAYER_BASE + "/yinhua/?url=" + encodeURIComponent(mczyUrl);

  try {
    var playerHtml = await httpGetRetry(playerPageUrl, {
      Referer: BASE,
      "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
    });
    var m3u8 = await decryptMcuePlayerHtml(playerHtml);

    if (m3u8 && String(m3u8).indexOf("http") === 0) {
      legado.log("[chapterContent] m3u8=" + m3u8.substring(0, 60));
      return String(m3u8);
    } else {
      legado.log("[chapterContent] decrypt returned: " + String(m3u8 || "").substring(0, 80));
    }
  } catch (e) {
    legado.log("[chapterContent] http decrypt err=" + e.message);
  }

  return "";
}
广告