diff --git a/open/weiguan_dj.js b/open/weiguan_dj.js new file mode 100644 index 0000000..cafa66d --- /dev/null +++ b/open/weiguan_dj.js @@ -0,0 +1,340 @@ +/* +@header({ + searchable: 1, + filterable: 1, + quickSearch: 1, + title: '围观短剧', + lang: 'cat' +}) +*/ + +let siteName = '围观短剧'; +let siteKey = ''; +let siteType = 0; + +let UA = "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.91 Mobile Safari/537.36"; + +let rule = { + host: 'https://api.drama.9ddm.com', + tagsUrl: '/drama/home/shortVideoTags?version_code=1500&os_type=1', + searchUrl: '/drama/home/search?version_code=1500&os_type=1', + detailUrl: '/drama/home/shortVideoDetail?version_code=1500&os_type=1' +}; + +const DEFAULT_HEADERS = { + 'User-Agent': UA, + 'Content-Type': 'application/json', + 'Accept': 'application/json' +}; + +function init(cfg) { + try { + siteName = (cfg.skey?.split('_')[1] || cfg.skey) || (cfg.key?.split('_')[1] || cfg.key) || '围观短剧'; + siteKey = cfg.skey; + siteType = cfg.stype; + console.log(`【${siteName}】初始化完成`); + } catch (e) { + console.log(`【${siteName}】初始化失败: ${e.message}`); + } +} + +function safeJSONParse(str, defaultValue = {}) { + if (!str) return defaultValue; + if (typeof str === 'object') return str; + try { + return JSON.parse(str); + } catch (e) { + console.log(`【${siteName}】JSON解析失败: ${e.message}`); + return defaultValue; + } +} + +// 通用请求函数 +async function request(url, options = {}) { + console.log(`【${siteName}】${options.method || 'GET'} ${url.split('?')[0]}`); + + const baseOptions = { + method: options.method || 'GET', + headers: { ...DEFAULT_HEADERS, ...options.headers }, + timeout: options.timeout || 15000 + }; + + let dataStr = ''; + const requestData = options.body || options.data; + if (requestData) { + dataStr = typeof requestData === 'string' ? requestData : JSON.stringify(requestData); + baseOptions.headers['Content-Type'] = baseOptions.headers['Content-Type'] || 'application/json'; + } + + for (let key of ['data', 'body']) { + try { + const reqOptions = { ...baseOptions, [key]: dataStr }; + const response = await req(url, reqOptions); + const content = response?.content || response?.data || response; + + if (!content) continue; + + const data = typeof content === 'object' ? content : safeJSONParse(content); + + if (data?.code && data.code !== 200) continue; + + console.log(`【${siteName}】${key}成功: ${JSON.stringify(data).substring(0, 300)}`); + return data; + } catch (e) { + console.log(`【${siteName}】${key}异常: ${e.message}`); + } + } + + console.log(`【${siteName}】全部失败`); + return null; +} + +async function home() { + let classes = [{ type_name: '全部', type_id: '全部' }]; + let filters = { '全部': [] }; + + const data = await request(`${rule.host}${rule.tagsUrl}`); + + if (data && data.code === 200) { + // 分类 + if (data.audiences && Array.isArray(data.audiences)) { + const audienceClasses = data.audiences.map(audience => ({ + type_name: audience, + type_id: audience + })); + classes.push(...audienceClasses); + + // 为每个分类创建筛选 + for (const audience of data.audiences) { + filters[audience] = []; + + if (data.tags && Array.isArray(data.tags) && data.tags.length > 0) { + const tagValues = [{ n: '全部', v: '' }]; + data.tags.forEach(tag => { + if (tag) tagValues.push({ n: tag, v: tag }); + }); + if (tagValues.length > 1) { + filters[audience].push({ key: 'tag', name: '标签', value: tagValues }); + } + } + + if (data.orders && Array.isArray(data.orders) && data.orders.length > 0) { + const orderValues = data.orders.map(order => ({ n: order, v: order })); + filters[audience].push({ key: 'order', name: '排序', value: orderValues }); + } + } + } + } + + return JSON.stringify({ class: classes, filters: filters }); +} + +async function homeVod() { + try { + const result = await category('全部', 1, {}, {}); + const data = typeof result === 'string' ? safeJSONParse(result) : result; + const list = (data && data.list) ? data.list.slice(0, 12) : []; + return JSON.stringify({ list: list }); + } catch (e) { + console.log(`【${siteName}】homeVod失败: ${e.message}`); + return JSON.stringify({ list: [] }); + } +} + +async function category(tid, pg, filter, extend) { + const videos = []; + const page = pg || 1; + + const tag = (extend && extend.tag) ? extend.tag : ""; + const order = (extend && extend.order) ? extend.order : ""; + + const orderMap = { "最热": "hot", "最新": "new", "热度": "hot", "时间": "new" }; + let orderValue = orderMap[order] || ""; + + const postData = { + audience: tid === '全部' ? "" : tid, + page: page, + pageSize: 30, + searchWord: "", + subject: tag, + order: orderValue + }; + + console.log(`【${siteName}】分类请求: tid=${tid}, tag=${tag}, order=${order}`); + + const data = await request(`${rule.host}${rule.searchUrl}`, { + method: 'POST', + body: postData + }); + + if (data && data.code === 200 && data.data && Array.isArray(data.data)) { + for (const it of data.data) { + if (it && it.oneId) { + videos.push({ + vod_id: String(it.oneId), + vod_name: it.title || '未知标题', + vod_pic: it.vertPoster || it.horizonPoster || '', + vod_remarks: `集数:${it.episodeCount || 0} 播放:${it.viewCount || 0}`, + vod_content: it.description || '', + vod_year: it.publishDate || '' + }); + } + } + } else { + console.log(`【${siteName}】分类数据异常:`, data?.code); + } + + console.log(`【${siteName}】分类获取到 ${videos.length} 条数据`); + + return JSON.stringify({ + list: videos, + page: page, + pagecount: page + 1, + limit: videos.length, + total: videos.length * (page + 1) + }); +} + +async function detail(id) { + if (!id) return JSON.stringify({ list: [] }); + + const url = `${rule.host}${rule.detailUrl}&oneId=${encodeURIComponent(id)}&page=1&pageSize=1000`; + const data = await request(url); + + if (data && data.code === 200 && data.data && Array.isArray(data.data)) { + const episodes = data.data.filter(ep => ep); + const firstEpisode = episodes[0] || {}; + + const playItems = []; + for (let i = 0; i < episodes.length; i++) { + const episode = episodes[i]; + let playSetting = episode.playSetting || episode.videoClarityList || []; + + if (typeof playSetting === 'string') { + playSetting = safeJSONParse(playSetting, []); + } + if (!Array.isArray(playSetting)) playSetting = []; + + const clarityInfo = {}; + for (const item of playSetting) { + if (item && item.url && item.name) { + clarityInfo[item.name] = item.url; + } + } + + if (Object.keys(clarityInfo).length > 0) { + const episodeNum = episode.playOrder || episode.episodeNumber || (i + 1); + playItems.push(`第${episodeNum}集$${JSON.stringify(clarityInfo)}`); + } + } + + const playUrl = playItems.join('#'); + if (!playUrl) return JSON.stringify({ list: [] }); + + const vod = { + vod_id: String(id), + vod_name: firstEpisode.title || '未知剧名', + vod_pic: firstEpisode.vertPoster || firstEpisode.horizonPoster || '', + vod_remarks: `共${episodes.length}集`, + vod_content: firstEpisode.description || '', + vod_play_from: '围观短剧', + vod_play_url: playUrl + }; + + return JSON.stringify({ list: [vod] }); + } + + return JSON.stringify({ list: [] }); +} + +async function play(flag, id, flags) { + let clarityInfo = {}; + + try { + if (typeof id === 'object') { + clarityInfo = id; + } else if (typeof id === 'string') { + clarityInfo = safeJSONParse(id, {}); + } + } catch (e) { + return JSON.stringify({ parse: 0, url: id || '', msg: '播放参数错误' }); + } + + const clarityOrder = ['4K', '超清', '1080P', '高清', '720P', '流畅', '480P']; + const urls = []; + const added = new Set(); + + for (const clarity of clarityOrder) { + const url = clarityInfo[clarity]; + if (url && typeof url === 'string' && url.startsWith('http') && !added.has(clarity)) { + urls.push(clarity, url); + added.add(clarity); + } + } + + if (urls.length > 0) { + return JSON.stringify({ + parse: 0, + url: urls, + header: { 'User-Agent': UA } + }); + } + + if (typeof id === 'string' && id.startsWith('http')) { + return JSON.stringify({ parse: 0, url: id, header: { 'User-Agent': UA } }); + } + + return JSON.stringify({ parse: 0, url: '', msg: '暂无可用播放地址' }); +} + +async function search(wd, quick, pg) { + if (!wd || wd.trim() === '') { + return JSON.stringify({ list: [], page: 1 }); + } + + const page = pg || 1; + const postData = { + audience: "", + page: page, + pageSize: 30, + searchWord: wd, + subject: "" + }; + + const data = await request(`${rule.host}${rule.searchUrl}`, { + method: 'POST', + body: postData, + timeout: 8000 + }); + + let videos = []; + if (data && data.code === 200 && data.data && Array.isArray(data.data)) { + videos = data.data.filter(it => it && it.oneId).map(it => ({ + vod_id: String(it.oneId), + vod_name: it.title || '未知标题', + vod_pic: it.vertPoster || it.horizonPoster || '', + vod_remarks: `集数:${it.episodeCount || 0} 播放:${it.viewCount || 0}`, + vod_content: it.description || '' + })); + } + + return JSON.stringify({ + list: videos, + page: page, + pagecount: videos.length === 30 ? page + 1 : page, + limit: videos.length, + total: videos.length * page + }); +} + +export function __jsEvalReturn() { + return { + init: init, + home: home, + homeVod: homeVod, + category: category, + detail: detail, + play: play, + search: search + }; +} \ No newline at end of file diff --git a/open/wpzy_search.js b/open/wpzy_search.js new file mode 100644 index 0000000..8830744 --- /dev/null +++ b/open/wpzy_search.js @@ -0,0 +1,436 @@ +/* +@header({ + searchable: 1, + filterable: 0, + quickSearch: 1, + title: '网盘资源[搜]', + lang: 'ds' +}) +*/ +import '../lib/htmlParser.js'; +import { Quark, Baidu, UC } from "../lib/pans.js"; + + +// ==================== 全局变量 ==================== +let siteName = '网盘资源搜索', siteKey = '', siteType = 0; + + +// ==================== 开关配置 (1=开启,0=关闭)==================== +let quarkInfinite = 0; +let quarkOrig = 1; +let quarkTransfer = 1; +let enableProxy = 0; +// ================================================ + +let UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36"; + +let headers = { + "User-Agent": UA, + "Accept": "application/json, text/plain, */*", + "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8" +}; + +let proxyurl = 'http://127.0.0.1:2525/proxy?url='; +let downThreads = '20'; + +let defaultImg = `https://cnb.cool/zhyadc/YsBox/-/git/raw/main/images/icon_cookie/网盘搜索.png`; + +let host = 'https://so.yinpai.xyz'; +let maxPages = 5; + +function safeJSONParse(str, defaultValue = {}) { + if (!str || typeof str === 'object') return str || defaultValue; + try { + return JSON.parse(str); + } catch { + return defaultValue; + } +} + +async function request(url, options = {}) { + const reqHeaders = { ...headers, ...options.headers }; + let postType = reqHeaders['Content-Type']?.includes('json') ? 'json' : + reqHeaders['Content-Type']?.includes('form') ? 'form' : ''; + + try { + const response = await req(url, { + method: options.method || 'GET', + headers: reqHeaders, + data: options.data, + postType: postType, + timeout: options.timeout || 15000 + }); + return response?.content || response?.data || response; + } catch { + return null; + } +} + +function mapResolution(res) { + const map = { + 'low': '流畅', 'normal': '标清', 'high': '高清', 'super': '超清', + '4k': '4K', '2k': '2K', 'hdr': 'HDR', 'dolby_vision': 'HDR', + 'M3U8_AUTO_480': '480P', 'M3U8_AUTO_720': '720P', 'M3U8_AUTO_1080': '1080P', + 'M3U8_AUTO_2K': '2K', 'M3U8_AUTO_4K': '4K' + }; + return map[res] || res; +} + +async function loadRemoteConfig(cfgUrl) { + try { + const res = await request(cfgUrl, { timeout: 10000 }); + if (res) { + const remoteCfg = safeJSONParse(res); + if (remoteCfg.quark_cookie?.length > 10) Quark.cookie = remoteCfg.quark_cookie; + if (remoteCfg.baidu_cookie?.length > 10) Baidu.cookie = remoteCfg.baidu_cookie; + if (remoteCfg.uc_cookie?.length > 10) UC.cookie = remoteCfg.uc_cookie; + if (remoteCfg.uc_token?.length > 10) UC.token = remoteCfg.uc_token; + if (remoteCfg.threads) downThreads = remoteCfg.threads; + if (remoteCfg.enableProxy !== undefined) enableProxy = remoteCfg.enableProxy; + if (remoteCfg.infinite !== undefined) quarkInfinite = remoteCfg.infinite; + if (remoteCfg.quark_original !== undefined) quarkOrig = remoteCfg.quark_original; + if (remoteCfg.quark_transfer !== undefined) quarkTransfer = remoteCfg.quark_transfer; + return true; + } + } catch (e) {} + return false; +} + +async function init(cfg) { + siteName = cfg.skey?.split('_')[1] || cfg.skey || siteName; + siteKey = cfg.skey; + siteType = cfg.stype; + + let ext = cfg.ext || cfg; + if (typeof ext === 'string') ext = decodeURIComponent(ext); + + if (typeof ext === 'string') { + await loadRemoteConfig(ext); + } +} + +function home() { + return JSON.stringify({ + class: [ + { type_id: 'search', type_name: '这是一个搜索源' } + ], + }); +} + +async function homeVod() { + return await category('search', 1, null, null); +} + +async function category(tid, pg, filter, ext) { + if (tid === 'search') { + return JSON.stringify({ + list: [{ + vod_id: 'only_search', + vod_name: '🔍 网盘资源搜索', + vod_pic: defaultImg, + }] + }); + } + return JSON.stringify({ list: [] }); +} + +async function detail(id) { + let detailObj; + try { + let decoded = decodeURIComponent(id); + detailObj = safeJSONParse(decoded); + } catch (e) { + let parts = id.split('|'); + detailObj = { + rid: parts[0], + name: parts[1] || '网盘资源', + panName: parts[2] || '夸克网盘' + }; + } + + let resourceId = detailObj.rid; + let vod_name = detailObj.name; + + let play_from = []; + let play_url = []; + let play_pic = []; + + let apiUrl = `${host}/api.php?ids=${resourceId}`; + let response = await request(apiUrl, { headers: headers, timeout: 15000 }); + + let shareUrl = null; + if (response) { + try { + let data = safeJSONParse(response); + if (data.list && data.list[0]) { + let item = data.list[0]; + shareUrl = item.vod_play_url?.split('$')?.[1] || ''; + vod_name = item.vod_name || vod_name; + } + } catch (e) {} + } + + if (shareUrl && shareUrl.startsWith('http')) { + let counters = { '夸克': 1, '百度': 1, '优汐': 1 }; + + if (/\.quark|pan\.quark/.test(shareUrl)) { + let shareData = Quark.getShareData(shareUrl); + if (shareData) { + let files = await Quark.getFilesByShareUrl(shareData); + if (files?.length) { + let url = files.map(v => { + let size = v.size ? ` 【${formatSize(v.size)}】` : ''; + return `${v.file_name}${size}$${[shareData.shareId, v.stoken, v.fid, v.share_fid_token, v.pdir_fid || '', v.subtitle?.fid || '', v.subtitle?.share_fid_token || ''].join('*')}`; + }).join('#'); + let imgs = files.map(v => v.thumbnail || v.thumb || v.pic || '').join('#'); + play_from.push(`夸克#${counters['夸克']++}`); + play_url.push(url); + play_pic.push(imgs); + } + } + } + else if (/\.baidu|pan\.baidu/.test(shareUrl)) { + let shareData = Baidu.getShareData(shareUrl); + if (shareData) { + let files = await Baidu.getFilesByShareUrl(shareData); + if (files?.length) { + let url = files.map(v => { + let size = v.size ? ` 【${formatSize(v.size)}】` : ''; + let info = { surl: shareData.surl, pwd: shareData.pwd || '' }; + return `${v.name}${size}$${[v.path, v.uk, v.shareid, v.fsid, JSON.stringify(info)].join('*')}`; + }).join('#'); + let imgs = files.map(v => v.thumbnail || v.thumb || v.pic || '').join('#'); + play_from.push(`百度#${counters['百度']++}`); + play_url.push(url); + play_pic.push(imgs); + } + } + } + else if (/\.uc|drive\.uc/.test(shareUrl)) { + let shareData = UC.getShareData(shareUrl); + if (shareData) { + let files = await UC.getFilesByShareUrl(shareData); + if (files?.length) { + let url = files.map(v => { + let size = v.size ? ` 【${formatSize(v.size)}】` : ''; + return `${v.file_name}${size}$${[shareData.shareId, v.stoken, v.fid, v.share_fid_token, v.subtitle?.fid || '', v.subtitle?.share_fid_token || ''].join('*')}`; + }).join('#'); + let imgs = files.map(v => v.thumbnail || v.thumb || v.pic || '').join('#'); + play_from.push(`优汐#${counters['优汐']++}`); + play_url.push(url); + play_pic.push(imgs); + } + } + } + } + + if (play_from.length === 0) { + play_from.push('提示'); + play_url.push(shareUrl ? `分享链接: ${shareUrl}` : `获取分享链接失败,资源ID: ${resourceId}`); + play_pic.push(''); + } + + let contentParts = [`资源ID: ${resourceId}`]; + if (shareUrl) { + contentParts.push(`网盘链接: ${shareUrl}`); + } + let vod_content = contentParts.join(' | '); + + let vod = { + vod_id: id, + vod_name: vod_name, + vod_pic: '', + vod_content: vod_content, + vod_remarks: play_from.length > 0 ? (play_from[0].split('#')[0] + '可播放') : '解析失败', + vod_play_from: play_from.join('$$$'), + vod_play_url: play_url.join('$$$'), + vod_play_pic: play_pic.join('$$$'), + vod_play_pic_ratio: 1.0 + }; + + return JSON.stringify({ list: [vod] }); +} + +async function search(wd, quick, pg) { + let page = pg || 1; + + const typeMap = [ + { type: 'baidu', name: '百度网盘', icon: 'https://cnb.cool/zhyadc/YsBox/-/git/raw/main/images/icon_cookie/百度.png' }, + { type: 'uc', name: '优汐网盘', icon: 'https://cnb.cool/zhyadc/YsBox/-/git/raw/main/images/icon_cookie/优汐.png' }, + { type: 'quark', name: '夸克网盘', icon: 'https://cnb.cool/zhyadc/YsBox/-/git/raw/main/images/icon_cookie/夸克.png' } + ]; + + let urls = []; + for (let t of typeMap) { + urls.push({ + url: `${host}/api.php?type=${t.type}&wd=${encodeURIComponent(wd)}`, + options: { headers: headers, timeout: 15000 } + }); + } + + let results = await batchFetch(urls); + let allList = []; + let seenIds = new Set(); + + for (let idx = 0; idx < results.length; idx++) { + let content = results[idx]; + if (!content) continue; + + let typeInfo = typeMap[idx]; + + try { + let data = safeJSONParse(content); + if (data.list && Array.isArray(data.list)) { + for (let item of data.list) { + let title = item.vod_name; + if (!title || title.length < 2) continue; + + if (seenIds.has(item.vod_id)) continue; + seenIds.add(item.vod_id); + + let detailObj = { + rid: item.vod_id, + name: title, + panName: typeInfo.name + }; + let detailId = encodeURIComponent(JSON.stringify(detailObj)); + + allList.push({ + vod_id: detailId, + vod_name: title, + vod_pic: typeInfo.icon, + vod_content: item.vod_remarks || '', + vod_remarks: typeInfo.name + }); + } + } + } catch (e) {} + } + + let filteredList = allList.filter(item => new RegExp(wd, "i").test(item.vod_name)); + + return JSON.stringify({ + page: page, + pagecount: maxPages, + limit: filteredList.length, + total: filteredList.length, + list: filteredList + }); +} + +async function play(flag, id, flags) { + let ids = id.split('*'); + let urls = []; + + const addUrl = (name, u) => { + if (u) urls.push(name, proxyUrl(u) + `&thread=${downThreads}`); + }; + + if (flag.startsWith('夸克') && ids.length >= 4) { + let [shareId, stoken, fid, share_fid_token] = ids; + let header = { + 'User-Agent': headers['User-Agent'], + 'origin': 'https://pan.quark.cn', + 'referer': 'https://pan.quark.cn/', + 'Cookie': Quark.cookie + }; + + if (quarkInfinite == 1) { + const tokenUrls = await Quark.getUrl(shareId, stoken, fid, share_fid_token); + tokenUrls?.forEach(item => { if (item?.url) addUrl("无限" + (item.name || ''), item.url); }); + } + + let shouldTransfer = quarkTransfer; + if (quarkInfinite != 1 && quarkOrig != 1 && quarkTransfer != 1) { + shouldTransfer = 1; + } + + if (shouldTransfer == 1) { + if (quarkOrig == 1) { + let down = await Quark.getDownload(shareId, stoken, fid, share_fid_token, true); + if (down && down.error) { + const errorMsg = down.message || '未知错误'; + return JSON.stringify({ parse: 0, msg: `夸克: ${errorMsg}`, header: header }); + } + if (down?.download_url) addUrl("原画", down.download_url); + } + + let transcoding = await Quark.getLiveTranscoding(shareId, stoken, fid, share_fid_token); + if (transcoding?.length) { + transcoding.filter(t => t.accessable).forEach(t => { + if (t?.video_info?.url) { + addUrl(mapResolution(t.resolution), t.video_info.url); + } + }); + } + } + + if (urls.length) { + return JSON.stringify({ parse: 0, url: urls, header: header }); + } + } + + if (flag.startsWith('百度') && ids.length >= 5) { + let [path, uk, shareid, fsid, shareDataStr] = ids; + let shareData = safeJSONParse(shareDataStr); + + let original = await Baidu.getAppShareUrl(path, uk, shareid, fsid, shareData); + if (original && !original.error) { + addUrl("原画", original); + } else { + const errorMsg = original?.message || '获取链接失败,请检查Cookie是否有效'; + return JSON.stringify({ parse: 0, msg: `百度: ${errorMsg}`, header: headers }); + } + + if (urls.length) { + return JSON.stringify({ + parse: 0, + url: urls, + header: { "User-Agent": 'netdisk;P2SP;2.2.91.136;android-android;' } + }); + } + } + + if (flag.startsWith('优汐') && ids.length >= 4) { + let [shareId, stoken, fid, share_fid_token] = ids; + + let down = await UC.getDownload(shareId, stoken, fid, share_fid_token, true); + if (down && down.error) { + const errorMsg = down.message || '未知错误'; + return JSON.stringify({ parse: 0, msg: `优汐: ${errorMsg}`, header: headers }); + } + if (down?.length) { + down.forEach(item => { if (item?.url) addUrl(mapResolution(item.name), item.url); }); + } + + if (urls.length) { + return JSON.stringify({ parse: 0, url: urls }); + } + } + + return JSON.stringify({ parse: 0, msg: `解析分享链接失败, 分享链接可能已失效`, header: headers }); +} + +function formatSize(bytes) { + let size = typeof bytes === 'string' ? parseInt(bytes, 10) : bytes; + if (!size || isNaN(size) || size <= 0) return '0 B'; + const units = ['B', 'KB', 'MB', 'GB', 'TB']; + let i = 0; + while (size >= 1024 && i < units.length - 1) { size /= 1024; i++; } + return size.toFixed(2) + ' ' + units[i]; +} + +function proxyUrl(url) { + if (!url) return ''; + if (enableProxy && proxyurl) return proxyurl + encodeURIComponent(url); + return url; +} + +async function proxy(params) { + let url = params.url; + return [302, '', '', { 'Location': url }]; +} + +export function __jsEvalReturn() { + return { init, home, homeVod, category, detail, play, search, proxy }; +} \ No newline at end of file diff --git a/open/ximalaya_ting.js b/open/ximalaya_ting.js new file mode 100644 index 0000000..b438322 --- /dev/null +++ b/open/ximalaya_ting.js @@ -0,0 +1,215 @@ +/* +@header({ + searchable: 1, + filterable: 1, + quickSearch: 1, + title: '喜马拉雅[听]', + lang: 'cat', +}) +*/ +let host = 'https://m.ximalaya.com'; +let searchHost = 'https://api.cenguigui.cn'; + +let siteName = '喜马拉雅', siteKey = '', siteType = 0; + +let UA = "Mozilla/5.0 (Linux; Android 10; Mobile) AppleWebKit/537.36 (Chrome/91.0.4472.120 Mobile Safari/537.36)"; + +const headers = { + 'User-Agent': UA, + 'Accept': 'application/json, text/plain, */*' +}; + +function init(cfg) { + siteName = cfg.skey?.split('_')[1] || cfg.skey || '喜马拉雅'; + siteKey = cfg.skey; + siteType = cfg.stype; + + if (cfg && typeof cfg === 'string') { + host = cfg; + } else if (cfg && typeof cfg === 'object') { + const ext = cfg.ext; + if (ext) { + host = ext.host || ext.hosturl || ext.url || ext.site || host; + } + } +} + +function safeJSONParse(str, defaultValue = {}) { + if (!str || typeof str === 'object') return str || defaultValue; + try { + return JSON.parse(str); + } catch { + return defaultValue; + } +} + +async function request(url, options = {}) { + const reqHeaders = { ...headers, ...options.headers }; + let postType = reqHeaders['Content-Type']?.includes('json') ? 'json' : + reqHeaders['Content-Type']?.includes('form') ? 'form' : ''; + + try { + const response = await req(url, { + method: options.method || 'GET', + headers: reqHeaders, + data: options.data, + postType: postType, + timeout: options.timeout || 15000 + }); + return response?.content || response?.data || response; + } catch { + return null; + } +} + +async function home(filter) { + const classNames = '有声书&儿童&音乐&相声&娱乐&广播剧&历史&外语'; + const classUrls = 'youshengshu&ertong&yinyue&xiangsheng&yule&guangbojv&lishi&waiyu'; + + const names = classNames.split('&'); + const urls = classUrls.split('&'); + + const classes = []; + for (let i = 0; i < names.length && i < urls.length; i++) { + classes.push({ type_id: urls[i], type_name: names[i] }); + } + + const filters = {}; + classes.forEach(cls => { filters[cls.type_id] = []; }); + + return JSON.stringify({ class: classes, filters: filters }); +} + +async function homeVod() { + return await category('youshengshu', 1, null, {}); +} + +async function category(tid, pg, filter, extend) { + const page = pg || 1; + + try { + const url = `${host}/m-revision/page/category/queryCategoryAlbumsByPage?sort=0&pageSize=50&page=${page}&categoryCode=${tid}`; + const html = await request(url); + + if (!html) { + return JSON.stringify({ list: [], page: page, pagecount: 1, limit: 50, total: 0 }); + } + + const data = safeJSONParse(html).data; + const albumList = data.albumBriefDetailInfos || []; + const videos = []; + + albumList.forEach(it => { + const vip = it.albumInfo?.albumVipPayType; + if (vip === 0) { + const id = `http://mobile.ximalaya.com/mobile/others/ca/album/track/${it.id}/true/0/200?albumId=${it.id}`; + videos.push({ + vod_id: id, + vod_name: it.albumInfo?.title || '未知标题', + vod_pic: `http://imagev2.xmcdn.com/${it.albumInfo?.cover}`, + vod_remarks: '免费' + }); + } + }); + + return JSON.stringify({ list: videos, page: page, pagecount: Math.ceil(videos.length / 50) + 1, limit: 50, total: videos.length }); + } catch (e) { + return JSON.stringify({ list: [], page: page, pagecount: 1, limit: 50, total: 0 }); + } +} + +async function detail(id) { + try { + const urls = []; + const albumIdMatch = id.match(/albumId=(\d+)/); + const albumId = albumIdMatch ? albumIdMatch[1] : ''; + + const html = await request(id); + const json = safeJSONParse(html); + const album = json.album || {}; + let data = json.tracks?.list || []; + const maxPageId = json.tracks?.maxPageId || 1; + + data.forEach(it => { + if (it.playPathAacv164) { + urls.push(`${it.title}$${it.playPathAacv164}`); + } + }); + + if (maxPageId > 1) { + for (let j = 2; j <= maxPageId; j++) { + const pageUrl = id.replace('/0/', `/${j}/`); + try { + const pageHtml = await request(pageUrl); + const pageJson = safeJSONParse(pageHtml); + const pageData = pageJson.tracks?.list || []; + pageData.forEach(it => { + if (it.playPathAacv164) { + urls.push(`${it.title}$${it.playPathAacv164}`); + } + }); + } catch (e) {} + } + } + + if (urls.length === 0) return JSON.stringify({ list: [] }); + + const vod = { + vod_id: id, + vod_name: album.title || '暂无名称', + vod_pic: album.coverLarge || '暂无图片', + vod_content: album.intro || '暂无简介', + vod_remarks: `共${urls.length}集`, + vod_play_from: '喜马拉雅', + vod_play_url: urls.join('#') + }; + + return JSON.stringify({ list: [vod] }); + } catch (e) { + return JSON.stringify({ list: [] }); + } +} + +async function play(flag, id, flags) { + if (!id) { + return JSON.stringify({ parse: 0, jx: 0, url: '', msg: '播放地址为空', header: {} }); + } + return JSON.stringify({ parse: 0, jx: 0, url: id, header: {} }); +} + +async function search(wd, quick, pg = "1") { + const page = parseInt(pg) || 1; + + try { + const url = `${searchHost}/api/music/ximalaya.php?name=${encodeURIComponent(wd)}`; + const html = await request(url); + + if (!html) { + return JSON.stringify({ list: [], page: page, pagecount: 0, limit: 20, total: 0 }); + } + + const data = safeJSONParse(html).data; + + if (!Array.isArray(data)) { + return JSON.stringify({ list: [], page: page, pagecount: 0, limit: 20, total: 0 }); + } + + const videos = data.map(it => { + const id = `http://mobile.ximalaya.com/mobile/others/ca/album/track/${it.albumId}/true/0/200?albumId=${it.albumId}`; + return { + vod_id: id, + vod_name: it.title || '未知标题', + vod_pic: it.cover || '', + vod_remarks: '喜马拉雅' + }; + }).filter(v => v.vod_id); + + return JSON.stringify({ list: videos, page: page, pagecount: 1, limit: videos.length, total: videos.length }); + } catch (e) { + return JSON.stringify({ list: [], page: page, pagecount: 0, limit: 20, total: 0 }); + } +} + +export function __jsEvalReturn() { + return { init, home, homeVod, category, detail, play, search }; +} \ No newline at end of file diff --git a/open/yangshi_juhe.js b/open/yangshi_juhe.js new file mode 100644 index 0000000..09ff1c7 --- /dev/null +++ b/open/yangshi_juhe.js @@ -0,0 +1,223 @@ +/* +@header({ + searchable: 1, + filterable: 1, + quickSearch: 1, + title: '聚合央视[视]', + lang: 'cat' +}) +*/ + +let siteName = '聚合央视', siteKey = '', siteType = 0; + +const platformList = [ + { name: '央视新闻', id: 'xinwen' }, + { name: '央视聚场', id: 'juchang' }, + { name: '央视大全', id: 'quan' } +]; + +const headers = { + 'User-Agent': 'Mozilla/5.0 (Linux; Android 11; M2007J3SC Build/RKQ1.200826.002; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/77.0.3865.120 MQQBrowser/6.2 TBS/045714 Mobile Safari/537.36' +}; + +const rule = { + xinwen: { host: 'http://api.cntv.cn', videoList: '/NewVideo/getVideoListByColumn', playUrl: 'https://cntv.playdreamer.cn/proxy/asp/hls/850/0303000a/3/default/' }, + juchang: { host: 'http://api.cntv.cn', videoList: '/NewVideo/getVideoListByColumn', playUrl: 'https://cntv.playdreamer.cn/proxy/asp/hls/850/0303000a/3/default/' }, + quan: { host: 'https://api.cntv.cn', columnSearch: '/lanmu/columnSearch', videoAlbum: '/list/getVideoAlbumList', albumDetail: '/NewVideo/getVideoListByAlbumIdNew', videoInfo: '/video/videoinfoByGuid', playUrl: 'https://cntv.playdreamer.cn/proxy/asp/hls/850/0303000a/3/default/' } +}; + +const filterOptions = { + xinwen: [{ key: "area", name: "分类", value: [{ "n": "新闻直播间", "v": "TOPC1451559129520755" }, { "n": "中国新闻", "v": "TOPC1451539894330405" }, { "n": "朝闻天下", "v": "TOPC1451558496100826" }, { "n": "新闻联播", "v": "TOPC1451528971114112" }, { "n": "晚间新闻", "v": "TOPC1451528792881669" }, { "n": "午夜新闻", "v": "TOPC1451558779639282" }, { "n": "新闻30分", "v": "TOPC1451559097947700" }, { "n": "24小时", "v": "TOPC1451558428005729" }, { "n": "新闻1+1", "v": "TOPC1451559066181661" }, { "n": "海峡两岸", "v": "TOPC1451540328102649" }, { "n": "今日关注", "v": "TOPC1451540389082713" }, { "n": "今日亚洲", "v": "TOPC1451540448405749" }, { "n": "今日环球", "v": "TOPC1571034705435323" }, { "n": "新闻调查", "v": "TOPC1451558819463311" }, { "n": "军事报道", "v": "TOPC1451527941788652" }, { "n": "经济信息联播", "v": "TOPC1451533782742171" }, { "n": "体坛快讯", "v": "TOPC1451550970356385" }, { "n": "焦点访谈", "v": "TOPC1451558976694518" }, { "n": "东方时空", "v": "TOPC1451558532019883" }, { "n": "新闻周刊", "v": "TOPC1451559180488841" }, { "n": "一线", "v": "TOPC1451543462858283" }] }], + juchang: [{ key: "area", name: "分类", value: [{ "n": "动画大放映", "v": "TOPC1451559025546574" }, { "n": "第一动画乐园", "v": "TOPC1451378857272262" }, { "n": "探索·发现", "v": "TOPC1451557893544236" }, { "n": "动物世界", "v": "TOPC1451378967257534" }, { "n": "人与自然", "v": "TOPC1451525103989666" }, { "n": "自然传奇", "v": "TOPC1451558150787467" }, { "n": "地理·中国", "v": "TOPC1451557421544786" }, { "n": "健康之路", "v": "TOPC1451557646802924" }, { "n": "百家讲坛", "v": "TOPC1451557052519584" }, { "n": "走进科学", "v": "TOPC1451558190239536" }, { "n": "是真的吗", "v": "TOPC1451534366388377" }, { "n": "故事里的中国", "v": "TOPC1451464884159276" }, { "n": "远方的家", "v": "TOPC1451541349400938" }, { "n": "跟着书本去旅行", "v": "TOPC1575253587571324" }, { "n": "今日说法", "v": "TOPC1451464665008914" }, { "n": "开讲啦", "v": "TOPC1451464884159276" }, { "n": "天网", "v": "TOPC1451530382483536" }, { "n": "高端访谈", "v": "TOPC1665739007799851" }, { "n": "对话", "v": "TOPC1514182710380601" }, { "n": "面对面", "v": "TOPC1451559038345600" }, { "n": "等着我", "v": "TOPC1451378757637200" }, { "n": "空中剧院", "v": "TOPC1451558856402351" }, { "n": "精彩音乐汇", "v": "TOPC1451541414450906" }, { "n": "音乐厅", "v": "TOPC1451534421925242" }, { "n": "民歌·中国", "v": "TOPC1451541994820527" }, { "n": "中国电影报道", "v": "TOPC1451354597100320" }, { "n": "星光大道", "v": "TOPC1451467630488780" }, { "n": "星推荐", "v": "TOPC1451469943519994" }, { "n": "方圆剧阵", "v": "TOPC1571217727564820" }, { "n": "正大综艺", "v": "TOPC1650782829200997" }, { "n": "第一时间", "v": "TOPC1451530259915198" }, { "n": "共同关注", "v": "TOPC1451558858788377" }, { "n": "经济半小时", "v": "TOPC1601362002656197" }, { "n": "经济大讲堂", "v": "TOPC1451533652476962" }, { "n": "正点财经", "v": "TOPC1453100395512779" }, { "n": "开门大吉", "v": "TOPC1451465894294259" }, { "n": "生活圈", "v": "TOPC1451546588784893" }, { "n": "生活提示", "v": "TOPC1451526037568184" }] }], + quan: [{ key: "area", name: "分类", value: [{ "n": "栏目大全", "v": "栏目大全" }, { "n": "特别节目", "v": "特别节目" }, { "n": "纪录片", "v": "纪录片" }, { "n": "电视剧", "v": "电视剧" }, { "n": "动画片", "v": "动画片" }] }] +}; + +const ruleFilterDef = { + xinwen: { area: 'TOPC1451559129520755' }, + juchang: { area: 'TOPC1451559025546574' }, + quan: { area: '栏目大全' } +}; + +function init(cfg) { + siteName = cfg.skey?.split('_')[1] || cfg.skey || '聚合央视'; + siteKey = cfg.skey; + siteType = cfg.stype; +} + +function safeJSONParse(str, defaultValue = {}) { + if (!str || typeof str === 'object') return str || defaultValue; + try { + return JSON.parse(str); + } catch { + return defaultValue; + } +} + +async function request(url, options = {}) { + const reqHeaders = { ...headers, ...options.headers }; + let postType = reqHeaders['Content-Type']?.includes('json') ? 'json' : + reqHeaders['Content-Type']?.includes('form') ? 'form' : ''; + + try { + const response = await req(url, { + method: options.method || 'GET', + headers: reqHeaders, + data: options.data, + postType: postType, + timeout: options.timeout || 15000 + }); + return response?.content || response?.data || response; + } catch { + return null; + } +} + +function getPlatList() { return platformList; } + +async function getXinwenList(typeId, page) { + let videos = []; + try { + const url = `${rule.xinwen.host}${rule.xinwen.videoList}?id=${typeId}&n=10&sort=desc&p=${page}&mode=0&serviceId=tvcctv`; + const html = await request(url); + const data = safeJSONParse(html); + const list = data.data?.list || []; + videos = list.map(item => ({ vod_id: `xinwen@${item.guid}`, vod_name: item.title || '未知视频', vod_pic: item.image || '', vod_remarks: `央视新闻 | ${item.time || ''}`, vod_content: '' })); + } catch (e) {} + return videos; +} + +async function getJuchangList(typeId, page) { + let videos = []; + try { + const url = `${rule.juchang.host}${rule.juchang.videoList}?id=${typeId}&n=10&sort=desc&p=${page}&mode=0&serviceId=tvcctv`; + const html = await request(url); + const data = safeJSONParse(html); + const list = data.data?.list || []; + videos = list.map(item => ({ vod_id: `juchang@${item.guid}`, vod_name: item.title || '未知视频', vod_pic: item.image || '', vod_remarks: `央视聚场 | ${item.time || ''}`, vod_content: '' })); + } catch (e) {} + return videos; +} + +async function getQuanList(typeId, page) { + let videos = []; + try { + const channelMap = { "特别节目": "CHAL1460955953877151", "纪录片": "CHAL1460955924871139", "电视剧": "CHAL1460955853485115", "动画片": "CHAL1460955899450127" }; + + if (typeId === '栏目大全') { + const url = `${rule.quan.host}${rule.quan.columnSearch}?p=${page}&n=20&serviceId=tvcctv&t=json`; + const html = await request(url); + const data = safeJSONParse(html); + const docs = data.response?.docs || []; + videos = docs.map(item => ({ vod_id: `quan@${item.lastVIDE?.videoSharedCode}|${item.column_firstclass}|${item.column_name}|${item.channel_name}|${item.column_brief}|${item.column_logo}|${item.lastVIDE?.videoTitle}|栏目大全`, vod_name: item.column_name || '未知栏目', vod_pic: item.column_logo || '', vod_remarks: `央视大全 | ${item.channel_name || ''}`, vod_content: item.column_brief || '' })); + } else { + const params = { p: page, n: 24, serviceId: 'tvcctv', t: 'json', channelid: channelMap[typeId] || '', fc: encodeURIComponent(typeId) }; + const queryString = Object.keys(params).map(key => `${key}=${params[key]}`).join('&'); + const url = `${rule.quan.host}${rule.quan.videoAlbum}?${queryString}`; + const html = await request(url); + const data = safeJSONParse(html); + const list = data.data?.list || []; + videos = list.map(item => ({ vod_id: `quan@${item.id}|${item.sc}|${item.title}|${item.channel}|${item.brief}|${item.image}|${item.count}|${typeId}`, vod_name: item.title || '未知视频', vod_pic: item.image || '', vod_remarks: `央视大全 | ${item.sc || ''}${item.year ? '·' + item.year : ''}${item.area ? '·' + item.area : ''}`, vod_content: item.brief || '' })); + } + } catch (e) {} + return videos; +} + +async function getXinwenDetail(id) { + return { vod_id: id, vod_name: '', vod_remarks: '', vod_play_from: '央视新闻', vod_play_url: `点击播放$${id}` }; +} + +async function getJuchangDetail(id) { + return { vod_id: id, vod_name: '', vod_remarks: '', vod_play_from: '央视聚场', vod_play_url: `点击播放$${id}` }; +} + +async function getQuanDetail(did) { + let vod = {}; + try { + const info = did.split("|"); + const cate = info[7]; + const ctid = info[0]; + const modeMap = { "特别节目": "0", "纪录片": "0", "电视剧": "0", "动画片": "1" }; + const mode = modeMap[cate] || '0'; + const albumUrl = `${rule.quan.host}${rule.quan.albumDetail}?id=${ctid}&serviceId=tvcctv&p=1&n=100&mode=${mode}&pub=1`; + const html = await request(albumUrl); + const data = safeJSONParse(html); + let playUrls = []; + if (data.errcode === '1001') { + const videoInfoUrl = `${rule.quan.host}${rule.quan.videoInfo}?guid=${ctid}&serviceId=tvcctv`; + const vInfoRes = await request(videoInfoUrl); + const vInfoData = safeJSONParse(vInfoRes); + const realCtid = vInfoData.ctid; + const columnUrl = `${rule.quan.host}/NewVideo/getVideoListByColumn?id=${realCtid}&d=&p=1&n=100&sort=desc&mode=0&serviceId=tvcctv&t=json`; + const colRes = await request(columnUrl); + const colData = safeJSONParse(colRes); + playUrls = colData.data?.list || []; + } else { + playUrls = data.data?.list || []; + } + const playList = playUrls.map(item => { const title = item.title || `第${item.index || '?'}集`; const cleanTitle = title.replace(/\$/g, ''); const guid = item.guid || ''; return `${cleanTitle}$${guid}`; }); + vod = { vod_id: did, vod_name: info[2] || '', vod_pic: info[5] || '', vod_content: info[4] || '', vod_remarks: info[6] ? `共${info[6]}集` : '', vod_play_from: playList.length > 0 ? '央视大全' : '', vod_play_url: playList.length > 0 ? playList.join('#') : '' }; + } catch (e) {} + return vod; +} + +async function home(filter) { + const platForms = getPlatList(); + const classes = platForms.map(item => ({ type_name: item.name, type_id: item.id, type_flag: '[CFS][SUBSITE2][FILTERBAR]' })); + const filters = {}; + platForms.forEach(item => { if (filterOptions[item.id]) filters[item.id] = filterOptions[item.id]; }); + return JSON.stringify({ class: classes, filters: filters }); +} + +async function homeVod() { + const platForms = getPlatList(); + const randomPlat = platForms[Math.floor(Math.random() * platForms.length)]; + const randomArea = ruleFilterDef[randomPlat.id]?.area || ''; + const categoryResult = await category(randomPlat.id, 1, { area: randomArea }, {}); + const categoryList = safeJSONParse(categoryResult).list || []; + return JSON.stringify({ list: categoryList }); +} + +async function category(tid, pg, filter, extend) { + const page = pg || 1; + extend = extend || {}; + const platformItem = platformList.find(p => p.id === tid); + if (!platformItem) return JSON.stringify({ list: [], page, pagecount: 1, limit: 0, total: 0 }); + const searchKeyword = extend?.custom; + if (searchKeyword) return await cfs(tid, searchKeyword, pg); + const area = filter?.area || extend?.area || ruleFilterDef[tid]?.area || ''; + const videos = []; + switch (tid) { + case 'xinwen': videos.push(...await getXinwenList(area, page)); break; + case 'juchang': videos.push(...await getJuchangList(area, page)); break; + case 'quan': videos.push(...await getQuanList(area, page)); break; + } + return JSON.stringify({ list: videos, page: page, pagecount: page + 1, limit: videos.length, total: videos.length * (page + 1) }); +} + +async function detail(id) { + const parts = id.split('@'); + const platform = parts[0]; + const did = parts.slice(1).join('@'); + let vod = {}; + if (platform === 'xinwen') vod = await getXinwenDetail(did); + else if (platform === 'juchang') vod = await getJuchangDetail(did); + else if (platform === 'quan') vod = await getQuanDetail(did); + return JSON.stringify({ list: [vod] }); +} + +async function play(flag, id, flags) { + const playUrl = `${rule.xinwen.playUrl}${id}/850.m3u8`; + return JSON.stringify({ parse: 0, url: playUrl, header: headers }); +} + +async function cfs(siteId, wd, pg) { + return JSON.stringify({ list: [], page: pg || 1, pagecount: 1, limit: 0, total: 0 }); +} + +async function search(wd, quick, pg) { + return JSON.stringify({ list: [], page: pg || 1, pagecount: 1, limit: 0, total: 0 }); +} + +export function __jsEvalReturn() { + return { init, home, homeVod, category, detail, play, search }; +} \ No newline at end of file diff --git a/open/yinyue_juhe.js b/open/yinyue_juhe.js new file mode 100644 index 0000000..2bd350e --- /dev/null +++ b/open/yinyue_juhe.js @@ -0,0 +1,708 @@ +/* +@header({ + searchable: 1, + filterable: 1, + quickSearch: 1, + title: '聚合音乐[听]', + lang: 'cat' +}) +*/ + +let siteName = '聚合音乐', siteKey = '', siteType = 0; + +const platformList = [ + { name: '网易云音乐', id: 'wangyi' }, + { name: '听海音乐', id: 'tinghai' }, + { name: '米兔音乐', id: 'mitu' } +]; + +const QUALITY_MAP = [ + ["超高", "hrMusic"], ["无损", "sqMusic"], ["极高", "hMusic"], + ["较高", "mMusic"], ["标准", "lMusic"] +]; + +const rule = { + wangyi: { + host: 'https://music.163.com', + playApi: 'http://oiapi.net/api/Music_163', + searchApi: 'http://mc.alger.fun/api/cloudsearch', + toplist: '/api/toplist', + hotPlaylist: '/api/playlist/list', + topArtists: '/api/artist/top', + personalized: '/api/personalized/playlist', + artistDetail: '/api/artist/', + playlistDetail: '/api/playlist/detail', + songDetail: '/api/song/detail', + songLyric: '/api/song/lyric', + referer: 'https://music.163.com/' + }, + tinghai: { + host: 'http://wapi.kuwo.cn', + tagPlaylist: '/api/pc/classify/playlist/getTagPlayList', + playlistInfo: '/api/www/playlist/playListInfo', + songUrl: 'https://nmobi.kuwo.cn/mobi.s', + lyricApi: 'https://kuwo.cn/openapi/v1/www/lyric/getlyric', + searchApi: 'https://search.kuwo.cn/r.s', + picApi: 'http://artistpicserver.kuwo.cn/pic.web', + referer: 'https://kuwo.cn/' + }, + mitu: { + host: 'https://www.qqmp3.vip', + songsApi: '/api/songs.php', + kwApi: '/api/kw.php', + referer: 'https://www.qqmp3.vip/' + } +}; + +const baseHeaders = { + 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1' +}; + +const filterOptions = { + wangyi: [{ key: "area", name: "分类", value: [{ "n": "推荐歌单", "v": "recommend" }, { "n": "排行榜", "v": "toplist" }, { "n": "热门歌单", "v": "hot" }, { "n": "热门歌手", "v": "artist" }] }], + tinghai: [{ key: "area", name: "分类", value: [{ "n": "专区", "v": "12" }, { "n": "主题", "v": "2189" }, { "n": "心情", "v": "146" }, { "n": "场景", "v": "376" }, { "n": "年代", "v": "637" }, { "n": "曲风流派", "v": "393" }, { "n": "语言", "v": "37" }] }], + mitu: [{ key: "area", name: "分类", value: [{ "n": "热门", "v": "hot" }, { "n": "新歌", "v": "new" }, { "n": "随机", "v": "rand" }] }] +}; + +const ruleFilterDef = { + wangyi: { area: 'recommend' }, + tinghai: { area: '12' }, + mitu: { area: 'hot' } +}; + +function init(cfg) { + siteName = cfg.skey?.split('_')[1] || cfg.skey || '聚合音乐'; + siteKey = cfg.skey; + siteType = cfg.stype; +} + +function safeJSONParse(str, defaultValue = {}) { + if (!str || typeof str === 'object') return str || defaultValue; + try { + return JSON.parse(str); + } catch { + return defaultValue; + } +} + +async function request(url, options = {}, retries = 2, platform = null) { + let headers = { ...baseHeaders }; + if (platform && rule[platform] && rule[platform].referer) { + headers['Referer'] = rule[platform].referer; + } + if (options.headers) { + headers = { ...headers, ...options.headers }; + } + + let reqHeaders = { ...headers }; + let postType = reqHeaders['Content-Type']?.includes('json') ? 'json' : + reqHeaders['Content-Type']?.includes('form') ? 'form' : ''; + + for (let i = 0; i < retries; i++) { + try { + const response = await req(url, { + method: options.method || 'GET', + headers: reqHeaders, + data: options.data, + postType: postType, + timeout: options.timeout || 10000 + }); + const content = response?.content || response?.data || response; + if (content && content.length > 50) return content; + if (i < retries - 1) await sleep(0.5); + return content; + } catch (e) { + if (i === retries - 1) return ''; + await sleep(0.5); + } + } + return ''; +} + +function sleep(seconds) { + return new Promise(resolve => setTimeout(resolve, seconds * 1000)); +} + +function hd(img) { + if (!img) return ''; + return img.replace('/120/', '/4000/').replace('/500/', '/2160/').replace('/150/', '/1000/').replace('/300/', '/1500/'); +} + +function getPicUrl(pic, size = '500y500') { + if (!pic) return ''; + return pic + '?param=' + size; +} + +function getSongPic(song, defaultPic, size = '300y300') { + let pic = song.al?.picUrl || song.album?.picUrl || defaultPic; + if (pic) { + pic = pic.replace('?param=500y500', '?param=' + size); + pic = pic.replace('?param=300y300', '?param=' + size); + } + return pic || ''; +} + +function buildWangyiPlayData(tracks, defaultPic) { + let songPicArr = tracks.map(s => getSongPic(s, defaultPic, '300y300')); + const playPic = songPicArr.join('#'); + + const playUrl = QUALITY_MAP.map(q => + tracks.map(s => { + let songPic = getSongPic(s, defaultPic, '300y300'); + let artistName = s.ar?.map(a => a.name).join('/') || s.artists?.map(a => a.name).join('/') || ''; + let displayName = artistName ? `${s.name} - ${artistName}` : s.name; + return `${displayName}$${s.id}|${q[1]}&&${songPic}`; + }).join('#') + ).join('$$$'); + + const playFrom = QUALITY_MAP.map(q => q[0]).join('$$$'); + + return { playFrom, playUrl, playPic }; +} + +function formatNumber(num) { + if (!num) return '0'; + if (num >= 10000) return (num / 10000).toFixed(1) + '万'; + return num.toString(); +} + +function getPlatList() { + return platformList; +} + +async function getWangyiList(type, page, extend) { + const limit = 20; + const offset = (page - 1) * limit; + let videos = []; + + try { + let url = ''; + let rawData = []; + + switch (type) { + case 'recommend': + url = `${rule.wangyi.host}${rule.wangyi.personalized}?limit=${page * limit}`; + break; + case 'toplist': + url = `${rule.wangyi.host}${rule.wangyi.toplist}`; + break; + case 'hot': + const cat = extend?.cat || '全部'; + url = `${rule.wangyi.host}${rule.wangyi.hotPlaylist}?cat=${encodeURIComponent(cat)}&limit=${limit}&offset=${offset}&order=hot`; + break; + case 'artist': + url = `${rule.wangyi.host}${rule.wangyi.topArtists}?limit=${limit}&offset=${offset}`; + break; + default: + return []; + } + + const html = await request(url, {}, 2, 'wangyi'); + const json = safeJSONParse(html); + + if (type === 'recommend') { + rawData = json.result || []; + if (page > 1) rawData = rawData.slice(offset); + videos = rawData.map(it => ({ + vod_id: `wangyi@playlist@${it.id}`, + vod_name: it.name, + vod_pic: getPicUrl(it.picUrl, '300y300'), + vod_remarks: `网易云 | 🎧${formatNumber(it.playCount || 0)}`, + vod_content: '' + })); + } else if (type === 'toplist') { + rawData = json.list || []; + videos = rawData.slice(offset, offset + limit).map(it => ({ + vod_id: `wangyi@toplist@${it.id}`, + vod_name: it.name, + vod_pic: getPicUrl(it.coverImgUrl || it.picUrl, '300y300'), + vod_remarks: `网易云 | ${it.updateFrequency || `${it.trackCount || 0}首歌曲`}`, + vod_content: '' + })); + } else if (type === 'artist') { + rawData = json.artists || []; + videos = rawData.map(it => ({ + vod_id: `wangyi@artist@${it.id}`, + vod_name: it.name, + vod_pic: getPicUrl(it.img1v1Url || it.picUrl, '300y300'), + vod_remarks: `网易云 | ${it.albumSize || 0}张专辑`, + vod_content: '' + })); + } else { + rawData = json.playlists || []; + videos = rawData.map(it => ({ + vod_id: `wangyi@playlist@${it.id}`, + vod_name: it.name, + vod_pic: getPicUrl(it.coverImgUrl, '300y300'), + vod_remarks: `网易云 | ${it.playCount ? formatNumber(it.playCount) : ''}`, + vod_content: '' + })); + } + } catch (e) {} + return videos; +} + +async function getTinghaiList(categoryId, page) { + let videos = []; + try { + const url = `${rule.tinghai.host}${rule.tinghai.tagPlaylist}?pn=${page}&rn=30&id=${categoryId}`; + const html = await request(url, {}, 2, 'tinghai'); + const json = safeJSONParse(html); + const data = json.data?.data || []; + videos = data.map(item => ({ + vod_id: `tinghai@${categoryId}@${item.id || item.pid}`, + vod_name: item.name || item.title || '未命名歌单', + vod_pic: hd(item.img || item.pic || item.cover || ''), + vod_remarks: `听海音乐 | ${item.listencnt ? formatNumber(item.listencnt) : ''}`, + vod_content: item.info || item.userName || '' + })); + } catch (e) {} + return videos; +} + +async function getMituList(type, page) { + let apiPath = ''; + if (type === 'hot') apiPath = 'api/songs.php'; + else if (type === 'new') apiPath = 'api/songs.php?type=new'; + else apiPath = 'api/songs.php?type=rand'; + + const url = `${rule.mitu.host}/${apiPath}`; + let videos = []; + + try { + const html = await request(url, {}, 2, 'mitu'); + const json = safeJSONParse(html); + if (json.code === 200 && Array.isArray(json.data)) { + videos = json.data.map(item => { + const vodData = { id: item.rid, name: item.name, pic: item.pic, artist: item.artist, downurl: item.downurl || [] }; + const vodId = `mitu@${type}@${encodeURIComponent(JSON.stringify(vodData))}`; + return { + vod_id: vodId, + vod_name: `${item.name} - ${item.artist}`, + vod_pic: item.pic || '', + vod_remarks: `米兔音乐 | ${type === 'hot' ? '热门' : (type === 'new' ? '新歌' : '随机')}`, + vod_content: `歌手:${item.artist}` + }; + }); + } + } catch (e) {} + return videos; +} + +async function getWangyiDetail(type, id) { + let vod = {}; + try { + let url = ''; + let data = {}; + let tracks = []; + + if (type === 'artist') { + url = `${rule.wangyi.host}${rule.wangyi.artistDetail}${id}`; + const html = await request(url, {}, 2, 'wangyi'); + const json = safeJSONParse(html); + data = json.artist || {}; + tracks = json.hotSongs || []; + const defaultPic = getPicUrl(data.picUrl || data.img1v1Url, '500y500'); + const { playFrom, playUrl, playPic } = buildWangyiPlayData(tracks, defaultPic); + vod = { + vod_id: `wangyi@artist@${id}`, + vod_name: data.name || '未知歌手', + vod_pic: defaultPic, + vod_content: data.briefDesc || data.name, + vod_remarks: `共${tracks.length}首`, + vod_play_from: playFrom, + vod_play_url: playUrl, + vod_play_pic: playPic, + vod_play_pic_ratio: 1.0 + }; + } else { + url = `${rule.wangyi.host}${rule.wangyi.playlistDetail}?id=${id}`; + const html = await request(url, {}, 2, 'wangyi'); + const json = safeJSONParse(html); + const playlist = json.result || json.playlist || {}; + data = playlist; + tracks = playlist.tracks || []; + const defaultPic = getPicUrl(data.coverImgUrl || data.picUrl, '500y500'); + const { playFrom, playUrl, playPic } = buildWangyiPlayData(tracks, defaultPic); + vod = { + vod_id: `wangyi@playlist@${id}`, + vod_name: data.name || '未知歌单', + vod_pic: defaultPic, + vod_content: data.description || data.name, + vod_remarks: `${formatNumber(data.playCount || 0)} | 共${tracks.length}首`, + vod_play_from: playFrom, + vod_play_url: playUrl, + vod_play_pic: playPic, + vod_play_pic_ratio: 1.0 + }; + } + } catch (e) {} + return vod; +} + +async function getTinghaiDetail(id) { + let vod = {}; + try { + const limit = 100; + let baseUrl = `${rule.tinghai.host}${rule.tinghai.playlistInfo}?pid=${id}&rn=${limit}&httpsStatus=1&pn=`; + let html = await request(baseUrl + '1', {}, 2, 'tinghai'); + let json = safeJSONParse(html); + let data = json.data || {}; + let songs = data.musicList || data.musiclist || []; + let total = parseInt(data.total || 0); + + if (total > limit) { + let tasks = []; + for (let p = 2; p <= Math.min(Math.ceil(total / limit), 5); p++) { + tasks.push(request(baseUrl + p, {}, 2, 'tinghai')); + } + let results = await Promise.all(tasks); + results.forEach(r => { + let d = safeJSONParse(r).data || {}; + songs = songs.concat(d.musicList || d.musiclist || []); + }); + } + + let playArr = []; + let songPicArr = []; + songs.forEach(it => { + let rid = (it.rid || it.musicrid || '').toString().replace('MUSIC_', ''); + let song = (it.name || '').replace(/ /g, ' '); + let artist = (it.artist || '').replace(/ /g, ' '); + let albumpic = hd(it.albumpic || it.pic); + let displayName = artist ? `${song} [${artist}]` : song; + if (rid) { + playArr.push(`${displayName}$${rid}&&${albumpic}&&${albumpic}`); + songPicArr.push(albumpic); + } + }); + + vod = { + vod_id: id, + vod_name: data.name || '听海歌单', + vod_pic: hd(data.img || data.img500), + vod_content: data.info || '', + vod_remarks: `共${songs.length}首`, + vod_play_from: "听海音乐", + vod_play_url: playArr.join('#'), + vod_play_pic: songPicArr.join('#'), + vod_play_pic_ratio: 1.0 + }; + } catch (e) {} + return vod; +} + +async function getMituDetail(encodedData) { + let vod = {}; + try { + const songData = safeJSONParse(encodedData); + const { id: rid, name, pic, artist, downurl } = songData; + let playUrl = ''; + let rawLrc = '暂无歌词'; + const res = await request(`${rule.mitu.host}${rule.mitu.kwApi}?rid=${rid}&type=json&level=exhigh&lrc=true`, {}, 2, 'mitu'); + const data = safeJSONParse(res); + if (data.code === 200 && data.data) { + playUrl = data.data.url || ''; + rawLrc = data.data.lrc || '暂无歌词'; + } + const displayLrc = rawLrc === '暂无歌词' ? rawLrc : rawLrc.replace(/\[\d{2}:\d{2}\.\d{2}\]/g, '\n'); + let playFrom = []; + let playUrls = []; + if (playUrl) { + playFrom.push('在线播放'); + playUrls.push(`第1集$${JSON.stringify({ url: playUrl, lrc: rawLrc, cover: pic })}`); + } + if (downurl && downurl.length) { + playFrom.push('网盘下载'); + const downUrls = downurl.map(item => { const [n, u] = item.split('$$'); return `${n}$push://${u}`; }).join('#'); + playUrls.push(downUrls); + } + vod = { + vod_id: rid, + vod_name: name, + vod_pic: pic, + vod_content: displayLrc, + vod_actor: artist, + vod_play_from: playFrom.join('$$$'), + vod_play_url: playUrls.join('$$$') + }; + } catch (e) {} + return vod; +} + +async function playWangyi(id) { + try { + const [musicId, qualityType] = id.split('|'); + const playApi = `${rule.wangyi.playApi}&id=${musicId}`; + const playJson = safeJSONParse(await request(playApi, {}, 2, 'wangyi')); + let songUrl = ''; + if (playJson && playJson.code === 0 && playJson.data && playJson.data.length > 0) { + songUrl = playJson.data[0].url || ''; + } + const lyricApi = `${rule.wangyi.host}${rule.wangyi.songLyric}?id=${musicId}&lv=1&kv=1&tv=-1`; + const lyricJson = safeJSONParse(await request(lyricApi, {}, 2, 'wangyi')); + let lyric = lyricJson.lrc?.lyric || ''; + if (lyricJson.tlyric?.lyric) lyric = lyric + '\n\n【翻译】\n' + lyricJson.tlyric.lyric; + const infoApi = `${rule.wangyi.host}${rule.wangyi.songDetail}?ids=[${musicId}]`; + const infoJson = safeJSONParse(await request(infoApi, {}, 2, 'wangyi')); + let cover = ''; + if (infoJson.songs && infoJson.songs[0]) { + const song = infoJson.songs[0]; + if (song.album && song.album.picUrl) cover = song.album.picUrl + '?param=500y500'; + else if (song.al && song.al.picUrl) cover = song.al.picUrl + '?param=500y500'; + } + return JSON.stringify({ parse: 0, url: songUrl, header: baseHeaders, lrc: lyric, cover: cover, pic: cover, height: 720 }); + } catch (e) { + return JSON.stringify({ parse: 0, url: id }); + } +} + +async function getTinghaiSongUrl(rid, br) { + const url = `${rule.tinghai.songUrl}?f=web&user=0&source=kwplayer_ar_4.4.2.7_B_nuoweida_vh.apk&type=convert_url_with_sign&rid=${rid}&format=flac&br=${br}`; + const html = await request(url, {}, 2, 'tinghai'); + const json = safeJSONParse(html); + return json?.data?.url?.trim() || ''; +} + +async function getTinghaiLyric(rid) { + for (let i = 0; i < 20; i++) { + try { + const url = `${rule.tinghai.lyricApi}?musicId=${rid}`; + const html = await request(url, {}, 2, 'tinghai'); + if (html) { + const json = safeJSONParse(html); + if (json.code === 200 && json.data && json.data.lrclist && json.data.lrclist.length > 0) { + const lrclist = json.data.lrclist; + const lyric = lrclist.map(item => { + const time = parseFloat(item.time) || 0; + const min = Math.floor(time / 60).toString().padStart(2, '0'); + const sec = Math.floor(time % 60).toString().padStart(2, '0'); + const ms = Math.floor((time % 1) * 100).toString().padStart(2, '0'); + return `[${min}:${sec}.${ms}]${item.lineLyric || ''}`; + }).join('\n'); + return lyric; + } + } + } catch (e) {} + if (i < 19) await sleep(0.01); + } + return '暂无歌词'; +} + +async function playTinghai(id) { + try { + const parts = id.split('&&'); + const firstPart = parts[0] || ''; + const firstParts = firstPart.split('$'); + const songId = firstParts.length > 1 ? firstParts[1] : firstParts[0]; + const albumPic = hd(parts[1]); + let url = await getTinghaiSongUrl(songId, '320kmp3'); + if (!url) url = await getTinghaiSongUrl(songId, '128kmp3'); + let lrc = await getTinghaiLyric(songId); + let picUrl = albumPic; + if (!picUrl) { + try { + let picRes = await request(`${rule.tinghai.picApi}?type=rid_pic&pictype=url&size=500&rid=${songId}`, {}, 2, 'tinghai'); + picUrl = picRes.trim().replace('/500/', '/2160/'); + } catch (e) {} + } + const result = { parse: 0, url: url || '', header: baseHeaders, height: 720 }; + if (picUrl) { result.pic = picUrl; result.cover = picUrl; } + if (lrc && lrc !== '暂无歌词') result.lrc = lrc; + return JSON.stringify(result); + } catch (e) { + return JSON.stringify({ parse: 0, url: id }); + } +} + +async function playMitu(id) { + try { + const playData = safeJSONParse(id); + let subt; + if (playData.lrc && playData.lrc !== '暂无歌词') { + subt = 'data:text/plain;charset=utf-8,' + encodeURIComponent(playData.lrc); + } + return JSON.stringify({ + parse: 0, + url: playData.url, + header: { ...baseHeaders, 'Referer': rule.mitu.referer }, + lrc: playData.lrc, + subt, + cover: playData.cover, + pic: playData.cover, + height: 720 + }); + } catch (e) { + return JSON.stringify({ parse: 0, url: id }); + } +} + +async function searchWangyi(wd) { + const results = []; + const searchTypes = [ + { type: 1, prefix: 'wangyi@song@', remark: '歌曲', key: 'songs' }, + { type: 10, prefix: 'wangyi@album@', remark: '专辑', key: 'albums' }, + { type: 1000, prefix: 'wangyi@playlist@', remark: '歌单', key: 'playlists' }, + { type: 100, prefix: 'wangyi@artist@', remark: '歌手', key: 'artists' } + ]; + try { + for (const st of searchTypes) { + const url = `${rule.wangyi.searchApi}?keywords=${encodeURIComponent(wd)}&type=${st.type}`; + const res = await request(url, {}, 2, 'wangyi'); + const json = safeJSONParse(res); + if (json.result?.[st.key]) { + for (const item of json.result[st.key]) { + const result = { vod_id: `${st.prefix}${item.id}`, vod_name: item.name, vod_remarks: st.remark, vod_pic: '' }; + if (st.type === 1) { + if (item.ar) result.vod_name += ' - ' + item.ar.map(a => a.name).join('/'); + if (item.al?.picUrl) result.vod_pic = getPicUrl(item.al.picUrl, '300y300'); + } else if (st.type === 10) { + if (item.artist) result.vod_name += ' - ' + item.artist.name; + if (item.picUrl) result.vod_pic = getPicUrl(item.picUrl, '300y300'); + } else if (st.type === 1000) { + if (item.coverImgUrl) result.vod_pic = getPicUrl(item.coverImgUrl, '300y300'); + result.vod_remarks += ` | ${formatNumber(item.playCount || 0)}`; + } else if (st.type === 100) { + const picUrl = item.picUrl || item.img1v1Url; + if (picUrl) result.vod_pic = getPicUrl(picUrl, '300y300'); + } + results.push(result); + } + } + } + } catch (e) {} + return results; +} + +async function searchTinghai(wd, page) { + const results = []; + const offset = (page - 1) * 30; + const url = `${rule.tinghai.searchApi}?client=kt&all=${encodeURIComponent(wd)}&pn=${offset}&rn=30&vipver=1&ft=music&encoding=utf8&rformat=json&mobi=1`; + try { + let html = ''; + let retry = 0; + while (!html && retry < 3) { + html = await request(url, {}, 2, 'tinghai'); + if (!html) await sleep(0.1); + retry++; + } + if (html) { + const json = safeJSONParse(html.replace(/'/g, '"')); + if (json.abslist) { + json.abslist.forEach(it => { + const rid = it.DC_TARGETID || it.MUSICRID?.replace('MUSIC_', '') || ''; + const pic = it.web_albumpic_short ? `http://img1.kuwo.cn/star/albumcover/${it.web_albumpic_short}` : (it.hts_MVPIC || ''); + results.push({ + vod_id: `tinghai@song@${rid}`, + vod_name: `${it.SONGNAME || it.NAME || '未知歌曲'} - ${it.ARTIST || '未知歌手'}`, + vod_pic: hd(pic), + vod_remarks: it.ALBUM || '听海音乐', + }); + }); + } + } + } catch (e) {} + return results; +} + +async function searchMitu(wd) { + const results = []; + const url = `${rule.mitu.host}/api/songs.php?type=search&keyword=${encodeURIComponent(wd)}`; + try { + const html = await request(url, {}, 2, 'mitu'); + const json = safeJSONParse(html); + if (json.code === 200 && Array.isArray(json.data)) { + json.data.forEach(item => { + const vodData = { id: item.rid, name: item.name, pic: item.pic, artist: item.artist, downurl: item.downurl || [] }; + results.push({ + vod_id: `mitu@song@${encodeURIComponent(JSON.stringify(vodData))}`, + vod_name: `${item.name} - ${item.artist}`, + vod_pic: item.pic || '', + vod_remarks: '米兔音乐' + }); + }); + } + } catch (e) {} + return results; +} + +async function cfs(siteId, wd, pg) { + const page = pg || 1; + let results = []; + if (siteId === 'wangyi') results = await searchWangyi(wd); + else if (siteId === 'tinghai') results = await searchTinghai(wd, page); + else if (siteId === 'mitu') results = await searchMitu(wd); + return JSON.stringify({ list: results, page: page, pagecount: page + 1, limit: results.length, total: results.length * (page + 1) }); +} + +async function home(filter) { + const platForms = getPlatList(); + const classes = platForms.map(item => ({ type_name: item.name, type_id: item.id })); + const filters = {}; + platForms.forEach(item => { if (filterOptions[item.id]) filters[item.id] = filterOptions[item.id]; }); + return JSON.stringify({ class: classes, filters: filters }); +} + +async function homeVod() { + const platForms = getPlatList(); + const randomPlat = platForms[Math.floor(Math.random() * platForms.length)]; + const randomArea = ruleFilterDef[randomPlat.id]?.area || ''; + const categoryResult = await category(randomPlat.id, 1, { area: randomArea }, {}); + const categoryList = safeJSONParse(categoryResult).list || []; + return JSON.stringify({ list: categoryList.slice(0, 20) }); +} + +async function category(tid, pg, filter, extend) { + const page = pg || 1; + extend = extend || {}; + const platformItem = platformList.find(p => p.id === tid); + if (!platformItem) return JSON.stringify({ list: [], page, pagecount: 1, limit: 0, total: 0 }); + const searchKeyword = extend?.custom; + if (searchKeyword) return await cfs(tid, searchKeyword, pg); + const area = filter?.area || extend?.area || ruleFilterDef[tid]?.area || ''; + const videos = []; + switch (tid) { + case 'wangyi': videos.push(...await getWangyiList(area, page, extend)); break; + case 'tinghai': videos.push(...await getTinghaiList(area, page)); break; + case 'mitu': videos.push(...await getMituList(area, page)); break; + } + return JSON.stringify({ list: videos, page: page, pagecount: page + 1, limit: videos.length, total: videos.length * (page + 1) }); +} + +async function detail(id) { + const parts = id.split('@'); + const platform = parts[0]; + const type = parts[1]; + const did = decodeURIComponent(parts.slice(2).join('@')); + let vod = {}; + switch (platform) { + case 'wangyi': vod = await getWangyiDetail(type, did); break; + case 'tinghai': vod = await getTinghaiDetail(did); break; + case 'mitu': vod = await getMituDetail(did); break; + } + return JSON.stringify({ list: [vod] }); +} + +async function play(flag, id, flags) { + if (flag.includes('网易云') || flag.includes('超高') || flag.includes('无损') || flag.includes('极高') || flag.includes('较高') || flag.includes('标准')) { + return await playWangyi(id); + } + if (flag.includes('听海')) return await playTinghai(id); + if (flag.includes('网盘下载')) return JSON.stringify({ parse: 0, url: id }); + if (flag.includes('在线播放')) return await playMitu(id); + return JSON.stringify({ parse: 0, url: id }); +} + +async function search(wd, quick, pg) { + const videos = []; + const page = pg || 1; + const searchPromises = [cfs('wangyi', wd, page), cfs('tinghai', wd, page), cfs('mitu', wd, page)]; + const searchResults = await Promise.all(searchPromises); + searchResults.forEach(result => { videos.push(...safeJSONParse(result).list || []); }); + const filteredResults = videos.filter(item => (item.vod_name || '').toLowerCase().includes(wd.toLowerCase())); + return JSON.stringify({ list: filteredResults, page: page, pagecount: page + 1, limit: filteredResults.length, total: filteredResults.length * (page + 1) }); +} + +export function __jsEvalReturn() { + return { init, home, homeVod, category, detail, play, search, cfs }; +} \ No newline at end of file