乌云影视
https://wooyun.tv
zpccool (13551) 2天前 下载:832
视频
乌云影视视频书源,使用站点前端 API 直取搜索、详情、目录和 m3u8 播放地址,不依赖浏览器嗅探。
// @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 };
}