Repository navigation
Expand file tree
/
Copy pathmanko-fun-mini-library.js
More file actions
101 lines (96 loc) · 20.5 KB
/
Copy pathmanko-fun-mini-library.js
File metadata and controls
101 lines (96 loc) · 20.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
// @name Manko.fun
const MANKO_BASE = 'https://manko.fun';
const MANKO_API = 'https://healertanker.com/';
const MANKO_AUTHOR = 'Alan huang';
const MANKO_UA = 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) AppleWebKit/605.1.15 Version/18.0 Mobile/15E148 Safari/604.1';
const MANKO_HEADERS = { 'User-Agent': MANKO_UA, Referer: MANKO_BASE + '/' };
const MANKO_PRIMARY_NAV = [
['popular', '🔥 热门作品', 'Most Popular'], ['rated', '⭐ 最佳评分', 'Top Rated'],
['new', '✨ 最新作品', 'New'], ['subtitle', '📝 中文字幕影片', 'Subtitle'],
['random', '🎲 随机', 'Random']
];
const MANKO_VIDEO_CATEGORIES = [
['censored', '马赛克', 'Censored'], ['uncensored', '无码赛克', 'Uncensored'],
['popular', '热门作品', 'Most Popular'], ['rated', '最佳评分', 'Top Rated'],
['fc2', 'FC2', 'FC2'], ['leaked', '马赛克已移除', 'Leaked']
];
const MANKO_CATEGORIES = MANKO_PRIMARY_NAV.concat(MANKO_VIDEO_CATEGORIES.filter(x => !MANKO_PRIMARY_NAV.some(y => y[0] === x[0])));
const MANKO_GENRE_ZH = { Action:'动作', Amateur:'业余', 'Adult Movie':'成人影片', 'Beautiful Girl Movie':'美少女', 'Big Tits':'巨乳', Blow:'口交', Breasts:'胸部', Butt:'臀部', Classic:'经典', Cosplay:'角色扮演', Creampie:'中出', Cunnilingus:'舔阴', Documentary:'纪实', Drama:'剧情', 'For Women':'女性向', Handjob:'手交', Kiss:'接吻', Massage:'按摩', Masturbation:'自慰', 'Mature Woman':'熟女', POV:'第一视角', Shaved:'无毛', Slender:'苗条', Squirting:'潮吹', VR:'VR', 'US/EU Porn':'欧美' };
function asCtx(v) { if (typeof v === 'string') { try { return JSON.parse(v); } catch (_) { return {}; } } return v || {}; }
function pick(c, keys, fallback) { c = asCtx(c); const pools = [c, c.params, c.config, c.settings, c.parameters, c.pagination, c.pageInfo].filter(Boolean); for (const p of pools) for (const k of keys) if (p[k] !== undefined && p[k] !== null && p[k] !== '') return p[k]; return fallback; }
function responseText(r) { if (typeof r === 'string') return r; if (!r) return ''; for (const x of [r.data, r.body, r.text, r.responseBody]) { if (typeof x === 'string') return x; if (x && typeof x === 'object') return JSON.stringify(x); } return typeof r === 'object' ? JSON.stringify(r) : String(r); }
async function httpGet(url) {
const opts = { headers: MANKO_HEADERS };
if (typeof Widget !== 'undefined' && Widget.http) {
if (typeof Widget.http.get === 'function') return responseText(await Widget.http.get(url, opts));
if (typeof Widget.http.request === 'function') return responseText(await Widget.http.request(Object.assign({ url, method: 'GET' }, opts)));
}
if (typeof $http !== 'undefined') {
if (typeof $http.get === 'function') return responseText(await $http.get(url, opts));
if (typeof $http.request === 'function') return responseText(await $http.request(Object.assign({ url, method: 'GET' }, opts)));
}
if (typeof fetch === 'function') return await (await fetch(url, opts)).text();
throw new Error('Manko:当前环境没有可用的 HTTP 客户端');
}
function decodeBase91(input) {
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!#$%&()*+,./:;<=>?@[]^_`{|}~"';
const text = String(input || ''); let value = 0, bits = 0, pending = -1; const bytes = [];
for (let i = 0; i < text.length; i++) { const digit = alphabet.indexOf(text[i]); if (digit < 0) continue; if (pending < 0) pending = digit; else { value |= (pending += 91 * digit) << bits; bits += (pending & 8191) > 88 ? 13 : 14; do { bytes.push(value & 255); value >>= 8; bits -= 8; } while (bits > 7); pending = -1; } }
if (pending > -1) bytes.push((value | pending << bits) & 255);
if (typeof TextDecoder !== 'undefined') return new TextDecoder('utf-8').decode(new Uint8Array(bytes));
let encoded = ''; for (const b of bytes) encoded += '%' + ('0' + b.toString(16)).slice(-2); try { return decodeURIComponent(encoded); } catch (_) { return bytes.map(b => String.fromCharCode(b)).join(''); }
}
async function apiGet(path) { const raw = await httpGet(MANKO_API + String(path).replace(/^\/+/, '')); let outer; try { outer = JSON.parse(raw); } catch (_) { throw new Error('Manko API:返回的不是 JSON'); } const payload = outer && typeof outer.data === 'string' ? decodeBase91(outer.data) : outer && outer.data !== undefined ? outer.data : outer; if (typeof payload === 'string') { try { return JSON.parse(payload); } catch (_) { throw new Error('Manko API:数据解码失败'); } } return payload; }
function categoryInfo(id) { return MANKO_CATEGORIES.find(x => x[0] === id) || MANKO_CATEGORIES[0]; }
function categoryPath(id, page) { const query = 'page=' + page + '&size=24&can_manko=true'; const base = 'swx/movie/search?' + query; if (id === 'popular') return base + '&most_popular=true'; if (id === 'rated') return base + '&top_rated=true'; if (id === 'subtitle') return base + '&subtitle_link.ZH=true'; if (id === 'random') return 'swx/movie/random-list?' + query; if (id === 'new') return base; return base + '&category=' + encodeURIComponent(categoryInfo(id)[2]); }
function apiMovie(m, rank) { const image = m.wide_thumbnail || m.thumbnail_image || m.cover_image || ''; return { id: String(m._id || m.item_id || ''), title: m.title || (m.title_display_name && m.title_display_name[0] && m.title_display_name[0].title) || '未命名', type: 'movie', poster: image, backdrop: image, imageHeaders: MANKO_HEADERS, year: Number(m.startyear || String(m.share_date || '').slice(0,4)) || undefined, rating: Number(m.imdb_score) || undefined, rank: rank || undefined, subtitle: m.share_date || '', remarks: m.can_play === false ? '暂不可播放' : '', action: { type: 'detail', itemId: String(m._id || m.item_id || '') }, _playUrl: m.wasabi_url_1 || m.missav_video || '' }; }
function genreZh(name) { return MANKO_GENRE_ZH[name] || name; }
function actorZh(a) { return (a.also_known_as && (a.also_known_as.tw || a.also_known_as.zh)) || a.name || (a.also_known_as && a.also_known_as.jp) || (a.also_known_as && a.also_known_as.en) || '未知演员'; }
function actorCard(a) { const id = String(a.person_id || a._id || ''); const image = a.profile_path || ''; return { id: 'actor:' + id, title: actorZh(a), subtitle: a.also_known_as && a.also_known_as.en || '演员作品', type: 'collection', poster: image, backdrop: image, imageHeaders: MANKO_HEADERS, action: { type: 'category', pageId: 'actor:' + encodeURIComponent(id), title: actorZh(a) } }; }
function castMember(a) { const id = String(a.person_id || a._id || ''); const name = actorZh(a); const image = a.profile_path || ''; const item = { id, name, role: '演员', character: '演员', profilePath: image, image, imageHeaders: MANKO_HEADERS }; if (id) item.action = { type: 'category', pageId: 'actor:' + encodeURIComponent(id), title: name, itemAspectRatio: '16:9' }; return item; }
function directoryCard(kind, value, title, preview) { const lead = preview[0] || {}; return { id: kind + ':' + value, title, subtitle: '浏览相关影片', type: 'collection', poster: lead.poster || '', backdrop: lead.backdrop || lead.poster || '', imageHeaders: lead.imageHeaders || MANKO_HEADERS, previewItems: preview.slice(0,3), action: { type: 'category', pageId: kind + ':' + encodeURIComponent(value), title } }; }
function directoryTextCard(kind, value, title, subtitle) { return { id: kind + ':' + value, title, subtitle: subtitle || '浏览相关影片', type: 'category', metadataText: '影片分类', action: { type: 'category', pageId: kind + ':' + encodeURIComponent(value), title } }; }
async function movieRows(path, page, size) { const data = await apiGet(path); const rows = Array.isArray(data) ? data : (data && (data.items || data.results || data.data)) || []; return { rows, items: rows.map((m, i) => apiMovie(m, (page - 1) * size + i + 1)).filter(x => x.id) }; }
function filteredMoviePath(kind, value, page, size) { const base = 'swx/movie/search?size=' + size + '&page=' + page + '&can_manko=true'; if (kind === 'category' && value === 'Most Popular') return base + '&most_popular=true'; if (kind === 'category' && value === 'Top Rated') return base + '&top_rated=true'; const filter = kind === 'genre' ? 'genre' : kind === 'maker' ? 'country' : 'category'; return base + '&' + filter + '=' + encodeURIComponent(value); }
async function previewCard(kind, value, title) { const r = await movieRows(filteredMoviePath(kind, value, 1, 3), 1, 3); return directoryCard(kind, value, title, r.items); }
async function primaryNavCard(def) { const r = await movieRows(categoryPath(def[0], 1).replace('size=24', 'size=3'), 1, 3); const lead = r.items[0] || {}; return { id: 'nav:' + def[0], title: def[1], subtitle: '浏览相关影片', type: 'collection', poster: lead.poster || '', backdrop: lead.backdrop || lead.poster || '', imageHeaders: lead.imageHeaders || MANKO_HEADERS, previewItems: r.items.slice(0,3), action: { type: 'category', pageId: def[0], title: def[1] } }; }
function searchMode(ctx) { const value = String(pick(ctx, ['searchType','searchBy','by','mode','scope','type','category'], 'title')).trim().toLowerCase(); return /actor|actress|performer|person|star|演员|女优|女優/.test(value) ? 'actor' : 'title'; }
function searchPath(query, page, mode) { const key = mode === 'actor' ? 'actor' : 'keyword'; return 'swx/movie/search?' + key + '=' + encodeURIComponent(query) + '&size=24&page=' + page + '&can_manko=true'; }
function codePrefix(query) { const m = String(query || '').trim().toUpperCase().match(/[A-Z]{2,}/); return m ? m[0] : ''; }
function canonicalCode(value) { return String(value || '').toUpperCase().replace(/[^A-Z0-9]/g, '').replace(/^\d{2,4}(?=[A-Z])/, ''); }
function getManifest() { return { id: 'manko-fun', name: 'Manko.fun 中文版', title: 'Manko.fun 中文版', version: '1.8.1', author: MANKO_AUTHOR, logo: MANKO_BASE + '/images/metaog.png', capabilities: { search: true, aggregation: true, playbackHistory: true }, aggregation: { search: true, playbackHistory: true } }; }
async function getCategory(ctx) {
const id = String(pick(ctx, ['pageId','categoryId','id'], 'new')); const page = Math.max(1, Number(pick(ctx, ['page','pg','currentPage','pageNumber','pageIndex'], 1)) || 1); let title = '影片', result;
if (id === 'genres') { const all = await apiGet('common/list-genre'); const list = Array.isArray(all) ? all : []; const limit = 48; const values = list.slice((page - 1) * limit, page * limit); const items = values.map(x => directoryTextCard('genre', x, genreZh(x), genreZh(x) === x ? '内容类型' : x)); return { pageType: 'category', id, title: '全部内容类型', style: 'discover.annualCategories', page, nextPage: page + 1, limit, total: list.length, totalPages: Math.ceil(list.length / limit), hasMore: page * limit < list.length, items }; }
else if (id === 'makers') { const all = await apiGet('common/country-list'); const list = Array.isArray(all) ? all : []; const limit = 48; const values = list.slice((page - 1) * limit, page * limit); const items = values.map(x => directoryTextCard('maker', x, x, '制作商')); return { pageType: 'category', id, title: '全部制作商', style: 'discover.annualCategories', page, nextPage: page + 1, limit, total: list.length, totalPages: Math.ceil(list.length / limit), hasMore: page * limit < list.length, items }; }
else if (id.indexOf('category:') === 0) { const value = decodeURIComponent(id.slice(9)); title = (MANKO_CATEGORIES.find(x => x[2] === value) || [null, value])[1]; result = await movieRows(filteredMoviePath('category', value, page, 24), page, 24); }
else if (id.indexOf('genre:') === 0) { const value = decodeURIComponent(id.slice(6)); title = genreZh(value); result = await movieRows('swx/movie/search?size=24&page=' + page + '&can_manko=true&genre=' + encodeURIComponent(value), page, 24); }
else if (id.indexOf('maker:') === 0) { const value = decodeURIComponent(id.slice(6)); title = value; result = await movieRows('swx/movie/search?size=24&page=' + page + '&can_manko=true&country=' + encodeURIComponent(value), page, 24); }
else if (id.indexOf('actor:') === 0) { const value = decodeURIComponent(id.slice(6)); title = String(pick(ctx, ['title'], '演员作品')); result = await movieRows('swx/movie/search?person_id=' + encodeURIComponent(value) + '&size=30&page=' + page + '&can_manko=true', page, 30); }
else if (id.indexOf('actors:') === 0) { const value = decodeURIComponent(id.slice(7)); const rows = await apiGet('swx/movie/actor/' + encodeURIComponent(value) + '?offset=' + ((page - 1) * 48) + '&limit=48'); const items = (Array.isArray(rows) ? rows : []).map(actorCard).filter(x => x.id !== 'actor:'); return { pageType: 'category', id, title: value === 'Censored' ? '有码演员' : value === 'Uncensored' ? '无码演员' : '西方演员', style: 'media.posterGrid', itemAspectRatio: '2:3', page, nextPage: page + 1, hasMore: items.length >= 48, items }; }
else { title = categoryInfo(id)[1]; result = await movieRows(categoryPath(id, page), page, 24); }
return { pageType: 'category', id, title, style: 'media.posterGrid', itemAspectRatio: '16:9', page, nextPage: page + 1, hasMore: result.rows.length >= (id.indexOf('actor:') === 0 ? 30 : 24), items: result.items };
}
async function getHome() { const first = await getCategory({ pageId: 'new', page: 1 }); const lazy = (id, title) => ({ id, title, style: 'discover.annualPosterStack', lazy: true, loadAction: { type: 'custom', id, sectionId: id, title }, items: [] }); return { pageType: 'home', id: 'manko-home-zh-v3', title: 'Manko.fun 中文版', heroAspectRatio: '16:9', hero: first.items.slice(0, 6), sections: [{ id: 'new', title: '✨ 最新作品', style: 'discover.standard', moreAction: { type: 'category', pageId: 'new', title: '✨ 最新作品' }, items: first.items.slice(0, 12) }, lazy('primary-nav', '快捷导航'), lazy('category-nav', '影片类别'), lazy('genre-nav', '内容类型'), lazy('maker-nav', '制作商'), lazy('actor-nav', '演员分类')] }; }
async function getHomeSection(ctx) { const id = String(pick(ctx, ['sectionId','id'], 'new')); try {
if (id === 'primary-nav') { const items = await loadNavCards(MANKO_PRIMARY_NAV); return { id, title: '快捷导航', style: 'discover.annualPosterStack', lazy: false, items: items.filter(x => x.poster) }; }
if (id === 'category-nav') { const items = await loadNavCards(MANKO_VIDEO_CATEGORIES); return { id, title: '影片类别', style: 'discover.annualPosterStack', lazy: false, items: items.filter(x => x.poster) }; }
if (id === 'genre-nav') { const p = await getCategory({ pageId: 'genres', page: 1 }); return { id, title: '内容类型', style: 'discover.annualCategories', lazy: false, moreAction: { type: 'category', pageId: 'genres', title: '全部内容类型' }, items: p.items }; }
if (id === 'maker-nav') { const p = await getCategory({ pageId: 'makers', page: 1 }); return { id, title: '制作商', style: 'discover.annualCategories', lazy: false, moreAction: { type: 'category', pageId: 'makers', title: '全部制作商' }, items: p.items }; }
if (id === 'actor-nav') { const defs = [['Censored','有码演员'],['Uncensored','无码演员'],['Western','西方演员']]; const items = await Promise.all(defs.map(async d => { const rows = await apiGet('swx/movie/actor/' + d[0] + '?offset=0&limit=3'); const previews = (Array.isArray(rows) ? rows : []).map(actorCard); const lead = previews[0] || {}; return { id: 'actors:' + d[0], title: d[1], subtitle: '浏览全部演员', type: 'collection', poster: lead.poster || '', backdrop: lead.backdrop || lead.poster || '', imageHeaders: MANKO_HEADERS, previewItems: previews, action: { type: 'category', pageId: 'actors:' + d[0], title: d[1] } }; })); return { id, title: '演员分类', style: 'discover.annualPosterStack', lazy: false, items: items.filter(x => x.poster) }; }
const p = await getCategory({ pageId: id, page: 1 }); return { id, title: p.title, style: 'discover.standard', lazy: false, moreAction: { type: 'category', pageId: id, title: p.title }, items: p.items.slice(0,12) };
} catch (e) { return { id, title: String(pick(ctx, ['title'], '分类')), style: 'discover.standard', lazy: false, items: [], error: String(e && e.message || e) }; } }
async function getDetail(ctx) { const id = String(pick(ctx, ['itemId','id'], '')); if (!id) throw new Error('Manko:缺少影片 ID'); const m = await apiGet('swx/movie/detail/' + encodeURIComponent(id)); const names = Array.isArray(m.title_display_name) ? m.title_display_name : []; const title = ((names.find(x => x.language === 'zh') || names.find(x => x.language === 'tw') || names.find(x => x.language === 'ja') || names[0]) || {}).title || m.title || id; const image = m.cover_image || m.wide_thumbnail || m.thumbnail_image || ''; const backdrop = (m.backdrop_images && m.backdrop_images[0]) || image; const descriptions = Array.isArray(m.discription) ? m.discription : []; const overview = ((descriptions.find(x => x.language === 'zh') || descriptions.find(x => x.language === 'tw') || descriptions.find(x => x.language === 'ja') || descriptions.find(x => x.language === 'en') || descriptions[0]) || {}).overview || ''; const playUrl = m.wasabi_url_1 || m.missav_video || m.webseed_url_1 || ''; const version = { id: 'default', name: '在线播放', subtitle: '播放时刷新地址', default: true, action: { type: 'play', itemId: id, versionId: 'default' } }; let cast = []; try { const actors = await apiGet('swx/movie/actors/' + encodeURIComponent(id) + '?size=12&page=1'); cast = (Array.isArray(actors) ? actors : []).map(castMember); } catch (_) {} return { id, title, type: 'movie', poster: image, backdrop, detailImageAspectRatio: '16:9', imageHeaders: MANKO_HEADERS, posterHeaders: MANKO_HEADERS, backdropHeaders: MANKO_HEADERS, overview, year: Number(m.startyear || String(m.share_date || '').slice(0,4)) || undefined, rating: Number(m.imdb_score) || undefined, runtimeMinutes: Number(m.video_duration) || undefined, genres: Array.isArray(m.tags) ? m.tags.map(genreZh) : [], cast, resourceGroups: playUrl ? [{ id: 'online', title: '在线播放', versions: [version] }] : [], recommendations: [] }; }
async function getResourceVersions(ctx) { const d = await getDetail(ctx); return d.resourceGroups; }
async function resolvePlayback(ctx) { const id = String(pick(ctx, ['itemId','id'], '')); let url = ''; if (id) { const m = await apiGet('swx/movie/detail/' + encodeURIComponent(id)); url = m.wasabi_url_1 || m.missav_video || m.webseed_url_1 || ''; } else url = String(pick(ctx, ['url','playUrl','videoUrl','path'], '')); if (!url) throw new Error('Manko 播放:源站未提供公开播放地址'); return { url, container: /\.mp4(?:\?|$)/i.test(url) ? 'mp4' : 'm3u8', headers: { Referer: 'https://javplayer.cc/' }, startPositionSeconds: 0, isLive: false, streamKind: 'vod' }; }
async function search(ctx) { const query = String(pick(ctx, ['query','keyword','text'], '')).trim(); const page = Math.max(1, Number(pick(ctx, ['page','pg','currentPage','pageNumber','pageIndex'], 1)) || 1); const mode = searchMode(ctx); const label = mode === 'actor' ? '演员' : '作品'; if (!query) return { pageType: 'search', id: 'manko-search', title: label + '搜索', query: '', keyword: '', searchType: mode, page, hasMore: false, items: [] }; let result = await movieRows(searchPath(query, page, mode), page, 24); let fallback = false; if (mode === 'title' && !result.rows.length && page === 1) { const prefix = codePrefix(query); if (prefix && prefix !== query.toUpperCase()) { const broad = await movieRows(searchPath(prefix, 1, mode), 1, 24); const wanted = canonicalCode(query); const matched = broad.items.filter(x => canonicalCode(x.title).indexOf(wanted) >= 0); result = { rows: matched.length ? matched : broad.rows, items: matched.length ? matched : broad.items }; fallback = true; } } return { pageType: 'search', id: 'manko-search', title: label + '搜索:' + query, query, keyword: query, searchType: mode, style: 'media.posterGrid', itemAspectRatio: '16:9', page, nextPage: page + 1, hasMore: !fallback && result.rows.length >= 24, items: result.items }; }
const api = { getManifest, getHome, getHomeSection, getCategory, getDetail, getResourceVersions, resolvePlayback, search };
Object.assign(globalThis, api);
if (typeof module !== 'undefined') module.exports = Object.assign({ _test: { decodeBase91, apiMovie, categoryPath, genreZh, actorZh, actorCard, castMember, directoryCard, directoryTextCard, filteredMoviePath, primaryNavCard, searchMode, searchPath, codePrefix, canonicalCode, MANKO_AUTHOR, MANKO_PRIMARY_NAV, MANKO_VIDEO_CATEGORIES } }, api);
async function loadNavCards(definitions) {
const cards = new Array(definitions.length); let next = 0;
await Promise.all(Array.from({ length: Math.min(2, definitions.length) }, async function () {
while (next < definitions.length) { const index = next++; try { cards[index] = await primaryNavCard(definitions[index]); } catch (_) {} }
}));
return cards.filter(Boolean);
}