布布影视

https://bbys.app

chen-muting (10820) 05/19 08:58 下载:4697

视频 免费 影视 在线播放
布布影视视频源;使用 FreeOK/MacCMS 接口作为数据入口,修复接口伪 JSON 换行导致解析为空的问题。
二维码导入(APP尚未完成该功能)
// @uuid        019e3dbd-8b98-7aef-a98f-f69849175be6
// ─── 元数据 ─────────────────────────────────────
// @name        布布影视
// @version     1.1.0
// @author      ChatGPT
// @url         https://bbys.app
// @url         https://www.freeokk.pro
// @logo        https://bbys.app/favicon.ico
// @type        video
// @enabled     true
// @minDelay    300
// @tags        免费,影视,在线播放
// @description 布布影视视频源;使用 FreeOK/MacCMS 接口作为数据入口,修复接口伪 JSON 换行导致解析为空的问题。

var BASE = 'https://bbys.app';
var API_BASE = 'https://www.freeokk.pro';
var API = API_BASE + '/api.php/provide/vod/';
var UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';

var CATEGORIES = {
  '电影': 1,
  '剧集': 2,
  '综艺': 3,
  '动漫': 4
};

function log(msg) {
  try { legado.log('[布布影视] ' + msg); } catch (e) {}
}

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

function htmlDecode(s) {
  s = String(s || '');
  return s
    .replace(/&#x([0-9a-fA-F]+);/g, function(_, n) { return String.fromCharCode(parseInt(n, 16)); })
    .replace(/&#(\d+);/g, function(_, n) { return String.fromCharCode(parseInt(n, 10)); })
    .replace(/ /g, ' ')
    .replace(/&/g, '&')
    .replace(/"/g, '"')
    .replace(/'/g, "'")
    .replace(/&lt;/g, '<')
    .replace(/&gt;/g, '>');
}

function cleanText(s) {
  return htmlDecode(String(s || '')
    .replace(/<script[\s\S]*?<\/script>/ig, ' ')
    .replace(/<style[\s\S]*?<\/style>/ig, ' ')
    .replace(/<[^>]+>/g, ' ')
    .replace(/\s+/g, ' ')
  ).trim();
}

function absUrl(base, url) {
  url = String(url || '').trim();
  if (!url) return '';
  if (/^https?:\/\//i.test(url)) return url;
  if (/^\/\//.test(url)) return 'https:' + url;
  base = trimSlash(base || API_BASE);
  if (url.charAt(0) === '/') return base + url;
  return base + '/' + url;
}

function apiUrl(params) {
  return API + '?' + params;
}

function headers(referer) {
  return {
    'User-Agent': UA,
    'Accept': 'application/json,text/plain,*/*',
    'Referer': referer || (API_BASE + '/')
  };
}

async function getText(url) {
  try {
    return await legado.http.get(url, headers(API_BASE + '/'));
  } catch (e) {
    log('GET失败 ' + url + ' -> ' + (e && e.message ? e.message : e));
    return '';
  }
}

// FreeOK 当前接口部分字段内含原始换行,严格 JSON.parse 会失败;这里做一次宽松修复。
function parseJsonLoose(text) {
  text = String(text || '').replace(/^\uFEFF/, '').trim();
  if (!text) return null;
  try { return JSON.parse(text); } catch (e) {}

  var out = '';
  var inStr = false;
  var esc = false;
  for (var i = 0; i < text.length; i++) {
    var ch = text.charAt(i);
    if (inStr) {
      if (esc) {
        out += ch;
        esc = false;
      } else if (ch === '\\') {
        out += ch;
        esc = true;
      } else if (ch === '"') {
        out += ch;
        inStr = false;
      } else if (ch === '\n') {
        out += '\\n';
      } else if (ch === '\r') {
        out += '\\r';
      } else if (ch === '\t') {
        out += '\\t';
      } else {
        out += ch;
      }
    } else {
      out += ch;
      if (ch === '"') inStr = true;
    }
  }
  try { return JSON.parse(out); } catch (e2) {
    log('JSON解析失败:' + (e2 && e2.message ? e2.message : e2));
    return null;
  }
}

async function getJson(url) {
  var text = await getText(url);
  return parseJsonLoose(text);
}

function pickIntro(v) {
  return cleanText(v.vod_blurb || v.vod_content || v.vod_sub || '');
}

function toBook(v) {
  v = v || {};
  var id = v.vod_id || v.id;
  var name = cleanText(v.vod_name || v.name || v.title || '');
  if (!id || !name) return null;
  var detailUrl = apiUrl('ac=detail&ids=' + encodeURIComponent(id));
  return {
    name: name,
    bookUrl: detailUrl,
    tocUrl: detailUrl,
    author: cleanText(v.vod_director || v.vod_actor || ''),
    coverUrl: absUrl(API_BASE, v.vod_pic || v.pic || v.cover || ''),
    intro: pickIntro(v),
    kind: cleanText(v.type_name || ''),
    latestChapter: cleanText(v.vod_remarks || v.note || v.remarks || ''),
    lastChapter: cleanText(v.vod_remarks || v.note || v.remarks || ''),
    updateTime: cleanText(v.vod_time || ''),
    status: cleanText(v.vod_remarks || v.note || v.remarks || ''),
    chapterCount: parseInt(v.vod_total || v.vod_serial || 0, 10) || 0
  };
}

function parseBooks(json) {
  var arr = [];
  if (json && Array.isArray(json.list)) arr = json.list;
  else if (json && json.data && Array.isArray(json.data)) arr = json.data;
  else if (json && json.data && Array.isArray(json.data.list)) arr = json.data.list;

  var list = [];
  for (var i = 0; i < arr.length; i++) {
    var item = toBook(arr[i]);
    if (item) list.push(item);
  }
  return list;
}

function firstVod(json) {
  if (json && json.list && json.list[0]) return json.list[0];
  if (json && json.data && json.data[0]) return json.data[0];
  if (json && json.data && json.data.list && json.data.list[0]) return json.data.list[0];
  return null;
}

async function search(keyword, page) {
  page = page || 1;
  log('search keyword=' + keyword + ' page=' + page);

  var url = apiUrl('ac=detail&wd=' + encodeURIComponent(keyword) + '&pg=' + page);
  var list = parseBooks(await getJson(url));
  if (list.length > 0) return list;

  // 备用:站内 suggest 接口。部分环境下可用。
  var suggest = await getJson(API_BASE + '/index.php/ajax/suggest?mid=1&wd=' + encodeURIComponent(keyword));
  var sList = [];
  var sArr = (suggest && (suggest.list || suggest.data || suggest.result)) || [];
  if (sArr && sArr.list) sArr = sArr.list;
  if (Array.isArray(sArr)) {
    for (var i = 0; i < sArr.length; i++) {
      var v = sArr[i] || {};
      var id = v.id || v.vod_id;
      var name = v.name || v.vod_name || v.title;
      if (!id || !name) continue;
      sList.push({
        name: cleanText(name),
        bookUrl: apiUrl('ac=detail&ids=' + encodeURIComponent(id)),
        tocUrl: apiUrl('ac=detail&ids=' + encodeURIComponent(id)),
        author: cleanText(v.actor || v.vod_actor || ''),
        coverUrl: absUrl(API_BASE, v.pic || v.vod_pic || ''),
        kind: cleanText(v.type_name || ''),
        latestChapter: cleanText(v.note || v.remarks || v.vod_remarks || ''),
        status: cleanText(v.note || v.remarks || v.vod_remarks || '')
      });
    }
  }
  return sList;
}

async function bookInfo(bookUrl) {
  log('bookInfo url=' + bookUrl);
  var json = await getJson(bookUrl);
  var v = firstVod(json);
  var info = toBook(v);
  if (info) return info;
  return { name: '', bookUrl: bookUrl, tocUrl: bookUrl, author: '', coverUrl: '', intro: '' };
}

function routeName(raw, index) {
  raw = String(raw || '').trim();
  if (!raw) return '线路' + (index + 1);
  var map = {
    ffm3u8: '非凡',
    rym3u8: '如意',
    bfzym3u8: '暴风',
    dyttm3u8: '电影天堂',
    youku: '优酷',
    '1080zyk': '1080资源'
  };
  return map[raw] || raw || ('线路' + (index + 1));
}

function fixPlayUrl(url) {
  url = String(url || '').trim();
  if (!url) return '';
  url = htmlDecode(url).replace(/\\\//g, '/');
  return url;
}

async function chapterList(tocUrl) {
  log('chapterList url=' + tocUrl);
  var json = await getJson(tocUrl);
  var v = firstVod(json);
  if (!v) return [];

  var froms = String(v.vod_play_from || '').split('$$$');
  var lines = String(v.vod_play_url || '').split('$$$');
  var chapters = [];

  for (var r = 0; r < lines.length; r++) {
    var line = lines[r] || '';
    if (!line) continue;
    var group = routeName(froms[r], r);
    var eps = line.split('#');
    for (var i = 0; i < eps.length; i++) {
      var ep = eps[i];
      if (!ep) continue;
      var p = ep.indexOf('$');
      if (p < 0) continue;
      var name = cleanText(ep.substring(0, p)) || ('第' + (i + 1) + '集');
      var url = fixPlayUrl(ep.substring(p + 1));
      if (!url) continue;
      chapters.push({ name: name, url: url, group: group });
    }
  }
  return chapters;
}

function detectType(url) {
  var u = String(url || '').split('?')[0].toLowerCase();
  if (/\.m3u8$/.test(u)) return 'hls';
  if (/\.mpd$/.test(u)) return 'dash';
  if (/\.flv$/.test(u)) return 'flv';
  return 'mp4';
}

async function chapterContent(chapterUrl) {
  log('chapterContent url=' + chapterUrl);
  chapterUrl = fixPlayUrl(chapterUrl);
  if (!chapterUrl) return '';

  return JSON.stringify({
    url: chapterUrl,
    type: detectType(chapterUrl),
    headers: {
      'User-Agent': UA,
      'Referer': API_BASE + '/'
    }
  });
}

async function explore(page, category) {
  page = page || 1;
  if (category === 'GETALL') return Object.keys(CATEGORIES);

  var typeId = CATEGORIES[category] || 1;
  log('explore category=' + category + ' page=' + page);
  var list = parseBooks(await getJson(apiUrl('ac=detail&t=' + typeId + '&pg=' + page)));
  return list;
}

function assertOk(cond, msg) {
  if (!cond) throw new Error('断言失败: ' + msg);
}

async function TEST(type) {
  if (type === '__list__') return ['search', 'explore', 'bookInfo', 'chapterList', 'chapterContent'];

  var results = await search('黑夜告白', 1);
  assertOk(results.length > 0, '搜索无结果');

  var exp = await explore(1, '电影');
  assertOk(exp.length > 0, '发现无结果');

  var info = await bookInfo(results[0].bookUrl);
  assertOk(info && info.tocUrl, '详情缺少 tocUrl');

  var chapters = await chapterList(info.tocUrl);
  assertOk(chapters.length > 0, '目录无结果');

  var play = await chapterContent(chapters[0].url);
  assertOk(play && play.length > 50, '播放地址为空');

  return {
    search: results.length,
    explore: exp.length,
    name: info.name,
    chapters: chapters.length,
    play: play
  };
}
广告