diff --git a/repo/api.yinyuetai.js b/repo/api.yinyuetai.js index 5a59a52..b2e2da4 100644 --- a/repo/api.yinyuetai.js +++ b/repo/api.yinyuetai.js @@ -1,6 +1,6 @@ // ==MiruExtension== // @name 音悦台MTV -// @version v0.0.1 +// @version v0.0.2 // @author vvsolo // @lang zh // @license MIT @@ -12,50 +12,94 @@ // ==/MiruExtension== export default class extends Extension { - #channelDefault = '6998499728862334976'; - #channelList = { - '6998499728862334976': '华语', - '6951459197061984256': '欧美', - '6998475633361805312': '韩语', - '6997855138153095168': '日语', - '7005423896983887872': '音悦人', - } - - #cache = { - res: {}, - items: [], + #opts = { + uptime: 0, + expire: 24*60, + channel: '6998499728862334976', + channels: { + '6998499728862334976': '华语', + '6951459197061984256': '欧美', + '6998475633361805312': '韩语', + '6997855138153095168': '日语', + '7005423896983887872': '音悦人', + }, } + #cache = new Map(); async createFilter(filter) { return { "data": { - title: "", + title: "Channel", max: 1, min: 1, - default: this.#channelDefault, - options: this.#channelList, + default: this.#opts.channel, + options: this.#opts.channels, } } } - async reqJSON(channelid, page) { + async latest(page) { + return await this.getBangumis(this.#opts.channel, page); + } + + async search(kw, page, filter) { + const channelid = filter?.data && filter.data[0] || ''; + if(channelid && !kw) { + this.#opts.channel = channelid; + return await this.getBangumis(channelid, page); + } + const res = await this.getCacheAll(); + if(kw) { + kw = kw.toLowerCase(); + return res.filter((v) => ~v.title.toLowerCase().indexOf(kw)); + } + return res; + } + + async detail(url) { + const res = await this.getCacheAll(); + const bangumi = res.find((v) => v.url === url); + bangumi.episodes = [{ + title: 'Clip', + urls: bangumi.urls.map(v => { + return { + name: v.display, + url: v.url + } + }) + }]; + return bangumi + } + + async watch(url) { + return { + type: 'hls', + url + } + } + + async getCacheAll() { + if (this.#cache.size < 1) { + return []; + } + const bangumi = []; + const values = this.#cache.values(); + let v; + while(v = values.next().value) { + bangumi.push(v); + } + return bangumi.flat(); + } + + async getBangumis(channelid, page) { const size = 20; const offsets = page*size; const baseUrl = `/video/explore/channelVideos?channelId=${channelid}&detailType=2&size=${size}&offset=${offsets}`; - const res = await this.request(baseUrl, { - cache: 'no-cache', - headers: { - 'Content-Type': 'application/json', - } - }); - if ('data' in res) { - return res.data; + const md5path = md5(baseUrl); + if (this.checkCache(md5path)) { + return this.#cache.get(md5path); } - return []; - } - - async getResource(channelid, page) { - const res = await this.reqJSON(channelid, page); + const res = await this.reqJSON(baseUrl); const bangumi = []; ~res.length && res.forEach(v => { const title = `${v.allArtistNames} - ${v.title}`; @@ -72,49 +116,28 @@ export default class extends Extension { //artists: v.allArtistNames, }) }) - this.#cache.items = this.#cache.items.concat(bangumi); + this.#cache.set(md5path, bangumi); + this.#opts.uptime = Date.now(); return bangumi; } - async latest(page) { - return await this.getResource(this.#channelDefault, page); - } - - async search(kw, page, filter) { - const channelid = filter?.data && filter.data[0] || ""; - if(channelid && !kw) { - this.#channelDefault = channelid; - const res = await this.getResource(channelid, page); - return res; - } - if(kw) { - kw = kw.toLowerCase(); - const res = this.#cache.items.filter((v) => ~v.title.toLowerCase().indexOf(kw)); - return res; - } - return this.#cache.items; - } - - async detail(url) { - const bangumi = this.#cache.items.find((v) => v.url === url); - bangumi.episodes = [ - { - title: 'Clip', - urls: bangumi.urls.map(v => { - return { - name: v.display, - url: v.url - } - }) + async reqJSON(path) { + const res = await this.request(path, { + cache: 'no-cache', + headers: { + 'Content-Type': 'application/json', } - ]; - return bangumi + }); + if ('data' in res) { + return res.data; + } + return []; } - async watch(url) { - return { - type: 'hls', - url - } + checkCache(item) { + const expire = +(this.#opts.expire); + return this.#cache.has(item) && + expire > 0 && + (Date.now() - this.#opts.uptime) < expire * 60 * 1000; } } diff --git a/repo/client.iptv.js b/repo/client.iptv.js index bef7f70..ff6f5db 100644 --- a/repo/client.iptv.js +++ b/repo/client.iptv.js @@ -1,39 +1,29 @@ // ==MiruExtension== // @name MyIPTV // @description A simple IPTV client -// @version v0.0.5 +// @version v0.0.6 // @author vvsolo // @lang all // @license MIT // @package client.iptv // @type bangumi -// @icon https://avatars.githubusercontent.com/u/55937028?s=200&v=4 -// @webSite https:// +// @icon https://s11.ax1x.com/2024/01/11/pFCMKit.png +// @webSite https://live.fanmingming.com // @nsfw false // ==/MiruExtension== + export default class extends Extension { - //https://raw.githubusercontent.com - //https://fastly.jsdelivr.net/gh - //https://gcore.jsdelivr.net/gh - //https://jsdelivr.b-cdn.net/gh - //https://github.moeyy.xyz/https://raw.githubusercontent.com - #rawurl = 'https://github.moeyy.xyz/https://raw.githubusercontent.com'; #opts = { url: 'https://live.fanmingming.com/tv/m3u/ipv6.m3u', - exturl: this.#rawurl + `/vvsolo/miru-extension-MyIPTV-sources@main/sources.json`, + exturl: "https://cdn.jsdelivr.net/gh/vvsolo/miru-extension-MyIPTV-sources/sources.json", lists: { - 'none': '', - '🇨🇳 fanmingming-IPV6': 'https://live.fanmingming.com/tv/m3u/ipv6.m3u', - '🇨🇳 fanmingming-IPV4': 'https://live.fanmingming.com/tv/m3u/v6.m3u', - '🇨🇳 MyIPTV-IPV6': this.#rawurl + `/vvsolo/miru-extension-MyIPTV-sources@main/ipv6.m3u`, - '🇨🇳 MyIPTV-IPV4': this.#rawurl + `/vvsolo/miru-extension-MyIPTV-sources@main/ipv4.m3u`, - '🇨🇳 YueChan-IPV6': this.#rawurl + `/YueChan/Live@main/IPTV.m3u`, - '🇨🇳 YueChan-Radio': this.#rawurl + `/YueChan/Live@main/Radio.m3u`, - '🇨🇳 YanG-1989-Gather': this.#rawurl + `/YanG-1989/m3u@main/Gather.m3u`, - '🇨🇳 BESTV': this.#rawurl + `/Ftindy/IPTV-URL@main/bestv.m3u`, - "🇨🇳 蓝鲸": this.#rawurl + `/Cyril0563/lanjing_live@main/TVbox_Free/LIVE/Free/HD_LIVE.txt`, - "🇨🇳 乐青多源": this.#rawurl + `/lqtv/lqtv.github.io/main/m3u/tv.m3u`, + "none": "", + "🇨🇳 fanmingming-IPV6": "https://live.fanmingming.com/tv/m3u/ipv6.m3u", + "🇨🇳 MyIPTV-IPV6": "https://cdn.jsdelivr.net/gh/vvsolo/miru-extension-MyIPTV-sources/ipv6.m3u", + "🇨🇳 MyIPTV-IPV4": "https://cdn.jsdelivr.net/gh/vvsolo/miru-extension-MyIPTV-sources/ipv4.m3u", + "🇨🇳 MyIPTV-VOD": "https://cdn.jsdelivr.net/gh/vvsolo/miru-extension-MyIPTV-sources/ipv4.vod.m3u", + "🇨🇳 MyIPTV-RADIO": "https://cdn.jsdelivr.net/gh/vvsolo/miru-extension-MyIPTV-sources/radio.m3u", } } #group = { @@ -50,10 +40,6 @@ export default class extends Extension { exturl: null } - fixUrl(path) { - return ~path.search(/raw\.githubusercontent\.com/) ? path.replace(/@(main|master)\//, '\/$1\/') : path; - } - async cacheJSON() { if (this.#cache.exturl && await this.checkExpire()) { return this.#cache.exturl; @@ -61,19 +47,15 @@ export default class extends Extension { const res = await this.request('', { headers: { 'Content-Type': 'application/json', - 'Miru-Url': this.fixUrl(this.#opts.exturl) + 'Miru-Url': this.#opts.exturl } }); return (this.#cache.exturl = res); } async load() { - const opts = this.#opts.lists; - for(let item in opts) { - opts[item] = this.fixUrl(opts[item]); - } - const res = await this.cacheJSON(); - Object.assign(opts, res || {}); + const lists = this.#opts.lists; + Object.assign(lists, (await this.cacheJSON()) || {}); await this.registerSetting({ title: 'Built-in Source', @@ -81,7 +63,7 @@ export default class extends Extension { type: 'radio', description: 'Choose the `Custom Source` below when you choose "None"', defaultValue: '', - options: opts + options: lists }); await this.registerSetting({ title: 'Custom Source', @@ -161,24 +143,31 @@ export default class extends Extension { const res = (await this.req(baseUrl)) .replace(/\r?\n/g, '\n') .replace(/\n+/g, '\n') + // fix + .replace(/\.m3u8\?\n([\w\=\-&]+)\n/g, '.m3u8?$1\n') + .replace(/\.m3u8\n\?([\w\=\-&]+)\n/g, '.m3u8?$1\n') + .replace(/^(#EXTINF:\-?[\d\.]+) *\,/gmi, '$1 ') + .replace(/^(#EXTINF:\-?[\d\.]+) ([^,]+)$/gmi, '$1,$2') .trim(); const ext = baseUrl.slice(baseUrl.lastIndexOf('.')).toLowerCase(); const content = res.split('\n'); - let bangumi = []; + + const repeats = {}; if (~res.search(/#genre#/i) || ext === '.txt') { let group, tmp; - const repeats = {}; content.forEach((item) => { - tmp = item.split(','); - if (tmp.length !== 2) { + // 有 genre 标记取 group 名称 + if (~item.search(/,#genre#$/i)) { + group = item.split(',#')[0]; return; } - const [title, url] = tmp; - if (url === '#genre#') { - group = title; + // 无分隔 标记取 group 名称 + if (!~item.search(/,/) && !~item.search(/(?:https?|rs[tcm]p|rsp|mms|udp)/)) { + group = item; return; } + let [title, url] = item.split(','); // 组合相同名称的台 if (title in repeats) { repeats[title].url += '#' + url; @@ -191,9 +180,7 @@ export default class extends Extension { group } }); - // 组合相同名称的台 - bangumi = Object.values(repeats); - } else if (~res.search(/#EXT(?:M3U|INF)/i) || ext === '.m3u') { + } else if (~res.search(/#EXT(?:M3U|INF)/i) || ['.m3u', '.m3u8'].includes(ext)) { let title, cover, group; let headers = {}; const vlcopt = { @@ -209,19 +196,26 @@ export default class extends Extension { for (let v in vlcopt) if (item.startsWith(vlcopt[v])) { headers[v] = item.slice(vlcopt[v].length); } - } else if (title && ~item.search(/^(?:https?|rs[tcm]p|rsp|mms)/) && !~item.search(/\.mpd/)) { - bangumi.push({ + } else if (title && ~item.search(/^(?:https?|rs[tcm]p|rsp|mms|udp)/) && !~item.search(/\.mpd/)) { + // 组合相同名称的台 + if (title in repeats && repeats[title].group === group) { + repeats[title].url += '#' + item.trim(); + return; + } + repeats[title] = { title, url: item.trim(), cover, group, headers, - }); + } title = ''; headers = {}; } }); } + // 组合相同名称的台 + const bangumi = Object.values(repeats) || []; this.#cache.uptime = Date.now(); return (this.#cache.items = this.#cache.res[md5path] = bangumi); } @@ -230,7 +224,7 @@ export default class extends Extension { if (page > 1) { return []; } - !~this.#cache.items.length && await this.latest(); + !~this.#cache.items.length && (await this.latest()); const filt = filter?.data && filter.data[0] || this.#group.val; const bangumi = this.#cache.items; if (filt === this.#group.val) { diff --git a/repo/club.everia.js b/repo/club.everia.js index 077ccb6..de1332b 100644 --- a/repo/club.everia.js +++ b/repo/club.everia.js @@ -1,6 +1,6 @@ // ==MiruExtension== // @name EVERIA.CLUB[Photo] -// @version v0.0.2 +// @version v0.0.3 // @author vvsolo // @lang all // @license MIT @@ -12,56 +12,40 @@ // ==/MiruExtension== export default class extends Extension { - #genres = {}; - #baseUrl = 'https://everia.club'; - #cacheCover = {}; - - async queryAll(res, selector, func) { - const finds = await Promise.all( - (await this.querySelectorAll(res, selector)).map(async (v, i) => { - const html = await v.content; - return await func(html, i); - }) - ); - return finds || []; + #opts = { + base: 'https://everia.club', + uptime: 0, + expire: 5, } - + #cache = new Map([ + ['@cover', {}] + ]); + async createFilter(filter) { - const res = await this.request(`/`); - const cates = {"All": "All"}; - await this.queryAll(res, '#menu-mainmenu > li.menu-item', async (html) => { - let title = await this.querySelector(html, 'a').text; - title = title.trim(); - cates[title] = title; - this.#genres[title] = await this.getAttributeText(html, 'a', 'href'); - }) + if (!this.checkCache('@genres')) { + const res = await this.request(`/`); + let genres = { + "All": "All" + }; + await this.queryAll(res, '#menu-mainmenu > li.menu-item', async (html) => { + const title = (await this.querySelector(html, 'a').text || '').trim(); + let href = await this.getAttributeText(html, 'a', 'href'); + href = href.replace(this.#opts.base, ''); + genres[href] = title; + }) + this.#cache.set('@genres', genres); + } return { "data": { title: "Category", max: 1, min: 1, default: "All", - options: cates, + options: this.#cache.get('@genres'), } } } - async getMangas(path) { - const res = await this.request(path); - return await this.queryAll(res, '#content .thumbnail', async (html) => { - let title = await this.getAttributeText(html, 'img', 'alt'); - const url = await this.getAttributeText(html, 'a', 'href'); - const cover = await this.getAttributeText(html, 'img', 'src'); - title = title.trim().replace('Read more about the article ', ''); - this.#cacheCover[url] = cover; - return { - title, - url, - cover - } - }) - } - async latest(page) { return await this.getMangas(`/page/${page}/`); } @@ -69,24 +53,25 @@ export default class extends Extension { async search(kw, page, filter) { const filt = filter?.data && filter.data[0] || 'All'; let seaKW = `/page/${page}/`; + if (filt != 'All') { + seaKW = filt + seaKW; + } if (kw) { - seaKW += `?s=${kw}`; - } else if (filt != 'All' && filt in this.#genres) { - seaKW = this.#genres[filt] + seaKW; + seaKW = `/page/${page}/?s=${kw}`; } - return await this.getMangas(seaKW.replace(this.#baseUrl, '')); + return await this.getMangas(seaKW); } async detail(url) { - const res = await this.request(url.replace(this.#baseUrl, '')); + const res = await this.req(url); const title = await this.querySelector(res, 'header > h1').text; - const imgs = await this.queryAll(res, '.wp-block-image', async (html, i) => { + const imgs = await this.queryAll(res, '.wp-block-image', async (html, v, i) => { return { name: `[P${(i + 1 + '').padStart(3, '0')}]`, url: await this.getAttributeText(html, 'img', 'src') } }) - const cover = this.#cacheCover[url] || imgs[0].url || ''; + const cover = this.#cache.get('@cover')[url] || imgs[0].url || ''; return { title: title.trim(), cover, @@ -104,4 +89,48 @@ export default class extends Extension { urls: [url] }; } + + async getMangas(path) { + const md5path = md5(path); + if (this.checkCache(md5path)) { + return this.#cache.get(md5path); + } + const res = await this.req(path); + const mangas = await this.queryAll(res, '#content .thumbnail', async (html) => { + let title = await this.getAttributeText(html, 'img', 'alt'); + const url = await this.getAttributeText(html, 'a', 'href'); + const cover = await this.getAttributeText(html, 'img', 'src'); + title = title.trim().replace('Read more about the article ', ''); + this.#cache.get('@cover')[url] = cover; + return { + title, + url, + cover + } + }) + //this.#cache.clear(); + this.#cache.set(md5path, mangas); + this.#opts.uptime = Date.now(); + return mangas; + } + + async req(path) { + return await this.request(path.replace(this.#opts.base, '')); + } + + async queryAll(res, selector, func) { + return await Promise.all( + (await this.querySelectorAll(res, selector)).map(async (v, i) => { + const html = await v.content; + return await func(html, v, i); + }) + ) || []; + } + + checkCache(item) { + const expire = +(this.#opts.expire); + return this.#cache.has(item) && + expire > 0 && + (Date.now() - this.#opts.uptime) < expire * 60 * 1000; + } } diff --git a/repo/club.manga18.js b/repo/club.manga18.js index 1635f48..f9c12d2 100644 --- a/repo/club.manga18.js +++ b/repo/club.manga18.js @@ -1,6 +1,6 @@ // ==MiruExtension== // @name manga18.club -// @version v0.0.2 +// @version v0.0.3 // @author vvsolo // @lang all // @license MIT @@ -12,86 +12,58 @@ // ==/MiruExtension== export default class extends Extension { - - #sources = { - '[en]manga18.club': 'https://manga18.club', - '[zh-CN]hanman18.com': 'https://hanman18.com', - '[fr]tumanhwas.club': 'https://tumanhwas.club', - '[fr]leercapitulo.net': 'https://leercapitulo.net', - //'[en]comic1000.com': 'https://comic1000.com', - //'[en]18porncomic.com': 'https://18porncomic.com', - //'[en]manga18.us': 'https://manga18.us', - //'[en]manhuascan.us': 'https://manhuascan.us', - }; - - #genres = {}; - #cacheCover = {}; - - async queryAll(res, selector, func) { - const finds = await Promise.all( - (await this.querySelectorAll(res, selector)).map(async (v, i) => { - const html = await v.content; - return await func(html, i); - }) - ); - return finds || []; + #opts = { + base: 'https://manga18.club', + sources: { + '[en]manga18.club': 'https://manga18.club', + '[zh-CN]hanman18.com': 'https://hanman18.com', + '[fr]tumanhwas.club': 'https://tumanhwas.club', + '[fr]leercapitulo.net': 'https://leercapitulo.net', + //'[en]comic1000.com': 'https://comic1000.com', + //'[en]18porncomic.com': 'https://18porncomic.com', + //'[en]manga18.us': 'https://manga18.us', + //'[en]manhuascan.us': 'https://manhuascan.us', + }, + uptime: 0, + expire: 5, } - + #cache = new Map([ + ['@cover', {}] + ]); + async load() { await this.registerSetting({ title: 'Source', key: 'source', type: 'radio', - defaultValue: 'https://manga18.club', - options: this.#sources + defaultValue: this.#opts.base, + options: this.#opts.sources }); } - async req(path) { - const baseUrl = await this.getSetting('source'); - if (~path.indexOf(baseUrl)) path = path.replace(baseUrl, ''); - return await this.request('', { - headers: { - //'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36', - 'Miru-Url': baseUrl + path - } - }); - } - async createFilter(filter) { - const res = await this.req(`/list-manga`); - const cates = {"All": "All"}; - await this.queryAll(res, '.grid_cate > ul > li', async (html) => { - let title = await this.querySelector(html, 'a').text; - title = title.trim(); - cates[title] = title; - this.#genres[title] = await this.getAttributeText(html, 'a', 'href'); - }) + if (!this.checkCache('@genres')) { + const res = await this.req(`/list-manga`); + let genres = { + "All": "All" + }; + await this.queryAll(res, '.grid_cate > ul > li', async (html) => { + const title = (await this.querySelector(html, 'a').text || '').trim(); + const href = await this.getAttributeText(html, 'a', 'href'); + genres[href] = title + }) + this.#cache.set('@genres', genres); + } return { "data": { - title: "GENRES", + title: "Genres", max: 1, min: 0, default: "All", - options: cates, + options: this.#cache.get('@genres'), } } } - - async getMangas(path) { - const res = await this.req(path); - return await this.queryAll(res, 'div.story_item > div.story_images', async (html) => { - const title = await this.getAttributeText(html, 'a', 'title'); - const url = await this.getAttributeText(html, 'a', 'href'); - const cover = await this.getAttributeText(html, 'img', 'src'); - this.#cacheCover[url] = cover; - return { - title: title.trim(), - url, - cover - } - }) - } async latest(page) { return await this.getMangas(`/list-manga/${page}`); @@ -99,11 +71,9 @@ export default class extends Extension { async search(kw, page, filter) { const filt = filter?.data && filter.data[0] || 'All'; - let seaKW = `/list-manga/${page}`; + let seaKW = filt === 'All' ? `/page/${page}/` : `/${filt}/${page}/`; if (kw) { - seaKW += `?search=${encodeURIComponent(kw)}`; - } else if (filt != 'All' && filt in this.#genres) { - seaKW = this.#genres[filt] + `/${page}`; + seaKW += `?s=${encodeURIComponent(kw)}`; } return await this.getMangas(seaKW); } @@ -112,14 +82,14 @@ export default class extends Extension { const res = await this.req(url); const title = await this.querySelector(res, '.detail_name > h1').text; const desc = await this.querySelector(res, '.detail_reviewContent').text; - const imgs = await this.queryAll(res, '.chapter_box .item > a', async (html, i) => { + const imgs = await this.queryAll(res, '.chapter_box .item > a', async (html) => { return { name: (await this.querySelector(html, 'a').text || '').trim(), url: await this.getAttributeText(html, 'a', 'href') } }) - const cover = this.#cacheCover[url] || (await this.getAttributeText(res, '.detail_avatar > img', 'src')) || ''; - const subtitle = await this.queryAll(res, '.detail_listInfo > .item', async (html, i) => { + const cover = this.#cache.get('@cover')[url] || (await this.getAttributeText(res, '.detail_avatar > img', 'src')) || ''; + const subtitle = await this.queryAll(res, '.detail_listInfo > .item', async (html) => { const _label = (await this.querySelector(html, '.info_label').text || ''); const _value = (await this.querySelector(html, '.info_value > a').text || await this.querySelector(html, '.info_value > span').text || ''); @@ -160,4 +130,54 @@ export default class extends Extension { } }; } + + async req(path) { + const baseUrl = await this.getSetting('source'); + if (~path.indexOf(baseUrl)) path = path.replace(baseUrl, ''); + return await this.request('', { + headers: { + 'Miru-Url': baseUrl + path + } + }); + } + + async getMangas(path) { + const baseUrl = await this.getSetting('source'); + const md5path = md5(baseUrl + path); + if (this.checkCache(md5path)) { + return this.#cache.get(md5path); + } + const res = await this.req(path); + const mangas = await this.queryAll(res, 'div.story_item > div.story_images', async (html) => { + const title = await this.getAttributeText(html, 'a', 'title'); + const url = await this.getAttributeText(html, 'a', 'href'); + const cover = await this.getAttributeText(html, 'img', 'src'); + this.#cache.get('@cover')[url] = cover; + return { + title: title.trim(), + url, + cover + } + }) + //this.#cache.clear(); + this.#cache.set(md5path, mangas); + this.#opts.uptime = Date.now(); + return mangas; + } + + async queryAll(res, selector, func) { + return await Promise.all( + (await this.querySelectorAll(res, selector)).map(async (v, i) => { + const html = await v.content; + return await func(html, v, i); + }) + ) || []; + } + + checkCache(item) { + const expire = +(this.#opts.expire); + return this.#cache.has(item) && + expire > 0 && + (Date.now() - this.#opts.uptime) < expire * 60 * 1000; + } } diff --git a/repo/com.freexcomic.js b/repo/com.freexcomic.js index e31308f..e90e7b4 100644 --- a/repo/com.freexcomic.js +++ b/repo/com.freexcomic.js @@ -1,6 +1,6 @@ // ==MiruExtension== // @name 愛看漫畫 -// @version v0.0.1 +// @version v0.0.2 // @author vvsolo // @lang zh-tw // @license MIT @@ -12,112 +12,80 @@ // ==/MiruExtension== export default class extends Extension { - #sources = { - 'mxsmh01.top': 'http://www.mxsmh01.top', - 'mxsmh1.com': 'http://www.mxsmh1.com', - 'mxs2.com': 'http://www.mxs2.com', - 'mxs02.top': 'http://www.mxs02.top', - 'mxs03.top': 'http://www.mxs03.top', - 'mxs04.top': 'http://www.mxs04.top', - '92hm.life': 'http://www.92hm.life' - }; - #cacheCover = {}; - - async queryAll(res, selector, func) { - const finds = await Promise.all( - (await this.querySelectorAll(res, selector)).map(async (v, i) => { - const html = await v.content; - return await func(html, i); - }) - ); - return finds || []; + #opts = { + base: 'http://www.mxsmh01.top', + sources: { + 'mxsmh01.top': 'http://www.mxsmh01.top', + 'mxsmh1.com': 'http://www.mxsmh1.com', + 'mxs2.com': 'http://www.mxs2.com', + 'mxs02.top': 'http://www.mxs02.top', + 'mxs03.top': 'http://www.mxs03.top', + 'mxs04.top': 'http://www.mxs04.top', + '92hm.life': 'http://www.92hm.life' + }, + filter: "/booklist", + filters: { + //"/update": "更新", + "/booklist": "全部", + "/booklist&end=-1": "连载", + "/booklist&end=1": "完结" + }, + uptime: 0, + expire: 12 * 60, } + #cache = new Map([ + ['@cover', {}] + ]); async load() { await this.registerSetting({ title: 'Source', key: 'source', type: 'radio', - defaultValue: 'http://www.mxsmh01.top', - options: this.#sources + defaultValue: this.#opts.base, + options: this.#opts.sources }); } - async req(path) { - const baseUrl = await this.getSetting('source'); - return await this.request('', { - headers: { - 'User-Agent': - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36', - 'Miru-Url': baseUrl + path - } - }); - } - - async getMangas(path) { - const res = await this.req(path); - return await this.queryAll(res, 'div.mh-item > a', async (html) => { - const title = await this.getAttributeText(html, 'a', 'title'); - const url = await this.getAttributeText(html, 'a', 'href'); - const cover = html.match(/background\-image: *url\((.+)\)/)[1] || ''; - this.#cacheCover[url] = cover; - return { - title: title.trim(), - url, - cover - } - }) - } - async latest(page) { return await this.getMangas(`/booklist?page=${page}`); } - async createFilter(filter) { - return { - "data": { - title: "状态", - max: 1, - min: 0, - default: "全部", - options: { - "全部": "全部", - "连载": "连载", - "完结": "完结" - }, - } - } - } async search(kw, page, filter) { if (kw && page > 1) { return []; } - const gens = { - "全部": "All", - "连载": "-1", - "完结": "1" - } - const filt = filter?.data && filter.data[0] || '全部'; - let seaKW = `/booklist?page=${page}`; + const filt = filter?.data && filter.data[0] || this.#opts.filter; + let seaKW = filt + `?page=${page}`; if (kw) { seaKW = `/search?keyword=${encodeURIComponent(kw)}`; - } else if (filt != '全部') { - seaKW += `&end=` + gens[filt]; } return await this.getMangas(seaKW); } + async createFilter(filter) { + return { + "data": { + title: "", + max: 1, + min: 1, + default: this.#opts.filter, + options: this.#opts.filters, + } + } + } + async detail(url) { const res = await this.req(url); const title = await this.querySelector(res, '.info > h1').text; const desc = await this.querySelector(res, 'p.content').text; - const imgs = await this.queryAll(res, '#detail-list-select > li', async (html, i) => { + const imgs = await this.queryAll(res, '#detail-list-select > li', async (html) => { return { name: (await this.querySelector(html, 'a').text || '').trim(), url: await this.getAttributeText(html, 'a', 'href') } }) - const cover = this.#cacheCover[url] || (await this.getAttributeText(res, '.cover > img', 'src')) || ''; + const cover = this.#cache.get('@cover')[url] || (await this.getAttributeText(res, '.cover > img', 'src')) || ''; const subtitle = []; (await this.querySelector(res, '.info').content || '').replace(/

(.+?)<\/p>/g, (m, m1) => { subtitle.push(m1.replace(/&/g, '&')); @@ -127,12 +95,10 @@ export default class extends Extension { title: title.trim(), cover, desc: subtitle.join('\n'), - episodes: [ - { - title: 'Directory', - urls: imgs.reverse() - } - ] + episodes: [{ + title: 'Directory', + urls: imgs.reverse() + }] }; } @@ -143,4 +109,55 @@ export default class extends Extension { urls }; } -} + + async getMangas(path) { + const baseUrl = await this.getSetting('source'); + const md5path = md5(baseUrl + path); + if (this.checkCache(md5path)) { + return this.#cache.get(md5path); + } + const res = await this.req(path); + const mangas = await this.queryAll(res, 'div.mh-item > a', async (html) => { + const title = await this.getAttributeText(html, 'a', 'title'); + const url = await this.getAttributeText(html, 'a', 'href'); + const cover = html.match(/background\-image: *url\((.+)\)/)[1] || ''; + this.#cache.get('@cover')[url] = cover; + return { + title: title.trim(), + url, + cover + } + }) + //this.#cache.clear(); + this.#cache.set(md5path, mangas); + this.#opts.uptime = Date.now(); + return mangas; + } + + async req(path) { + const baseUrl = await this.getSetting('source'); + if (~path.indexOf(baseUrl)) path = path.replace(baseUrl, ''); + return await this.request('', { + headers: { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36', + 'Miru-Url': baseUrl + path + } + }); + } + + async queryAll(res, selector, func) { + return await Promise.all( + (await this.querySelectorAll(res, selector)).map(async (v, i) => { + const html = await v.content; + return await func(html, v, i); + }) + ) || []; + } + + checkCache(item) { + const expire = +(this.#opts.expire); + return this.#cache.has(item) && + expire > 0 && + (Date.now() - this.#opts.uptime) < expire * 60 * 1000; + } +} \ No newline at end of file diff --git a/repo/com.manga18fx.js b/repo/com.manga18fx.js new file mode 100644 index 0000000..6979341 --- /dev/null +++ b/repo/com.manga18fx.js @@ -0,0 +1,143 @@ +// ==MiruExtension== +// @name manga18fx +// @version v0.0.1 +// @author vvsolo +// @lang en +// @license MIT +// @type manga +// @icon https://manga18fx.com/images/favicon-96x96.jpg +// @package com.manga18fx +// @webSite https://manga18fx.com +// @nsfw true +// ==/MiruExtension== + +export default class extends Extension { + #opts = { + base: 'https://manga18fx.com', + uptime: 0, + expire: 5, + } + #cache = new Map([ + ['@cover', {}] + ]); + + async latest(page) { + return await this.getMangas(`/page/${page}?orderby=latest`); + } + + async search(kw, page, filter) { + const filt = filter?.data && filter.data[0] || 'All'; + let seaKW = filt === 'All' ? `/page/${page}?orderby=latest` : `${filt}/${page}`; + if (kw) { + seaKW = `/search?q=${kw}&page=${page}`; + } + return await this.getMangas(seaKW); + } + + async createFilter(filter) { + if (!this.checkCache('@genres')) { + const res = await this.request(`/`); + let genres = { + "All": "All" + }; + await this.queryAll(res, '.genre-menu > ul > li', async (html) => { + let title = (await this.querySelector(html, 'a').text || '').trim(); + const href = await this.getAttributeText(html, 'a', 'href'); + genres[href] = title; + }) + this.#cache.set('@genres', genres); + } + return { + "data": { + title: "Genre", + max: 1, + min: 1, + default: "All", + options: this.#cache.get('@genres'), + } + } + } + + async detail(url) { + const res = await this.req(url); + const title = (await this.querySelector(res, '.post-title > h1').text).trim(); + const desc = (await this.querySelector(res, '.dsct > p').text).trim(); + const imgs = await this.queryAll(res, '.row-content-chapter li.a-h', async (html) => { + return { + name: await this.querySelector(html, 'a').text, + url: await this.getAttributeText(html, 'a', 'href'), + } + }) + const cover = this.#cache.get('@cover')[url] || (await this.getAttributeText(res, 'img.img-loading', 'data-src')) || ''; + const info = (await this.queryAll(res, '.post-content .post-content_item', async (html) => { + return html.replace(/<\/?[^>]+>/g, '').replace(/\n/g, ' ').replace(/&/g, '&').trim(); + }) || []).join('\n'); + return { + title: title.trim(), + cover, + desc: info + '\n\n' + desc, + episodes: [{ + title: 'Graphis', + urls: imgs + }] + }; + } + + async watch(url) { + const res = await this.req(url); + const urls = (await this.queryAll(res, '.page-break', async (html) => { + return await this.getAttributeText(html, 'img', 'data-src'); + })) || []; + return { + urls + }; + } + + async getMangas(path) { + const md5path = md5(path); + if (this.checkCache(md5path)) { + return this.#cache.get(md5path); + } + const res = await this.request(path); + const mangas = await this.queryAll(res, '.listupd .page-item', async (html) => { + let title = await this.getAttributeText(html, '.thumb-manga img', 'alt'); + const url = await this.getAttributeText(html, '.thumb-manga a', 'href'); + const cover = await this.getAttributeText(html, '.thumb-manga img', 'data-src'); + let update = await this.queryAll(html, '.btn-link', async (v) => { + return v.match(/class="btn-link">([^<]+)<\/a>/)[1].trim(); + }) || []; + title = title.trim(); + this.#cache.get('@cover')[url] = cover; + return { + title, + url, + cover, + update: update.join('\n'), + } + }) + //this.#cache.clear(); + this.#cache.set(md5path, mangas); + this.#opts.uptime = Date.now(); + return mangas; + } + + async req(path) { + return await this.request(path.replace(this.#opts.base, '')); + } + + async queryAll(res, selector, func) { + return await Promise.all( + (await this.querySelectorAll(res, selector)).map(async (v, i) => { + const html = await v.content; + return await func(html, v, i); + }) + ) || []; + } + + checkCache(item) { + const expire = +(this.#opts.expire); + return this.#cache.has(item) && + expire > 0 && + (Date.now() - this.#opts.uptime) < expire * 60 * 1000; + } +} \ No newline at end of file