乌云影视

https://wooyun.tv

zpccool (13551) 2天前 下载:832

视频
乌云影视视频书源,使用站点前端 API 直取搜索、详情、目录和 m3u8 播放地址,不依赖浏览器嗅探。
二维码导入(APP尚未完成该功能)
// @name        乌云影视
// @uuid        wuyunyingshi
// @version     1.0.0
// @author      AI
// @url         https://wooyun.tv
// @type        video
// @enabled     true
// @description 乌云影视视频书源,使用站点前端 API 直取搜索、详情、目录和 m3u8 播放地址,不依赖浏览器嗅探。

var BASE = 'https://wooyun.tv';
var UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36';
var JSON_HEADERS = {
  'User-Agent': UA,
  'Accept': 'application/json, text/plain, */*',
  'Accept-Language': 'zh-CN,zh;q=0.9',
  'Content-Type': 'application/json',
  'Referer': BASE + '/'
};
var PLAY_HEADERS = {
  'User-Agent': UA,
  'Accept': '*/*',
  'Accept-Language': 'zh-CN,zh;q=0.9',
  'Referer': BASE + '/'
};
var CATEGORIES = {
  '推荐': '/movie/media/home/custom/classify/{page}/3?limit=12',
  '电影': '/movie/media/search',
  '电视剧': '/movie/media/search',
  '韩剧': '/movie/media/search',
  '短剧': '/movie/media/search',
  '动画': '/movie/media/search',
  '综艺': '/movie/media/search'
};
var TOP_CODES = {
  '电影': 'movie',
  '电视剧': 'tv_series',
  '韩剧': 'korean_drama',
  '短剧': 'short_drama',
  '动画': 'animation',
  '综艺': 'variety'
};

function init() {
  legado.log('[init] 乌云影视 ready');
}

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

function recoverText(s) {
  s = String(s || '');
  if (!/[ÃÂ]|[äåæçèé][\x80-\xBF]/.test(s)) return s;
  var enc = '';
  for (var i = 0; i < s.length; i++) {
    var c = s.charCodeAt(i);
    if (c < 256) {
      enc += '%' + ('0' + c.toString(16)).slice(-2);
    } else {
      enc += encodeURIComponent(s.charAt(i));
    }
  }
  try {
    var out = decodeURIComponent(enc);
    return out || s;
  } catch (e) {
    return s;
  }
}

function fixObj(o) {
  if (o === null || o === undefined) return o;
  if (typeof o === 'string') return recoverText(o);
  if (Array.isArray(o)) {
    for (var i = 0; i < o.length; i++) o[i] = fixObj(o[i]);
    return o;
  }
  if (typeof o === 'object') {
    for (var k in o) {
      if (Object.prototype.hasOwnProperty.call(o, k)) o[k] = fixObj(o[k]);
    }
    return o;
  }
  return o;
}

function joinList(v) {
  if (!v) return '';
  if (Array.isArray(v)) return v.filter(function(x) { return !!x; }).join(',');
  return String(v);
}

function firstValue() {
  for (var i = 0; i < arguments.length; i++) {
    if (arguments[i] !== undefined && arguments[i] !== null && String(arguments[i]) !== '') return arguments[i];
  }
  return '';
}

function mediaIdFromUrl(url) {
  url = String(url || '');
  var m = /(?:mediaId=|\/play\/)(\d+)/i.exec(url);
  return m ? m[1] : url.replace(/\D/g, '');
}

function epFromUrl(url) {
  var m = /[?&]ep=(\d+)/i.exec(String(url || ''));
  if (m) return Number(m[1]);
  m = /\/play\/\d+--(\d+)/i.exec(String(url || ''));
  return m ? Number(m[1]) : null;
}

function apiUrl(path) {
  return BASE + '/api/proxy?url=' + encodeURIComponent(path);
}

async function apiGet(path) {
  var text = await legado.http.get(apiUrl(path), JSON_HEADERS);
  var json = JSON.parse(text);
  if (json.code !== 200 || json.isSuccess === false) throw new Error(recoverText(json.resultMsg || 'API error'));
  return fixObj(json.data);
}

async function apiPost(path, body) {
  var text = await legado.http.post(apiUrl(path), JSON.stringify(body || {}), JSON_HEADERS);
  var json = JSON.parse(text);
  if (json.code !== 200 || json.isSuccess === false) throw new Error(recoverText(json.resultMsg || 'API error'));
  return fixObj(json.data);
}

function mediaToBook(m) {
  m = m || {};
  var mediaType = m.mediaType || {};
  var name = firstValue(m.title, m.mediaName);
  var id = firstValue(m.id, m.mediaId);
  var cats = [];
  if (mediaType.name) cats.push(mediaType.name);
  if (m.region) cats.push(m.region);
  if (m.releaseYear) cats.push(String(m.releaseYear));
  if (m.genres) cats = cats.concat(m.genres);
  if (m.mediaCategories) {
    for (var i = 0; i < m.mediaCategories.length; i++) {
      if (m.mediaCategories[i] && m.mediaCategories[i].name) cats.push(m.mediaCategories[i].name);
    }
  }
  return {
    name: name,
    author: joinList(firstValue(m.directors, m.actors)),
    bookUrl: BASE + '/play/' + id,
    tocUrl: BASE + '/play/' + id,
    coverUrl: firstValue(m.posterUrlS3, m.posterUrl, m.coverUrl, m.backdropUrlS3, m.backdropUrl),
    intro: firstValue(m.overview, m.description, m.originalTitle),
    kind: cats.filter(function(x) { return !!x; }).join(','),
    latestChapter: firstValue(m.episodeStatus, m.recentlyUpdatedEpisodes && m.recentlyUpdatedEpisodes.length ? ('更新至' + m.recentlyUpdatedEpisodes[m.recentlyUpdatedEpisodes.length - 1]) : ''),
    latestChapterUrl: BASE + '/play/' + id,
    updateTime: firstValue(m.releaseDate, m.createTime, m.updateTime),
    status: firstValue(m.episodeStatus, '')
  };
}

function parseHomeRecords(data) {
  var out = [];
  var seen = {};
  var records = (data && data.records) || [];
  for (var i = 0; i < records.length; i++) {
    var list = records[i].mediaResources || records[i].records || [];
    for (var j = 0; j < list.length; j++) {
      var item = mediaToBook(list[j]);
      if (item.bookUrl && !seen[item.bookUrl]) {
        seen[item.bookUrl] = true;
        out.push(item);
      }
    }
  }
  return out;
}

async function explore(page, category) {
  legado.log('[explore] page=' + page + ' category=' + category);
  if (page === 'GETALL' || category === 'GETALL' || page === undefined) {
    return ['推荐', '电影', '电视剧', '韩剧', '短剧', '动画', '综艺'];
  }
  if (typeof page === 'string' && !/^\d+$/.test(page) && category === undefined) {
    category = page;
    page = 1;
  }
  page = Number(page || 1);
  category = category || '推荐';
  if (category === '推荐') {
    return parseHomeRecords(await apiGet('/movie/media/home/custom/classify/' + page + '/3?limit=12'));
  }
  var data = await apiPost('/movie/media/search', {
    menuCodeList: [],
    pageIndex: page,
    pageSize: 12,
    searchKey: '',
    topCode: TOP_CODES[category] || ''
  });
  var rows = data.records || [];
  var out = [];
  for (var i = 0; i < rows.length; i++) out.push(mediaToBook(rows[i]));
  return out;
}

async function search(keyword, page) {
  legado.log('[search] keyword=' + keyword + ' page=' + page);
  var data = await apiPost('/movie/media/search', {
    menuCodeList: [],
    pageIndex: Number(page || 1),
    pageSize: 10,
    searchKey: String(keyword || ''),
    topCode: ''
  });
  var rows = data.records || [];
  var out = [];
  for (var i = 0; i < rows.length; i++) out.push(mediaToBook(rows[i]));
  return out;
}

async function bookInfo(bookUrl) {
  legado.log('[bookInfo] url=' + bookUrl);
  var id = mediaIdFromUrl(bookUrl);
  var m = await apiGet('/movie/media/detail?mediaId=' + id);
  var item = mediaToBook(m);
  item.bookUrl = BASE + '/play/' + id;
  item.tocUrl = item.bookUrl;
  return item;
}

async function chapterList(tocUrl) {
  legado.log('[chapterList] url=' + tocUrl);
  var id = mediaIdFromUrl(tocUrl);
  var groups = await apiGet('/movie/media/video/list?mediaId=' + id + '&lineName=&resolutionCode=');
  var out = [];
  for (var i = 0; i < groups.length; i++) {
    var group = groups[i] || {};
    var videos = group.videoList || [];
    var groupName = firstValue(group.lineName, group.name, group.resolutionName, '默认线路');
    for (var j = 0; j < videos.length; j++) {
      var v = videos[j] || {};
      var epNo = Number(firstValue(v.epNo, j + 1));
      var name = epNo === 0 ? '正片' : ('第' + epNo + '集');
      if (v.remark) name += ' ' + v.remark;
      out.push({
        name: name,
        url: 'wooyun://play?mediaId=' + id + '&ep=' + epNo + '&line=' + encodeURIComponent(groupName),
        group: groupName
      });
    }
  }
  return out;
}

async function chapterContent(chapterUrl) {
  legado.log('[chapterContent] url=' + chapterUrl);
  var id = mediaIdFromUrl(chapterUrl);
  var ep = epFromUrl(chapterUrl);
  var lm = /[?&]line=([^&]+)/i.exec(String(chapterUrl || ''));
  var line = lm ? decodeURIComponent(lm[1]) : '';
  var groups = await apiGet('/movie/media/video/list?mediaId=' + id + '&lineName=&resolutionCode=');
  var selected = null;
  for (var i = 0; i < groups.length; i++) {
    var group = groups[i] || {};
    var groupName = firstValue(group.lineName, group.name, group.resolutionName, '默认线路');
    if (line && groupName !== line) continue;
    var videos = group.videoList || [];
    for (var j = 0; j < videos.length; j++) {
      var v = videos[j] || {};
      var vEp = Number(firstValue(v.epNo, j + 1));
      if (ep === null || vEp === ep) {
        selected = v;
        break;
      }
    }
    if (selected) break;
  }
  if (!selected || !selected.playUrl) throw new Error('未找到播放地址');
  var url = String(selected.playUrl).replace(/\\\//g, '/');
  if (!/^https?:\/\//i.test(url)) throw new Error('播放地址不是直链');
  return JSON.stringify({
    url: url,
    type: /\.m3u8(?:$|\?)/i.test(url) ? 'hls' : 'mp4',
    headers: {
      'User-Agent': UA,
      'Referer': BASE + '/play/' + id
    }
  });
}

async function TEST(type) {
  if (type === '__list__') return ['search', 'explore', 'bookInfo', 'chapterList', 'chapterContent'];
  if (type === 'search') {
    var s = await search('仙逆', 1);
    return { passed: s.length > 0, message: 'search found=' + s.length + (s[0] ? ' first=' + s[0].name : '') };
  }
  if (type === 'explore') {
    var cats = await explore(1, 'GETALL');
    var books = await explore(1, '推荐');
    return { passed: cats.length >= 6 && books.length > 0, message: 'explore cats=' + cats.length + ' books=' + books.length };
  }
  if (type === 'bookInfo') {
    var b = await bookInfo(BASE + '/play/171');
    return { passed: !!b.name, message: 'bookInfo name=' + b.name };
  }
  if (type === 'chapterList') {
    var c = await chapterList(BASE + '/play/171');
    return { passed: c.length > 0, message: 'chapterList cnt=' + c.length + (c[0] ? ' first=' + c[0].name : '') };
  }
  if (type === 'chapterContent') {
    var list = await chapterList(BASE + '/play/171');
    var text = await chapterContent(list[0].url);
    return { passed: text.indexOf('.m3u8') > 0, message: 'chapterContent len=' + text.length };
  }
  return { passed: false, message: 'Unknown test type: ' + type };
}
广告