From dd22ecf25dccc5d08a5b10c90a28862748086d00 Mon Sep 17 00:00:00 2001 From: siritami <102145692+FiorenMas@users.noreply.github.com> Date: Thu, 9 Jul 2026 00:14:47 +0700 Subject: [PATCH 1/3] Add Cosplaytele --- plugins/english/CosplayTele/index.ts | 318 ++++++++++++++++++ plugins/english/CosplayTele/webview/index.ts | 323 +++++++++++++++++++ plugins/index.ts | 54 ++-- public/static/src/en/cosplaytele/icon.png | Bin 0 -> 3983 bytes 4 files changed, 669 insertions(+), 26 deletions(-) create mode 100644 plugins/english/CosplayTele/index.ts create mode 100644 plugins/english/CosplayTele/webview/index.ts create mode 100644 public/static/src/en/cosplaytele/icon.png diff --git a/plugins/english/CosplayTele/index.ts b/plugins/english/CosplayTele/index.ts new file mode 100644 index 00000000..c9516757 --- /dev/null +++ b/plugins/english/CosplayTele/index.ts @@ -0,0 +1,318 @@ +import { fetchText } from '@libs/fetch'; +import { Plugin } from '@/types/plugin'; +import { load as loadCheerio } from 'cheerio'; +import { defaultCover } from '@libs/defaultCover'; +import { NovelStatus } from '@libs/novelStatus'; +import { FilterTypes, Filters } from '@libs/filterInputs'; +import { decodeHtmlEntities, encodeHtmlEntities } from '@libs/utils'; +import { isUrlAbsolute } from '@libs/isAbsoluteUrl'; + +const SITE = 'https://cosplaytele.com'; + +const PHOTOS_SUFFIX = '#cosplaytele-photos'; +const VIDEO_SUFFIX = '#cosplaytele-video'; + +const levelCosplayOptions = [ + { label: 'All', value: '' }, + { label: 'Cosplay Nude', value: `${SITE}/category/nude/` }, + { label: 'Cosplay Ero', value: `${SITE}/category/cosplay-ero/` }, + { label: 'Cosplay', value: `${SITE}/category/cosplay/` }, +]; + +const topCosplayOptions = [ + { label: 'All', value: '' }, + { label: '24 hours', value: `${SITE}/24-hours/` }, + { label: '3 day', value: `${SITE}/3-day/` }, + { label: '7 Day', value: `${SITE}/7-day/` }, +]; + +function cleanTitle(raw: string): string { + let title = decodeHtmlEntities(raw).trim(); + title = title.replace(/\s*[“"']?\d+\s+photos?(?:\s+and\s+\d+\s+videos?)?[”"']?\s*$/i, ''); + title = title.replace(/\s*[“"']?\d+\s+videos?(?:\s+and\s+\d+\s+photos?)?[”"']?\s*$/i, ''); + title = title.replace(/[“"”]/g, '').trim(); + return title; +} + +function toPath(url: string): string { + if (!url) return '/'; + if (!isUrlAbsolute(url)) return url.startsWith('/') ? url : `/${url}`; + try { + const u = new URL(url); + if (u.origin === SITE) return `${u.pathname}${u.search}`; + return url; + } catch { + return url; + } +} + +function buildListUrl( + pageNo: number, + levelCosplay: string, + topCosplay: string, +): string { + const base = levelCosplay || topCosplay || `${SITE}/`; + const normalized = base.endsWith('/') ? base : `${base}/`; + if (pageNo <= 1) return normalized; + if (normalized === `${SITE}/`) { + return `${SITE}/page/${pageNo}/`; + } + return `${normalized}page/${pageNo}/`; +} + +function parseNovelCards($: ReturnType): Plugin.NovelItem[] { + const novels: Plugin.NovelItem[] = []; + const seen = new Set(); + + $('.large-10 .box-blog-post').each((_, box) => { + const $box = $(box); + const $link = $box.find('.post-title a, h5 a').first(); + const href = $link.attr('href'); + if (!href || href === '#') return; + const path = toPath(href); + if (seen.has(path)) return; + seen.add(path); + + const rawName = $link.text().trim() || $link.attr('title') || ''; + const name = cleanTitle(rawName); + if (!name) return; + + let cover = + $box.find('.box-image img').attr('data-src') || + $box.find('.box-image img').attr('src') || + $box.find('img').first().attr('src') || + defaultCover; + if (cover && !isUrlAbsolute(cover)) { + cover = SITE + cover; + } + + novels.push({ name, path, cover }); + }); + + return novels; +} + +function extractAuthor($: ReturnType): string { + const cosplayerLink = $('blockquote a[href*="/category/"]').first().text().trim(); + if (cosplayerLink) return cosplayerLink; + + const block = $('blockquote').first().text(); + const match = block.match(/Cosplayer:\s*([^\n\r]+)/i); + return match?.[1]?.trim() || ''; +} + +function extractDownloadSummary($: ReturnType): string { + const urls: string[] = []; + const $content = $('.entry-content').first(); + + $content.find('a.button, a[class*="button"]').each((_, el) => { + const $a = $(el); + const text = $a.find('span').first().text().trim() || $a.text().trim(); + if (!/download/i.test(text)) return; + const href = ($a.attr('href') || '').trim(); + if (href && href !== '#') { + urls.push(href); + } + }); + + if (!urls.length) return ''; + return `Download link:\n${urls.join('\n')}`; +} + +function collectPhotoUrls($: ReturnType): string[] { + const urls: string[] = []; + const seen = new Set(); + const $content = $('.entry-content').first(); + + $content.find('figure a[href]').each((_, el) => { + const href = $(el).attr('href')?.trim(); + if (!href || !/\.(webp|jpe?g|png|gif)(\?|$)/i.test(href)) return; + const full = isUrlAbsolute(href) ? href : SITE + href; + if (seen.has(full)) return; + seen.add(full); + urls.push(full); + }); + + return urls; +} + +function collectVideoEmbeds($: ReturnType): string[] { + const embeds: string[] = []; + const seen = new Set(); + const $content = $('.entry-content').first(); + + $content.find('iframe[src]').each((_, el) => { + const src = $(el).attr('src')?.trim(); + if (!src || /googletagmanager/i.test(src)) return; + if (seen.has(src)) return; + seen.add(src); + embeds.push(src); + }); + + return embeds; +} + +class CosplayTelePlugin implements Plugin.PluginBase { + id = 'cosplaytele'; + name = '🖼️ CosplayTele'; + icon = 'src/en/cosplaytele/icon.png'; + site = SITE; + version = '1.0.0'; + + customJS = 'src/en/cosplaytele/custom.js'; + + imageRequestInit: Plugin.ImageRequestInit = { + headers: { + Referer: `${SITE}/`, + }, + }; + + filters = { + levelCosplay: { + label: 'Level Cosplay', + type: FilterTypes.Picker, + value: '', + options: levelCosplayOptions, + }, + topCosplay: { + label: 'Top Cosplay', + type: FilterTypes.Picker, + value: '', + options: topCosplayOptions, + }, + } satisfies Filters; + + private async fetchPage(url: string): Promise { + return fetchText(url.startsWith('http') ? url : `${SITE}${url}`); + } + + async popularNovels( + pageNo: number, + { filters }: Plugin.PopularNovelsOptions, + ): Promise { + const level = filters.levelCosplay.value as string; + const top = filters.topCosplay.value as string; + const url = buildListUrl(pageNo, level, top); + const html = await this.fetchPage(url); + const $ = loadCheerio(html); + return parseNovelCards($); + } + + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise { + const term = encodeURIComponent(searchTerm.trim()); + const url = + pageNo > 1 + ? `${SITE}/page/${pageNo}/?s=${term}` + : `${SITE}/?s=${term}`; + const html = await this.fetchPage(url); + const $ = loadCheerio(html); + return parseNovelCards($); + } + + async parseNovel(novelPath: string): Promise { + const path = novelPath.split('#')[0]; + const html = await this.fetchPage(path); + const $ = loadCheerio(html); + + const rawTitle = + $('h1.entry-title').first().text().trim() || + $('meta[property="og:title"]').attr('content') || + 'Untitled'; + const name = cleanTitle(rawTitle); + + const author = extractAuthor($); + const genres: string[] = []; + $('.entry-category a, h6.entry-category a').each((_, el) => { + const g = $(el).text().trim(); + if (g) genres.push(g); + }); + + let cover = + $('meta[property="og:image"]').attr('content') || + $('.entry-content figure img').first().attr('src') || + defaultCover; + if (cover && !isUrlAbsolute(cover)) cover = SITE + cover; + + const summary = extractDownloadSummary($); + const embeds = collectVideoEmbeds($); + + const chapters: Plugin.ChapterItem[] = [ + { + name: 'Photos', + path: `${path}${PHOTOS_SUFFIX}`, + chapterNumber: 1, + }, + ]; + + if (embeds.length > 0) { + chapters.push({ + name: 'Video', + path: `${path}${VIDEO_SUFFIX}`, + chapterNumber: 2, + }); + } + + return { + path, + name, + author, + artist: author, + cover, + genres: genres.join(', '), + status: NovelStatus.Completed, + summary, + chapters, + }; + } + + async parseChapter(chapterPath: string): Promise { + if (chapterPath.endsWith(PHOTOS_SUFFIX)) { + const novelPath = chapterPath.slice(0, -PHOTOS_SUFFIX.length); + const html = await this.fetchPage(novelPath); + const $ = loadCheerio(html); + const photos = collectPhotoUrls($); + if (!photos.length) { + return '

No photos found.

'; + } + const imgs = photos + .map( + url => + `
`, + ) + .join('\n'); + return `
${imgs}
`; + } + + if (chapterPath.endsWith(VIDEO_SUFFIX)) { + const novelPath = chapterPath.slice(0, -VIDEO_SUFFIX.length); + const html = await this.fetchPage(novelPath); + const $ = loadCheerio(html); + const embeds = collectVideoEmbeds($); + if (!embeds.length) { + return '

No embedded video on this post.

'; + } + + const embedJson = encodeHtmlEntities(JSON.stringify(embeds)); + return [ + '', + '', + ``, + '', + '', + ].join('\n'); + } + + return '

Unknown chapter.

'; + } + + resolveUrl(path: string, isNovel?: boolean): string { + const clean = path.split('#')[0]; + if (isUrlAbsolute(clean)) return clean; + return `${SITE}${clean.startsWith('/') ? clean : `/${clean}`}`; + } +} + +export default new CosplayTelePlugin(); \ No newline at end of file diff --git a/plugins/english/CosplayTele/webview/index.ts b/plugins/english/CosplayTele/webview/index.ts new file mode 100644 index 00000000..7dff7139 --- /dev/null +++ b/plugins/english/CosplayTele/webview/index.ts @@ -0,0 +1,323 @@ +/// + +const SITE = 'https://cosplaytele.com'; + +type EmbedPlayerWindow = Window & { + LNReaderPlayer?: { + log: (msg: string) => void; + playHls: (url: string, customHlsConfig?: Record) => void; + playIframe: (url: string) => void; + playDirect: (url: string) => void; + }; + reader?: { + fetch: (url: string, init?: RequestInit) => Promise; + }; +}; + +function log(msg: string) { + const w = window as EmbedPlayerWindow; + w.LNReaderPlayer?.log(`[CosplayTele] ${msg}`); +} + +async function fetchText(url: string, referer: string): Promise { + const w = window as EmbedPlayerWindow; + const init: RequestInit = { + headers: { + Referer: referer, + 'User-Agent': navigator.userAgent, + }, + }; + if (w.reader?.fetch) { + const res = await w.reader.fetch(url, init); + return res.text(); + } + const res = await fetch(url, init); + return res.text(); +} + +function extractM3u8FromHtml(html: string): string | null { + const patterns = [ + /file:\s*['"]([^'"]+\.m3u8[^'"]*)['"]/i, + /['"](https?:\/\/[^'"]+\.m3u8[^'"]*)['"]/i, + /(https?:\/\/[^\s"'<>]+\/index\.m3u8[^\s"'<>]*)/i, + /(https?:\/\/[^\s"'<>]+\.m3u8[^\s"'<>]*)/i, + ]; + for (const re of patterns) { + const m = html.match(re); + if (m?.[1]) return m[1].replace(/\\u002F/g, '/'); + } + return null; +} + +function extractEncryptedPayload(html: string): { + ciphertext: string; + key: string; +} | null { + const payload = html.match(/const\s+videoURL\s*=\s*['"]([^'"]+)['"]/); + const key = html.match( + /decryptLink\s*\(\s*videoURL\s*,\s*['"]([0-9a-f]{32})['"]\s*\)/i, + ); + if (!payload?.[1] || !key?.[1]) return null; + return { ciphertext: payload[1], key: key[1] }; +} + +function base64ToBytes(b64: string): Uint8Array { + const binary = atob(b64); + const out = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i); + return out; +} + +/** cossora.stream embed: AES-CBC (CryptoJS-compatible) with IV prepended to ciphertext */ +async function decryptCossoraLink( + encryptedB64: string, + keyUtf8: string, +): Promise { + try { + const raw = base64ToBytes(encryptedB64); + if (raw.length < 32) return null; + const iv = raw.slice(0, 16); + const data = raw.slice(16); + const keyBytes = new TextEncoder().encode(keyUtf8); + const cryptoKey = await crypto.subtle.importKey( + 'raw', + keyBytes, + { name: 'AES-CBC' }, + false, + ['decrypt'], + ); + const plain = await crypto.subtle.decrypt( + { name: 'AES-CBC', iv }, + cryptoKey, + data, + ); + const url = new TextDecoder().decode(plain).trim(); + return url || null; + } catch { + return null; + } +} + +async function resolveEmbedM3u8(embedUrl: string): Promise { + const html = await fetchText(embedUrl, `${SITE}/`); + const direct = extractM3u8FromHtml(html); + if (direct) return direct; + + const encrypted = extractEncryptedPayload(html); + if (encrypted) { + const decrypted = await decryptCossoraLink( + encrypted.ciphertext, + encrypted.key, + ); + if (decrypted && /\.m3u8/i.test(decrypted)) return decrypted; + if (decrypted && /^https?:\/\//i.test(decrypted)) return decrypted; + } + + return null; +} + +/** + * Fetch m3u8 sub-playlist via reader.fetch to bypass CORS, + * create blob URL, and use custom loader for segments. + * Skips the master m3u8 — passes the media playlist directly. + */ +async function playHlsViaReader(m3u8Url: string, embedUrl: string): Promise { + const w = window as EmbedPlayerWindow; + if (!w.LNReaderPlayer) return; + + const referer = new URL(embedUrl).origin + '/'; + + // Fetch the master m3u8 to find the sub-playlist URL + const masterText = await fetchText(m3u8Url, referer); + log(`Master m3u8 length: ${masterText.length}`); + + // Parse sub-playlist URL from master + let subUrl = ''; + const lines = masterText.split('\n'); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed && !trimmed.startsWith('#') && /^https?:\/\//i.test(trimmed)) { + subUrl = trimmed; + break; + } + } + + if (!subUrl) { + log('No sub-playlist found in master, trying master as media playlist'); + // Master might already be a media playlist — use it directly + await playMediaPlaylist(masterText, m3u8Url, referer); + return; + } + + // Fetch the sub-playlist (media playlist) via reader.fetch + const subText = await fetchText(subUrl, referer); + log(`Sub-playlist length: ${subText.length}`); + + await playMediaPlaylist(subText, subUrl, referer); +} + +/** + * Take a media playlist text, rewrite relative URIs to absolute, + * create a blob URL, and start playback with CORS-bypassing loaders. + */ +async function playMediaPlaylist( + playlistText: string, + playlistUrl: string, + referer: string, +): Promise { + const w = window as EmbedPlayerWindow; + if (!w.LNReaderPlayer) return; + + const subUrlObj = new URL(playlistUrl); + const subBase = subUrlObj.origin + subUrlObj.pathname.replace(/\/[^/]*$/, '/'); + + // Step 1: Pre-fetch AES-128 key(s) via reader.fetch → blob URLs + // hls.js loads keys through its own internal loader (not fLoader), + // so we must replace key URIs with blob URLs to avoid CORS. + const keyBlobMap = new Map(); + for (const [, rawUri] of playlistText.matchAll(/URI="([^"]+)"/gi)) { + if (keyBlobMap.has(rawUri)) continue; + const absoluteKeyUrl = /^https?:\/\//i.test(rawUri) + ? rawUri + : subBase + rawUri; + try { + const res = await window.reader?.fetch(absoluteKeyUrl, { + headers: { Referer: referer }, + }); + if (res?.ok) { + const buf = await res.arrayBuffer(); + const blobUrl = URL.createObjectURL(new Blob([buf])); + keyBlobMap.set(rawUri, blobUrl); + log(`Key fetched → blob: ${blobUrl.substring(0, 50)}...`); + } + } catch (err) { + log(`Key fetch failed: ${(err as Error).message}`); + } + } + + // Step 2: Replace key URIs with blob URLs in playlist text + let rewritten = playlistText; + for (const [rawUri, blobUrl] of keyBlobMap) { + rewritten = rewritten.replace(rawUri, blobUrl); + } + + // Create blob URL for the media playlist + const blobUrl = URL.createObjectURL( + new Blob([rewritten], { type: 'application/vnd.apple.mpegurl' }), + ); + log(`Media playlist blob URL: ${blobUrl.substring(0, 50)}...`); + + // Custom loader that fetches via reader.fetch to bypass CORS + class ProxyLoader { + _config: any; + _controller: AbortController | null = null; + context: any = null; + stats = { + aborted: false, + loaded: 0, + retry: 0, + total: 0, + chunkCount: 0, + bwEstimate: 0, + loading: { start: 0, first: 0, end: 0 }, + parsing: { start: 0, end: 0 }, + buffering: { start: 0, first: 0, end: 0 }, + }; + + constructor(config: any) { + this._config = config; + } + + destroy() { + this.abort(); + } + + abort() { + this._controller?.abort(); + this._controller = null; + } + + load(ctx: any, _cfg: any, cbs: any) { + this.context = ctx; + this.stats.loading.start = performance.now(); + + window.reader + ?.fetch(ctx.url, { + method: 'GET', + headers: { Referer: referer }, + referrer: referer, + }) + .then((resp) => { + if (!resp.ok) throw new Error(`HTTP ${resp.status}`); + this.stats.loading.first = performance.now(); + return resp.arrayBuffer(); + }) + .then((buf) => { + this.stats.loading.end = performance.now(); + this.stats.loaded = buf.byteLength; + this.stats.total = buf.byteLength; + cbs.onSuccess({ data: buf }, this.stats, ctx, null); + }) + .catch((err) => { + if (err.name === 'AbortError') return; + this.stats.loading.end = performance.now(); + cbs.onError({ code: 0, text: err.message }, ctx, null, this.stats); + }); + } + + getResponseData(xhr: any) { + return xhr.response; + } + } + + w.LNReaderPlayer.playHls(blobUrl, { + fLoader: ProxyLoader, + }); + + log('HLS player started with CORS-bypassing loaders'); +} + +(async function main() { + const w = window as EmbedPlayerWindow; + if (!w.LNReaderPlayer) return; + + const root = document.getElementById('cosplaytele-player'); + if (!root) { + log('Missing player root.'); + return; + } + + let embeds: string[] = []; + try { + const raw = root.getAttribute('data-embeds') || '[]'; + embeds = JSON.parse(raw) as string[]; + } catch (e) { + log(`Invalid embed list: ${(e as Error).message}`); + return; + } + + if (!embeds.length) { + log('No embed URLs.'); + return; + } + + for (let i = 0; i < embeds.length; i++) { + const embed = embeds[i]; + log(`Resolving embed ${i + 1}/${embeds.length}...`); + try { + const m3u8Url = await resolveEmbedM3u8(embed); + if (m3u8Url) { + log(`Playing HLS: ${m3u8Url}`); + await playHlsViaReader(m3u8Url, embed); + return; + } + log(`Fallback iframe: ${embed}`); + w.LNReaderPlayer.playIframe(embed); + return; + } catch (err) { + log(`Embed failed: ${(err as Error).message}`); + } + } + + log('All video sources failed.'); +})(); \ No newline at end of file diff --git a/plugins/index.ts b/plugins/index.ts index 81a75909..087743f9 100644 --- a/plugins/index.ts +++ b/plugins/index.ts @@ -2,32 +2,33 @@ import { Plugin } from '@/types/plugin'; import p_0 from '@plugins/japanese/NocSyosetu'; import p_1 from '@plugins/japanese/Pixiv'; import p_2 from '@plugins/korean/Newtoki'; -import p_3 from '@plugins/multi/M3UPlayer'; -import p_4 from '@plugins/vietnamese/AkayTruyen'; -import p_5 from '@plugins/vietnamese/AnimeVietsub'; -import p_6 from '@plugins/vietnamese/AnimeVsub'; -import p_7 from '@plugins/vietnamese/BaoMoi'; -import p_8 from '@plugins/vietnamese/DocTruyenLN'; -import p_9 from '@plugins/vietnamese/HentaiZ'; -import p_10 from '@plugins/vietnamese/JukaNovel'; -import p_11 from '@plugins/vietnamese/KKPhim'; -import p_12 from '@plugins/vietnamese/LNHako'; -import p_13 from '@plugins/vietnamese/LNKuro'; -import p_14 from '@plugins/vietnamese/Luvevaland'; -import p_15 from '@plugins/vietnamese/MeTruyenCV'; -import p_16 from '@plugins/vietnamese/Motchill'; -import p_17 from '@plugins/vietnamese/MotTruyen'; -import p_18 from '@plugins/vietnamese/NguonC'; -import p_19 from '@plugins/vietnamese/OPhim'; -import p_20 from '@plugins/vietnamese/PhimFun'; -import p_21 from '@plugins/vietnamese/SangTacViet'; -import p_22 from '@plugins/vietnamese/TieuThuyetMang'; -import p_23 from '@plugins/vietnamese/TomatoMTL'; -import p_24 from '@plugins/vietnamese/TruyenCV'; -import p_25 from '@plugins/vietnamese/TruyenFull'; -import p_26 from '@plugins/vietnamese/Valvrareteam'; -import p_27 from '@plugins/vietnamese/YeuAnime'; -import p_28 from '@plugins/vietnamese/ZumiNovel'; +import p_3 from '@plugins/english/CosplayTele'; +import p_4 from '@plugins/multi/M3UPlayer'; +import p_5 from '@plugins/vietnamese/AkayTruyen'; +import p_6 from '@plugins/vietnamese/AnimeVietsub'; +import p_7 from '@plugins/vietnamese/AnimeVsub'; +import p_8 from '@plugins/vietnamese/BaoMoi'; +import p_9 from '@plugins/vietnamese/DocTruyenLN'; +import p_10 from '@plugins/vietnamese/HentaiZ'; +import p_11 from '@plugins/vietnamese/JukaNovel'; +import p_12 from '@plugins/vietnamese/KKPhim'; +import p_13 from '@plugins/vietnamese/LNHako'; +import p_14 from '@plugins/vietnamese/LNKuro'; +import p_15 from '@plugins/vietnamese/Luvevaland'; +import p_16 from '@plugins/vietnamese/MeTruyenCV'; +import p_17 from '@plugins/vietnamese/Motchill'; +import p_18 from '@plugins/vietnamese/MotTruyen'; +import p_19 from '@plugins/vietnamese/NguonC'; +import p_20 from '@plugins/vietnamese/OPhim'; +import p_21 from '@plugins/vietnamese/PhimFun'; +import p_22 from '@plugins/vietnamese/SangTacViet'; +import p_23 from '@plugins/vietnamese/TieuThuyetMang'; +import p_24 from '@plugins/vietnamese/TomatoMTL'; +import p_25 from '@plugins/vietnamese/TruyenCV'; +import p_26 from '@plugins/vietnamese/TruyenFull'; +import p_27 from '@plugins/vietnamese/Valvrareteam'; +import p_28 from '@plugins/vietnamese/YeuAnime'; +import p_29 from '@plugins/vietnamese/ZumiNovel'; const PLUGINS: Plugin.PluginBase[] = [ p_0, @@ -59,5 +60,6 @@ const PLUGINS: Plugin.PluginBase[] = [ p_26, p_27, p_28, + p_29, ]; export default PLUGINS; diff --git a/public/static/src/en/cosplaytele/icon.png b/public/static/src/en/cosplaytele/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..fc457dffc90eb8ce09bf44bfb602c3094cb3d635 GIT binary patch literal 3983 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 MeFLLiqiGlZKX#*IkpKVy literal 0 HcmV?d00001 From 18e8b01b5a328781276c6d663b629e84e9452910 Mon Sep 17 00:00:00 2001 From: Elysia <71698422+aiko-chan-ai@users.noreply.github.com> Date: Thu, 9 Jul 2026 00:21:26 +0700 Subject: [PATCH 2/3] Update plugins/english/CosplayTele/webview/index.ts --- plugins/english/CosplayTele/webview/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/english/CosplayTele/webview/index.ts b/plugins/english/CosplayTele/webview/index.ts index 7dff7139..e7c07b8c 100644 --- a/plugins/english/CosplayTele/webview/index.ts +++ b/plugins/english/CosplayTele/webview/index.ts @@ -10,6 +10,7 @@ type EmbedPlayerWindow = Window & { playDirect: (url: string) => void; }; reader?: { + // eslint-disable-next-line fetch: (url: string, init?: RequestInit) => Promise; }; }; From 026f3ff57fa001131e3582a8b2fa4a0e9ffe3fbc Mon Sep 17 00:00:00 2001 From: Elysia <71698422+aiko-chan-ai@users.noreply.github.com> Date: Thu, 9 Jul 2026 00:21:33 +0700 Subject: [PATCH 3/3] Update plugins/english/CosplayTele/webview/index.ts --- plugins/english/CosplayTele/webview/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/english/CosplayTele/webview/index.ts b/plugins/english/CosplayTele/webview/index.ts index e7c07b8c..c1f903df 100644 --- a/plugins/english/CosplayTele/webview/index.ts +++ b/plugins/english/CosplayTele/webview/index.ts @@ -22,6 +22,7 @@ function log(msg: string) { async function fetchText(url: string, referer: string): Promise { const w = window as EmbedPlayerWindow; + // eslint-disable-next-line const init: RequestInit = { headers: { Referer: referer,