diff --git a/js/优夏.js b/js/优夏.js new file mode 100644 index 0000000..16eccdb --- /dev/null +++ b/js/优夏.js @@ -0,0 +1,469 @@ +import cheerio from 'assets://js/lib/cheerio.min.js'; + +const appConfig = { + siteName: "伦理专题影片", + siteUrl: "http://yoxayg.com" +}; +const UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"; + +async function init(ext) { + console.log("初始化爬虫:", appConfig.siteName); +} + +const classList = [ + { type_id: "1", type_name: "电影" }, + { type_id: "2", type_name: "电视剧" }, + { type_id: "3", type_name: "综艺" }, + { type_id: "4", type_name: "动漫" }, + { type_id: "36", type_name: "短剧" }, + { type_id: "6", type_name: "动作片" }, + { type_id: "7", type_name: "喜剧片" }, + { type_id: "8", type_name: "爱情片" }, + { type_id: "9", type_name: "科幻片" }, + { type_id: "10", type_name: "恐怖片" }, + { type_id: "11", type_name: "剧情片" }, + { type_id: "12", type_name: "战争片" }, + { type_id: "20", type_name: "记录片" }, + { type_id: "34", type_name: "伦理片" }, + { type_id: "13", type_name: "国产剧" }, + { type_id: "14", type_name: "香港剧" }, + { type_id: "15", type_name: "韩国剧" }, + { type_id: "16", type_name: "欧美剧" }, + { type_id: "21", type_name: "台湾剧" }, + { type_id: "22", type_name: "日本剧" }, + { type_id: "23", type_name: "海外剧" }, + { type_id: "24", type_name: "泰国剧" }, + { type_id: "25", type_name: "大陆综艺" }, + { type_id: "26", type_name: "港台综艺" }, + { type_id: "27", type_name: "日韩综艺" }, + { type_id: "28", type_name: "欧美综艺" }, + { type_id: "29", type_name: "国产动漫" }, + { type_id: "30", type_name: "日韩动漫" }, + { type_id: "31", type_name: "欧美动漫" }, + { type_id: "32", type_name: "港台动漫" }, + { type_id: "33", type_name: "海外动漫" } +]; + +const typeSlugMap = { + "1": "dianying", "2": "dianshiju", "3": "zongyi", "4": "dongman", "6": "dongzuopian", + "7": "xijupian", "8": "aiqingpian", "9": "kehuanpian", "10": "kongbupian", "11": "juqingpian", + "12": "zhanzhengpian", "13": "guochanju", "14": "xianggangju", "15": "hanguoju", "16": "oumeiju", + "20": "jilupian", "21": "taiwanju", "22": "ribenju", "23": "haiwaiju", "24": "taiguoju", + "25": "daluzongyi", "26": "gangtaizongyi", "27": "rihanzongyi", "28": "oumeizongyi", + "29": "guochandongman", "30": "rihandongman", "31": "oumeidongman", "32": "gangtaidongman", + "33": "haiwaidongman", "34": "lunlipian", "36": "duanju" +}; + +const typeParentMap = { + "1": 0, "2": 0, "3": 0, "4": 0, "6": 1, "7": 1, "8": 1, "9": 1, "10": 1, + "11": 1, "12": 1, "13": 2, "14": 2, "15": 2, "16": 2, "20": 1, "21": 2, "22": 2, + "23": 2, "24": 2, "25": 3, "26": 3, "27": 3, "28": 3, "29": 4, "30": 4, + "31": 4, "32": 4, "33": 4, "34": 1, "36": 2 +}; + +function topType(tid) { + tid = parseInt(tid || 0, 10); + if (tid === 36) return 36; + const p = parseInt(typeParentMap[String(tid)] || 0, 10); + return p > 0 ? p : tid; +} + +function typeSlug(tid) { + return typeSlugMap[String(parseInt(tid || 0, 10))] || ('type' + parseInt(tid || 0, 10)); +} + +function getAreaFilter() { + return { + "key": "area", "name": "地区", "value": [ + { "n": "全部", "v": "" }, { "n": "大陆", "v": "大陆" }, { "n": "香港", "v": "香港" }, + { "n": "台湾", "v": "台湾" }, { "n": "美国", "v": "美国" }, { "n": "日本", "v": "日本" }, + { "n": "韩国", "v": "韩国" }, { "n": "英国", "v": "英国" }, { "n": "法国", "v": "法国" }, + { "n": "德国", "v": "德国" }, { "n": "泰国", "v": "泰国" }, { "n": "印度", "v": "印度" }, + { "n": "其他", "v": "其他" } + ] + }; +} + +function getYearFilter() { + let years = [{ "n": "全部", "v": "" }]; + const currentYear = new Date().getFullYear(); + for (let y = currentYear; y >= 2010; y--) { + years.push({ "n": String(y), "v": String(y) }); + } + return { "key": "year", "name": "年份", "value": years }; +} + +const commonFilters = [getAreaFilter(), getYearFilter()]; + +const myFilters = {}; +classList.forEach(item => { + myFilters[item.type_id] = commonFilters; +}); + +function fixUrl(u) { + if (!u) return ''; + if (u.startsWith('http')) return u; + if (u.startsWith('//')) return 'http:' + u; + if (u.startsWith('/')) return appConfig.siteUrl + u; + return u; +} + +async function home(filter) { + let list = []; + try { + const html = (await req(appConfig.siteUrl, { + method: "GET", + headers: { "User-Agent": UA } + })).content; + const $ = cheerio.load(html); + let seen = {}; + + $(".tpl-card[href]").each(function () { + let vod_id = $(this).attr("href"); + if (!vod_id || seen[vod_id]) return; + + let vod_name = $(this).attr("title") || $(this).find("strong").first().text().trim() || ""; + vod_name = vod_name.replace(/免费在线观看$/, ""); + let vod_pic = fixUrl($(this).find("img").first().attr("src") || ""); + let vod_remarks = $(this).find("em").first().text().trim(); + + if (vod_name && vod_id) { + seen[vod_id] = true; + list.push({ vod_id, vod_name, vod_pic, vod_remarks }); + } + }); + } catch (e) { + console.error("首页推荐获取失败:", e.message); + } + + return JSON.stringify({ + class: classList, + filters: myFilters, + list: list.slice(0, 30) + }); +} + +function buildCategoryUrl(tid, pg, extend) { + extend = extend || {}; + let top = topType(tid); + let topSlug = typeSlug(top); + let childSlug = (tid != top) ? typeSlug(tid) : ''; + + let path; + if (childSlug && tid != 36) { + path = `/${topSlug}/${childSlug}/`; + } else { + path = `/${topSlug}/`; + } + + if (pg > 1) { + path += `page-${pg}.html`; + } + + let area = extend.area || ''; + let year = extend.year || ''; + if (area || year) { + let params = []; + if (area) params.push('area=' + encodeURIComponent(area)); + if (year) params.push('year=' + encodeURIComponent(year)); + return appConfig.siteUrl + `/api_proxy.php?ac=list&t=${tid}&pg=${pg}&${params.join('&')}`; + } + + return appConfig.siteUrl + path; +} + +function parseListHtml(html) { + const $ = cheerio.load(html); + let list = []; + let vodIds = {}; + + $(".tpl-card.card-poster, .tpl-card.card-wide").each(function () { + let $a = $(this); + let vod_id = $a.attr("href"); + if (!vod_id || vodIds[vod_id]) return; + + let vod_name = $a.attr("title") || $a.find("strong").first().text().trim() || ""; + vod_name = vod_name.replace(/免费在线观看$/, ""); + let vod_pic = fixUrl($a.find("img").first().attr("src") || ""); + let vod_remarks = $a.find("em").first().text().trim() || $a.find("small").first().text().trim(); + + if (vod_name && vod_id) { + vodIds[vod_id] = true; + list.push({ vod_id, vod_name, vod_pic, vod_remarks }); + } + }); + + if (list.length === 0) { + try { + let data = JSON.parse(html); + if (data.list && Array.isArray(data.list)) { + data.list.forEach(item => { + let vod_id = `/${typeSlug(topType(item.type_id))}/${(item.vod_en || item.vod_name || '').toLowerCase().replace(/[^a-z0-9]+/g, '')}.html`; + let vod_name = item.vod_name || ""; + let vod_pic = item.vod_pic || ""; + let vod_remarks = item.vod_remarks || ""; + if (vod_name) { + list.push({ vod_id, vod_name, vod_pic, vod_remarks }); + } + }); + } + } catch (e) { + } + } + + let pagecount = 1; + $(".pagination a").each(function () { + let href = $(this).attr("href") || ''; + let m = href.match(/page-(\d+)/); + if (m) { + let p = parseInt(m[1]); + if (p > pagecount) pagecount = p; + } + }); + + return { list, pagecount }; +} + +async function category(tid, pg, filter, extend) { + pg = pg || 1; + extend = extend || {}; + + let url = buildCategoryUrl(tid, pg, extend); + + try { + const html = (await req(url, { + method: "GET", + headers: { "User-Agent": UA, "Referer": appConfig.siteUrl } + })).content; + const result = parseListHtml(html); + return JSON.stringify(result); + } catch (e) { + console.error("分类列表获取失败:", e.message); + return JSON.stringify({ list: [], pagecount: 0 }); + } +} + +async function search(wd, quick, page) { + page = page || 1; + try { + const url = `${appConfig.siteUrl}/search-${encodeURIComponent(wd)}${page > 1 ? '-page-' + page : ''}.html`; + const html = (await req(url, { + method: "GET", + headers: { "User-Agent": UA, "Referer": appConfig.siteUrl } + })).content; + const result = parseListHtml(html); + return JSON.stringify(result); + } catch (e) { + console.error("搜索失败:", e.message); + return JSON.stringify({ list: [], pagecount: 0 }); + } +} + +async function detail(id) { + try { + const html = (await req(appConfig.siteUrl + id, { + method: "GET", + headers: { "User-Agent": UA, "Referer": appConfig.siteUrl } + })).content; + const $ = cheerio.load(html); + + let vod_name = $("h1").first().text().trim(); + + let vod_pic = fixUrl($(".detail-poster img").first().attr("src") || ""); + + let vod_director = ""; + let vod_actor = ""; + let vod_area = ""; + let vod_year = ""; + let vod_content = ""; + let vod_class = ""; + let vod_remarks = ""; + + $(".detail-tags span").each(function () { + let text = $(this).text().trim(); + if (/^\d{4}$/.test(text) && !vod_year) vod_year = text; + else if (/大陆|香港|台湾|美国|日本|韩国|英国|法国|德国|泰国|印度|其他/.test(text) && !vod_area) vod_area = text; + else if (!vod_class && text !== vod_year && text !== vod_area && !/更新至|集|期|TC|HD|正片/.test(text)) vod_class = text; + }); + + vod_remarks = $(".detail-poster span").first().text().trim(); + + $(".detail-meta p").each(function () { + let text = $(this).text(); + if (text.includes("主演:") && !vod_actor) { + vod_actor = text.replace("主演:", "").trim(); + } + if (text.includes("导演:") && !vod_director) { + vod_director = text.replace("导演:", "").trim(); + } + }); + + let $intro = $(".detail-intro"); + if ($intro.length > 0) { + vod_content = $intro.first().text().replace("简介:", "").trim(); + } + if (!vod_content) { + $intro = $(".article-text"); + if ($intro.length > 0) { + vod_content = $intro.first().text().trim(); + } + } + + let lines = []; + let playlists = []; + let epArray = []; + + $(".episode-grid a").each(function () { + let name = $(this).text().trim(); + let href = $(this).attr('href') || ''; + if (name && href) { + epArray.push({ name, href }); + } + }); + + epArray.sort((a, b) => { + let numA = parseInt(a.name.match(/第(\d+)/)?.[1] || 0); + let numB = parseInt(b.name.match(/第(\d+)/)?.[1] || 0); + return numA - numB; + }); + + let episodes = epArray.map(ep => `${ep.name}$${ep.href}`); + + if (episodes.length > 0) { + lines.push("默认"); + playlists.push(episodes); + } else { + let playHref = $(".detail-actions a.btn-primary").attr("href") || ""; + if (playHref) { + lines.push("默认"); + playlists.push([`正片$${playHref}`]); + } + } + + if (lines.length === 0) { + lines.push("默认"); + playlists.push([`暂无播放地址$${id}`]); + } + + const { vod_play_from, vod_play_url } = buildVodPlayData(lines, playlists); + + return JSON.stringify({ + list: [{ + vod_id: id, + vod_name, + vod_pic, + vod_actor, + vod_director, + vod_remarks, + vod_year, + vod_area, + vod_content, + vod_class, + vod_play_from, + vod_play_url + }] + }); + } catch (error) { + console.error(`解析详情页异常 [ID: ${id}]:`, error); + return JSON.stringify({ list: [] }); + } +} + +function buildVodPlayData(lines, playlists) { + const processedPlaylists = playlists.map(eps => eps.join('#')); + return { + vod_play_from: lines.filter(Boolean).join('$$$'), + vod_play_url: processedPlaylists.join('$$$') + }; +} + +async function play(flag, id, flags) { + try { + if (id.startsWith("http")) { + return JSON.stringify({ + parse: 0, + Header: { "User-Agent": UA, "Referer": appConfig.siteUrl }, + url: id + }); + } + + const html = (await req(`${appConfig.siteUrl}${id}`, { + method: "GET", + headers: { "User-Agent": UA, "Referer": appConfig.siteUrl } + })).content; + + // 方式1:从 noscript 中提取 m3u8 URL + let noscriptMatch = html.match(/