金牌影院

https://vv3nwjk.com

zpccool (13551) 2天前 下载:615

视频
优化清晰度选择,优化登录令牌获取;崩溃优化
二维码导入(APP尚未完成该功能)
// @name 金牌影院
// @uuid jinpaiyinyuan
// @url https://vv3nwjk.com
// @type video
// @version 1.3.5
// @author AI
// @enabled true
// @description 优化清晰度选择,优化登录令牌获取;崩溃优化

var BASE = 'https://vv3nwjk.com';
var WWW_BASE = 'https://www.vv3nwjk.com';
var API_BASE = BASE + '/api';
var SIGN_KEY = 'cb808529bae6b6be45ecfab29a4889bc';
var SCOPE = 'jpyy_vv3nwjk';
var UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0';
var AUTH_KEYS = ['authorization', 'token', 'accessToken', 'loginToken', 'userToken'];
var CATEGORIES = {
  '首页': '/',
  '电影': '/vod/show/id/1',
  '电视剧': '/vod/show/id/2',
  '综艺': '/vod/show/id/3',
  '动漫': '/vod/show/id/4'
};

var PAGE_HEADERS = {
  'User-Agent': UA,
  'Accept': 'application/json, text/plain, */*',
  'Accept-Language': 'zh-CN,zh;q=0.9',
  'Referer': BASE + '/',
  'Origin': BASE,
  'client-type': '1'
};

var PLAY_HEADERS = {
  'User-Agent': UA,
  'Accept': '*/*',
  'Accept-Language': 'zh-CN,zh;q=0.9,zh-TW;q=0.8,zh-HK;q=0.7,en-US;q=0.6,en;q=0.5',
  'Accept-Encoding': 'gzip, deflate, br, zstd',
  'Referer': BASE + '/',
  'Origin': BASE,
  'Sec-Fetch-Dest': 'empty',
  'Sec-Fetch-Mode': 'cors',
  'Sec-Fetch-Site': 'cross-site'
};

function init() {
  legado.log('[init] 金牌影院_vv ready');
}

function log(msg) {
  try {
    legado.log(msg);
  } catch (e) {}
}

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

function trim(s) {
  return String(s || '').replace(/^\s+|\s+$/g, '');
}

function stripHtml(s) {
  return trim(String(s || '')
    .replace(/<script[\s\S]*?<\/script>/gi, '')
    .replace(/<style[\s\S]*?<\/style>/gi, '')
    .replace(/<[^>]+>/g, ' ')
    .replace(/&nbsp;/g, ' ')
    .replace(/&amp;/g, '&')
    .replace(/&quot;/g, '"')
    .replace(/&#39;/g, "'")
    .replace(/\s+/g, ' '));
}

function decodeJsonString(s) {
  s = String(s || '');
  try {
    return JSON.parse('"' + s.replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '"');
  } catch (e) {
    return s
      .replace(/\\u([0-9a-fA-F]{4})/g, function(all, hex) { return String.fromCharCode(parseInt(hex, 16)); })
      .replace(/\\r\\n|\\n|\\r/g, ' ')
      .replace(/\\"/g, '"')
      .replace(/\\\\/g, '\\');
  }
}

function normalizeNextData(html) {
  return String(html || '')
    .replace(/\\u0026/g, '&')
    .replace(/\\u003c/g, '<')
    .replace(/\\u003e/g, '>')
    .replace(/\\"/g, '"')
    .replace(/\\r\\n|\\n|\\r/g, ' ');
}

function extractField(block, field) {
  var re = new RegExp('"' + field + '"\\s*:\\s*(?:"((?:\\\\.|[^"\\\\])*)"|([^,}\\]]+))');
  var m = re.exec(block);
  if (!m) return '';
  var v = m[1] !== undefined ? decodeJsonString(m[1]) : trim(m[2]);
  if (v === 'null' || v === 'undefined') return '';
  return v;
}

function absoluteUrl(url) {
  url = trim(url);
  if (!url) return '';
  if (/^https?:\/\//i.test(url)) return url;
  if (url.indexOf('//') === 0) return 'https:' + url;
  if (url.charAt(0) === '/') return BASE + url;
  return BASE + '/' + url;
}

function absoluteByBase(url, baseUrl) {
  url = trim(url);
  if (!url || url.charAt(0) === '#') return url;
  if (/^(https?:)?\/\//i.test(url)) return url.indexOf('//') === 0 ? 'https:' + url : url;
  if (/^(data|blob):/i.test(url)) return url;
  try {
    return new URL(url, baseUrl).toString();
  } catch (e) {
    var base = baseUrl.split('?')[0];
    base = base.substring(0, base.lastIndexOf('/') + 1);
    while (url.indexOf('../') === 0) {
      url = url.substring(3);
      base = base.replace(/[^\/]+\/$/, '');
    }
    if (url.indexOf('./') === 0) url = url.substring(2);
    return base + url;
  }
}

function getVodId(url) {
  var m = String(url || '').match(/\/detail\/(\d+)/);
  if (m) return m[1];
  m = String(url || '').match(/\/vod\/play\/(\d+)\/\d+\/\d+/);
  if (m) return m[1];
  m = String(url || '').match(/(?:^|[?&])id=(\d+)/);
  return m ? m[1] : '';
}

function getNid(url) {
  var s = String(url || '');
  var parts = s.split('||');
  if (parts.length > 1 && /^\d+$/.test(parts[1])) return parts[1];
  var m = s.match(/\/vod\/play\/\d+\/\d+\/(\d+)/);
  return m ? m[1] : '';
}

function getDeviceId() {
  var id = '';
  try {
    id = legado.config.read(SCOPE, 'deviceId');
  } catch (e) {}
  if (!id) {
    id = 'web-' + Date.now() + '-' + Math.random().toString(16).slice(2);
    try {
      legado.config.write(SCOPE, 'deviceId', id);
    } catch (e2) {}
  }
  return id;
}

function readConfig(key) {
  var value = '';
  try {
    value = legado.config.read(SCOPE, key);
  } catch (e) {}
  if (!value) {
    try {
      value = legado.config.read(key);
    } catch (e2) {}
  }
  return trim(value);
}

function writeConfig(key, value) {
  try {
    legado.config.write(SCOPE, key, value);
  } catch (e) {}
  try {
    legado.config.write(key, value);
  } catch (e2) {}
}

function getAuthorizationHeader() {
  for (var i = 0; i < AUTH_KEYS.length; i++) {
    var value = readConfig(AUTH_KEYS[i]);
    if (value) return value;
  }
  return '';
}

function saveAuthorization(value) {
  value = trim(value);
  writeConfig('authorization', value);
  if (value) writeConfig('token', value);
  return value ? '登录令牌已保存' : '登录令牌已清空';
}

function clearAuthorization() {
  for (var i = 0; i < AUTH_KEYS.length; i++) writeConfig(AUTH_KEYS[i], '');
  return '登录令牌已清空';
}

function escapeHtml(s) {
  return String(s || '')
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#39;');
}

function settingsPage() {
  var auth = getAuthorizationHeader();
  var savedText = auth ? '已保存' : '未保存';
  return {
    type: 'html',
    html: '<!doctype html><html><head><meta charset="utf-8"><style>' +
      'body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;margin:18px;color:#202124;background:#fff}' +
      '.row{display:flex;gap:8px;margin-top:10px}.box{width:100%;box-sizing:border-box;padding:10px;border:1px solid #d0d7de;border-radius:6px}' +
      'button{padding:9px 12px;border:1px solid #d0d7de;border-radius:6px;background:#f6f8fa}.primary{background:#0969da;color:#fff;border-color:#0969da}' +
      '#msg{margin-top:10px;color:#57606a;font-size:13px;word-break:break-all}</style></head><body>' +
      '<div>登录令牌: <b id="state">' + savedText + '</b></div>' +
      '<textarea id="auth" class="box" rows="5" placeholder="粘贴 authorization / token">' + escapeHtml(auth) + '</textarea>' +
      '<div class="row"><button class="primary" onclick="saveAuth()">保存令牌</button><button onclick="clearAuth()">清空令牌</button><button onclick="openLogin()">打开登录</button><button onclick="syncToken()">同步令牌</button></div>' +
      '<div id="msg"></div><script>' +
      'function msg(t){document.getElementById("msg").textContent=t||""}' +
      'async function saveAuth(){try{var v=document.getElementById("auth").value;msg(await legado.callSource("saveAuthorization",v));document.getElementById("state").textContent=v?"已保存":"未保存"}catch(e){msg(e.message||String(e))}}' +
      'async function clearAuth(){try{msg(await legado.callSource("clearAuthorization"));document.getElementById("auth").value="";document.getElementById("state").textContent="未保存"}catch(e){msg(e.message||String(e))}}' +
      'async function openLogin(){try{msg(await legado.callSource("openLogin"))}catch(e){msg(e.message||String(e))}}' +
      'async function syncToken(){try{var r=await legado.callSource("syncLoginToken");msg(r);location.reload()}catch(e){msg(e.message||String(e))}}' +
      '</script></body></html>'
  };
}

async function openLogin() {
  var id = legado.browser.acquire('jpyy-login', { visible: true, userAgent: UA, timeoutSecs: 120 });
  legado.browser.navigate(id, BASE + '/', { waitUntil: 'load', timeoutSecs: 120 });
  return '已打开登录窗口,请在网页内完成登录';
}

function tokenExtractScript() {
  return 'return (function(){function pick(o){if(!o)return "";var keys=["authorization","token","accessToken","loginToken","userToken"];for(var i=0;i<keys.length;i++){if(o[keys[i]])return o[keys[i]];}if(o.state){var v=pick(o.state);if(v)return v;}if(o.user){var u=pick(o.user);if(u)return u;}if(o.userInfo){var ui=pick(o.userInfo);if(ui)return ui;}return ""}function parse(v){try{return pick(JSON.parse(v))}catch(e){return ""}}var keys=["authorization","token","accessToken","loginToken","userToken"];for(var i=0;i<keys.length;i++){var v=localStorage.getItem(keys[i])||sessionStorage.getItem(keys[i]);if(v)return v;}var us=parse(localStorage.getItem("userStore")||sessionStorage.getItem("userStore")||"");if(us)return us;for(var j=0;j<localStorage.length;j++){var k=localStorage.key(j);var vv=localStorage.getItem(k);if(/token|authorization/i.test(k)&&vv)return vv;var pv=parse(vv);if(pv)return pv;}return "";})()';
}

async function syncLoginToken() {
  var id = legado.browser.acquire('jpyy-login', { visible: true, userAgent: UA, timeoutSecs: 120 });
  var script = tokenExtractScript();
  var token = '';
  try {
    token = trim(legado.browser.eval(id, script));
  } catch (e) {}
  if (!token) {
    legado.browser.navigate(id, BASE + '/', { waitUntil: 'load', timeoutSecs: 60 });
    token = trim(legado.browser.eval(id, script));
  }
  if (!token) return '未找到登录令牌:请先在打开的网页中登录,再点击同步令牌';
  saveAuthorization(token);
  return '登录令牌已同步';
}

function sortedQuery(params) {
  var keys = [];
  for (var k in params) {
    if (params.hasOwnProperty(k) && params[k] !== undefined && params[k] !== null && params[k] !== '') keys.push(k);
  }
  keys.sort();
  var arr = [];
  for (var i = 0; i < keys.length; i++) arr.push(keys[i] + '=' + encodeURIComponent(String(params[keys[i]])));
  return arr.join('&');
}

async function apiGet(path, params, referer) {
  params = params || {};
  var query = sortedQuery(params);
  var t = String(Date.now());
  var md5 = await legado.md5(query + '&key=' + SIGN_KEY + '&t=' + t);
  var sign = await legado.sha1(md5);
  var headers = {};
  for (var k in PAGE_HEADERS) headers[k] = PAGE_HEADERS[k];
  headers.Referer = referer || BASE + '/';
  headers.t = t;
  headers.sign = sign;
  headers.authorization = getAuthorizationHeader();
  headers.deviceId = getDeviceId();
  var apiUrl = API_BASE + path + (query ? '?' + query : '');
  var text = '';
  var lastErr = null;
  for (var attempt = 0; attempt < 3; attempt++) {
    try {
      text = await httpGetRetry(apiUrl, headers);
      lastErr = null;
      break;
    } catch (e) {
      lastErr = e;
    }
  }
  if (lastErr) throw lastErr;
  var json = JSON.parse(text);
  if (json.code && json.code !== 200) throw new Error('api error ' + json.code + ': ' + (json.msg || ''));
  return json.data !== undefined ? json.data : json;
}

async function getVideoDetail(vodId, referer) {
  var data = await apiGet('/mw-movie/anonymous/video/detail', { id: vodId }, referer || (BASE + '/detail/' + vodId));
  return data && data.data ? data.data : (data || {});
}

function normalizePlayInfo(data) {
  return data && data.data ? data.data : (data || {});
}

function getPlayList(info) {
  return (info && (info.list || info.urls)) || [];
}

function isAuthKeyPlayUrl(url) {
  return /[?&]auth_key=/i.test(String(url || ''));
}

function isIpBoundPlayUrl(url) {
  url = String(url || '');
  return /[?&]whip=/i.test(url) || /[?&]sign=/i.test(url) || /\/\/ppvod01\.kqgfbs\.com\//i.test(url);
}

function hasAuthKeyPlayUrl(list) {
  for (var i = 0; i < list.length; i++) {
    if (list[i] && isAuthKeyPlayUrl(list[i].url)) return true;
  }
  return false;
}

function hasIpBoundPlayUrl(list) {
  for (var i = 0; i < list.length; i++) {
    if (list[i] && isIpBoundPlayUrl(list[i].url)) return true;
  }
  return false;
}

async function getEpisodePlayInfo(vodId, nid) {
  var data = await apiGet('/mw-movie/anonymous/v2/video/episode/url', {
    clientType: 1,
    id: vodId,
    nid: nid
  }, BASE + '/vod/play/' + vodId + '/1/' + nid);
  return normalizePlayInfo(data);
}

function makeBookItem(v) {
  var vodId = String(v.vodId || v.id || v.videoId || '');
  var bookUrl = BASE + '/detail/' + vodId;
  return {
    name: v.vodName || v.name || v.title || ('影片' + vodId),
    author: v.vodDirector || v.director || v.vodActor || v.actor || '',
    bookUrl: bookUrl,
    tocUrl: bookUrl,
    coverUrl: absoluteUrl(v.vodPic || v.cover || v.pic || v.coverUrl || ''),
    intro: stripHtml(v.vodContent || v.vodBlurb || v.content || v.desc || ''),
    latestChapter: v.vodVersion || v.latestChapter || v.remarks || '',
    latestChapterUrl: bookUrl,
    updateTime: v.vodTime || v.updateTime || '',
    status: v.vodVersion || v.status || '',
    kind: v.typeName || v.vodClass || ''
  };
}

function parseVodItemsFromNextData(html) {
  var text = normalizeNextData(html);
  var items = [];
  var seen = {};
  var re = /"vodId"\s*:\s*(\d+)/g;
  var m;
  while ((m = re.exec(text)) !== null) {
    var vodId = m[1];
    if (seen[vodId]) continue;
    var next = text.indexOf('"vodId"', re.lastIndex);
    var block = text.slice(m.index, next > m.index ? next : m.index + 5000);
    var name = extractField(block, 'vodName');
    if (!name) continue;
    seen[vodId] = true;
    items.push(makeBookItem({
      vodId: vodId,
      vodName: name,
      vodDirector: extractField(block, 'vodDirector'),
      vodActor: extractField(block, 'vodActor'),
      vodPic: extractField(block, 'vodPic'),
      vodContent: extractField(block, 'vodContent') || extractField(block, 'vodBlurb'),
      vodBlurb: extractField(block, 'vodBlurb'),
      vodRemarks: extractField(block, 'vodRemarks'),
      vodVersion: extractField(block, 'vodVersion'),
      vodPubdate: extractField(block, 'vodPubdate'),
      vodClass: extractField(block, 'vodClass'),
      typeName: extractField(block, 'typeName')
    }));
  }
  return items;
}

function parseListFromHtml(html) {
  var items = [];
  var seen = {};
  var re = /href=["']\/detail\/(\d+)["'][\s\S]{0,1200}?(?:alt=["']([^"']+)["']|title=["']([^"']+)["']|class=["'][^"']*name[^"']*["'][^>]*>([^<]+)<|<h[1-6][^>]*>([^<]+)<|<span[^>]*>([^<]+)<)/gi;
  var m;
  while ((m = re.exec(html)) !== null) {
    var id = m[1];
    if (seen[id]) continue;
    var name = stripHtml(m[2] || m[3] || m[4] || m[5] || m[6] || ('影片' + id));
    if (!name || name.length > 80) name = '影片' + id;
    seen[id] = true;
    items.push({ name: name, bookUrl: BASE + '/detail/' + id, tocUrl: BASE + '/detail/' + id, author: '', coverUrl: '', intro: '', latestChapter: '', kind: '' });
  }
  return items;
}

async function search(keyword, page) {
  page = page || 1;
  keyword = keyword || '';
  var url = BASE + '/vod/search/' + encodeURIComponent(keyword) + (page > 1 ? '?page=' + page : '');
  var html = await httpGetRetry(url, PAGE_HEADERS);
  var items = parseVodItemsFromNextData(html);
  return items.length ? items : parseListFromHtml(html);
}

async function explore(page, category) {
  if (page === 'GETALL' || category === 'GETALL' || page === undefined) {
    return ['首页', '电影', '电视剧', '综艺', '动漫', '\u8bbe\u7f6e'];
  }
  if (typeof page === 'string' && !/^\d+$/.test(page) && category === undefined) {
    category = page;
    page = 1;
  }
  page = page || 1;
  category = category || '首页';
  if (category === '\u8bbe\u7f6e') return settingsPage();
  var path = CATEGORIES[category] || CATEGORIES['首页'];
  var url = BASE + path + (page > 1 ? (path === '/' ? '?page=' + page : '/page/' + page) : '');
  var html = await httpGetRetry(url, PAGE_HEADERS);
  var items = parseVodItemsFromNextData(html);
  return items.length ? items : parseListFromHtml(html);
}

async function bookInfo(bookUrl) {
  var vodId = getVodId(bookUrl);
  if (!vodId) throw new Error('missing vodId: ' + bookUrl);
  return makeBookItem(await getVideoDetail(vodId, BASE + '/detail/' + vodId));
}

async function chapterList(tocUrl) {
  var vodId = getVodId(tocUrl);
  if (!vodId) throw new Error('missing vodId: ' + tocUrl);
  var detail = await getVideoDetail(vodId, BASE + '/detail/' + vodId);
  var list = detail.episodeList || detail.vodEpisodeList || detail.episodes || [];
  list.sort(function(a, b) { return Number(a.sort || a.episodeSort || 0) - Number(b.sort || b.episodeSort || 0); });
  var chapters = [];
  for (var i = 0; i < list.length; i++) {
    var ep = list[i] || {};
    var nid = ep.nid || ep.id || ep.episodeId;
    if (!nid) continue;
    chapters.push({ name: ep.name || ep.title || detail.vodVersion || ('第' + (i + 1) + '集'), url: BASE + '/detail/' + vodId + '||' + nid, group: detail.vodVersion || '默认' });
  }
  return chapters;
}

function looksLikeM3u8(text) {
  return trim(text).indexOf('#EXTM3U') === 0;
}

function absolutizeM3u8(text, m3u8Url) {
  text = String(text || '').replace(/\r\n/g, '\n').replace(/\r/g, '\n');
  var lines = text.split('\n');
  for (var i = 0; i < lines.length; i++) {
    var line = trim(lines[i]);
    if (!line) lines[i] = line;
    else if (line.charAt(0) === '#') lines[i] = line.replace(/URI="([^"]+)"/g, function(all, uri) { return 'URI="' + absoluteByBase(uri, m3u8Url) + '"'; });
    else lines[i] = absoluteByBase(line, m3u8Url);
  }
  return lines.join('\n');
}

async function fetchM3u8Text(m3u8Url) {
  var text = await httpGetRetry(m3u8Url, PLAY_HEADERS);
  if (!looksLikeM3u8(text)) throw new Error('m3u8 content invalid: ' + String(text || '').slice(0, 80));
  return absolutizeM3u8(text, m3u8Url);
}

async function isPlayableM3u8(m3u8Url) {
  try {
    var text = await legado.http.get(m3u8Url, PLAY_HEADERS);
    return looksLikeM3u8(text);
  } catch (e) {
    log('[play] direct m3u8 check failed: ' + (e && e.message ? e.message : e));
    return false;
  }
}

function playUrlPathKey(url) {
  var m = String(url || '').match(/\/splitOut\/[^?]+\/index\.m3u8/i);
  return m ? m[0] : '';
}

function samePlayPath(a, b) {
  var aa = playUrlPathKey(a);
  var bb = playUrlPathKey(b);
  return aa && bb && aa === bb;
}

function browserStateScript(auth, resolution) {
  var script = '';
  if (auth) {
    var tokenJson = JSON.stringify(auth);
    script += 'localStorage.setItem("token",' + tokenJson + ');localStorage.setItem("authorization",' + tokenJson + ');sessionStorage.setItem("token",' + tokenJson + ');sessionStorage.setItem("authorization",' + tokenJson + ');try{var u=JSON.parse(localStorage.getItem("userStore")||"{\\"state\\":{}}");u.state=u.state||{};u.state.token=' + tokenJson + ';u.state.isLogin=true;localStorage.setItem("userStore",JSON.stringify(u));}catch(e){};';
  }
  if (resolution !== undefined && resolution !== null && resolution !== '') {
    script += 'localStorage.setItem("userResolution",' + JSON.stringify(String(resolution)) + ');';
  }
  return script ? script + 'return true;' : 'return true;';
}

function injectBrowserState(id, auth, resolution) {
  var script = browserStateScript(auth, resolution);
  try {
    legado.browser.eval(id, script);
    return true;
  } catch (e) {
    legado.browser.navigate(id, BASE + '/', { waitUntil: 'load', timeoutSecs: 8 });
    legado.browser.eval(id, script);
    return true;
  }
}

async function captureM3u8(playUrl, preferredUrl, resolution) {
  var id = legado.browser.acquire('jpyy-play', { visible: false, muted: true, userAgent: UA, timeoutSecs: 12 });
  var auth = getAuthorizationHeader();
  var found = '';
  var bodyUrl = '';
  legado.browser.onRequest(id, function(event) {
    var url = event && event.url ? String(event.url) : '';
    var lower = url.toLowerCase();
    if (lower.indexOf('.m3u8') === -1) return;
    if (event.status !== 200) return;
    var bodyOk = event.responseBody && looksLikeM3u8(event.responseBody);
    if (preferredUrl && samePlayPath(url, preferredUrl) && (bodyOk || lower.indexOf('auth_key=') !== -1)) {
      found = url;
      return;
    }
    if (bodyOk && !bodyUrl) bodyUrl = url;
    if (!found && lower.indexOf('auth_key=') !== -1) found = url;
  }, { captureBody: true, urlPattern: 'm3u8' });
  try {
    try {
      injectBrowserState(id, auth, resolution);
    } catch (e1) {
      log('[captureM3u8] inject state failed: ' + (e1 && e1.message ? e1.message : e1));
    }
    legado.browser.navigate(id, playUrl, { waitUntil: 'networkidle', timeoutSecs: 12 });
  } finally {
    try { legado.browser.offRequest(id); } catch (e) {}
  }
  if (!found && bodyUrl) found = bodyUrl;
  if (!found) throw new Error('未抓到可用 m3u8: ' + playUrl);
  return found;
}

function getSelectedQualityId(selectedCategories) {
  if (!selectedCategories) return '';
  if (typeof selectedCategories === 'string') {
    try {
      selectedCategories = JSON.parse(selectedCategories);
    } catch (e) {
      return selectedCategories;
    }
  }
  return selectedCategories.quality || selectedCategories.definition || selectedCategories.resolution || '';
}

function qualityId(item, index) {
  return 'q' + index + '_' + String(item.resolution || item.resolutionName || index).replace(/[^\w-]+/g, '_');
}

function qualityName(item, index) {
  return item.resolutionName || String(item.resolution || ('线路' + (index + 1)));
}

async function chapterContent(chapterUrl, selectedCategories) {
  var vodId = getVodId(chapterUrl);
  var nid = getNid(chapterUrl);
  if (!vodId) throw new Error('missing vodId: ' + chapterUrl);
  if (!nid) {
    var chapters = await chapterList(BASE + '/detail/' + vodId);
    if (!chapters.length) throw new Error('no episode: ' + vodId);
    nid = getNid(chapters[0].url);
  }
  var playInfo = await getEpisodePlayInfo(vodId, nid);
  var list = playInfo.list || playInfo.urls || [];
  var auth = getAuthorizationHeader();
  var selectedQualityId = getSelectedQualityId(selectedCategories);
  var selected = null;
  var fallback = null;
  var qualities = [];
  var qualityIndexById = {};
  var options = [];
  for (var i = 0; i < list.length; i++) {
    var item = list[i] || {};
    if (!item.url) continue;
    var id = qualityId(item, i);
    item._qualityId = id;
    var name = qualityName(item, i);
    var label = name;
    var locked = item.needLogin === true && !auth;
    if (locked) label += ' (需登录)';
    if (!locked) {
      qualityIndexById[id] = qualities.length;
      qualities.push({ label: name, url: item.url });
    }
    options.push({ id: id, label: label, badge: item.needLogin === true ? '登录' : '' });
    if (selectedQualityId && selectedQualityId === id && !locked) selected = item;
    if (!fallback && !locked) fallback = item;
  }
  if (!selected) selected = fallback;
  if (!selected && auth && list.length) {
    for (var j = 0; j < list.length; j++) {
      if (list[j] && list[j].url) {
        selected = list[j];
        break;
      }
    }
  }
  if (!selected || !selected.url) throw new Error(auth ? ('未获取到播放地址: ' + chapterUrl) : '未登录或登录令牌未同步,请到设置页登录后同步令牌');
  if (isIpBoundPlayUrl(selected.url)) {
    try {
      if (await isPlayableM3u8(selected.url)) {
        log('[chapterContent] direct m3u8 playable');
      } else {
        var captured = await captureM3u8(BASE + '/vod/play/' + vodId + '/1/' + nid, selected.url, selected.resolution);
        log('[chapterContent] replaced ip-bound url with captured m3u8');
        selected.url = captured;
      }
    } catch (e) {
      log('[chapterContent] capture fallback failed: ' + (e && e.message ? e.message : e));
    }
  }
  var q = qualityIndexById[selected._qualityId];
  if (q !== undefined && qualities[q]) {
    qualities[q].url = selected.url;
  }
  return JSON.stringify({
    url: selected.url,
    type: 'hls',
    headers: PLAY_HEADERS,
    qualities: qualities,
    categories: [{
      id: 'quality',
      label: '\u6e05\u6670\u5ea6',
      defaultSelected: selected._qualityId || '',
      options: options
    }]
  });
}
广告