From 7b0846ccfcbb0220ce51bf40cce89342b94c7e81 Mon Sep 17 00:00:00 2001 From: FFFold Date: Fri, 17 Jul 2026 21:03:44 +0800 Subject: [PATCH 1/9] fix(route/iwara): migrate API from apiq.iwara.tv to api.iwara.tv - Replace Puppeteer with ofetch for ranking and subscriptions routes - Fix video detail API response format (file vs files) - Handle null slug in user route links - Remove deprecated apiqRootUrl --- lib/routes/iwara/index.ts | 4 +- lib/routes/iwara/ranking.ts | 38 ++--- lib/routes/iwara/subscriptions.ts | 245 ++++++++++++++---------------- lib/routes/iwara/utils.ts | 1 - 4 files changed, 129 insertions(+), 159 deletions(-) diff --git a/lib/routes/iwara/index.ts b/lib/routes/iwara/index.ts index 13ed58f660e7..5bf7702c2288 100644 --- a/lib/routes/iwara/index.ts +++ b/lib/routes/iwara/index.ts @@ -56,8 +56,8 @@ async function handler(ctx) { const items = list.map((item) => ({ title: item.title, author: username, - link: `${rootUrl}/${type}/${item.id}/${item.slug}`, - category: item.tags.map((i) => i.id), + link: `${rootUrl}/${type}/${item.id}${item.slug ? `/${item.slug}` : ''}`, + category: item.tags?.map((i) => i.id) || [], description: parseThumbnail(type, item), pubDate: parseDate(item.createdAt), })); diff --git a/lib/routes/iwara/ranking.ts b/lib/routes/iwara/ranking.ts index 9a5746dacb93..804a872f08c3 100644 --- a/lib/routes/iwara/ranking.ts +++ b/lib/routes/iwara/ranking.ts @@ -1,8 +1,8 @@ import { config } from '@/config'; import type { Route } from '@/types'; import cache from '@/utils/cache'; +import ofetch from '@/utils/ofetch'; import { parseDate } from '@/utils/parse-date'; -import { getPlaywrightPage } from '@/utils/playwright'; import { apiRootUrl, parseThumbnail, rootUrl, typeMap } from './utils'; @@ -32,7 +32,6 @@ export const route: Route = { maintainers: ['CaoMeiYouRen233'], handler, features: { - requirePuppeteer: true, nsfw: true, }, radar: [ @@ -58,33 +57,20 @@ async function handler(ctx) { const items = await cache.tryGet( `iwara:ranking:${type}:${sort}:${rating}`, async () => { - const { page, destroy } = await getPlaywrightPage(url, { - onBeforeLoad: async (page) => { - await page.route('**/*', (route) => { - const request = route.request(); - request.resourceType() === 'document' || request.resourceType() === 'script' || request.resourceType() === 'xhr' || request.resourceType() === 'fetch' ? route.continue() : route.abort(); - }); - }, - gotoConfig: { - waitUntil: 'networkidle', + const response = await ofetch(url, { + headers: { + 'user-agent': config.trueUA, }, }); - try { - const content = await page.evaluate(() => document.querySelector('pre')?.textContent || document.body.textContent); - const response = JSON.parse(content || '{}'); - - return response.results.map((item) => ({ - title: item.title, - author: item.user.name, - link: `${rootUrl}/${type === 'video' ? 'video' : 'image'}/${item.id}${item.slug ? `/${item.slug}` : ''}`, - category: item.tags?.map((i) => i.id) || [], - description: parseThumbnail(type, item), - pubDate: parseDate(item.createdAt), - })); - } finally { - await destroy(); - } + return response.results.map((item) => ({ + title: item.title, + author: item.user.name, + link: `${rootUrl}/${type === 'video' ? 'video' : 'image'}/${item.id}${item.slug ? `/${item.slug}` : ''}`, + category: item.tags?.map((i) => i.id) || [], + description: parseThumbnail(type, item), + pubDate: parseDate(item.createdAt), + })); }, config.cache.routeExpire, false diff --git a/lib/routes/iwara/subscriptions.ts b/lib/routes/iwara/subscriptions.ts index a9fc712e4b50..fd0aa7ee8d78 100644 --- a/lib/routes/iwara/subscriptions.ts +++ b/lib/routes/iwara/subscriptions.ts @@ -5,11 +5,11 @@ import { config } from '@/config'; import ConfigNotFoundError from '@/errors/types/config-not-found'; import type { Route } from '@/types'; import cache from '@/utils/cache'; +import ofetch from '@/utils/ofetch'; import { parseDate } from '@/utils/parse-date'; -import { getPlaywrightPage } from '@/utils/playwright'; import { renderSubscriptionImages } from './templates/subscriptions'; -import { apiqRootUrl, imageRootUrl, rootUrl } from './utils'; +import { apiRootUrl, imageRootUrl, rootUrl } from './utils'; const md = MarkdownIt({ html: true, @@ -40,7 +40,6 @@ export const route: Route = { description: '', }, ], - requirePuppeteer: true, antiCrawler: false, supportBT: false, supportPodcast: false, @@ -69,129 +68,96 @@ async function handler() { const username = config.iwara.username; const password = config.iwara.password; - const { page, destroy } = await getPlaywrightPage(rootUrl, { - gotoConfig: { - waitUntil: 'domcontentloaded', - }, - }); + const fetchJson = (url: string, options: any = {}) => + ofetch(url, { + headers: { + ...apiHeaders, + ...options.headers, + }, + ...(options.body ? { body: JSON.stringify(options.body) } : {}), + ...(options.method ? { method: options.method } : {}), + }); - try { - const fetchApi = (url: string, options: any) => - page.evaluate( - async (args) => { - const res = await fetch(args.url, { - method: args.options.method || 'GET', - headers: args.options.headers, - body: args.options.body ? JSON.stringify(args.options.body) : undefined, - }); - if (!res.ok) { - throw new Error(`HTTP error! status: ${res.status}`); - } - return res.json(); + // login and get refresh token + const refreshHeaders = await cache.tryGet( + 'iwara:token', + async () => { + const result = await fetchJson(`${apiRootUrl}/user/login`, { + method: 'POST', + headers: apiHeaders, + body: { email: username, password }, + }); + return { authorization: 'Bearer ' + result.token }; + }, + 30 * 24 * 60 * 60, + false + ); + + // get access token + const authHeaders = await cache.tryGet( + 'iwara:authToken', + async () => { + const result = await fetchJson(`${apiRootUrl}/user/token`, { + method: 'POST', + headers: { + ...apiHeaders, + Authorization: refreshHeaders.authorization, }, - { url, options } - ); - - // login and get refresh token - const refreshHeaders = await cache.tryGet( - 'iwara:token', - async () => { - const result = await fetchApi(`${apiqRootUrl}/user/login`, { - method: 'POST', - headers: apiHeaders, - body: { email: username, password }, - }); - return { authorization: 'Bearer ' + result.token }; - }, - 30 * 24 * 60 * 60, - false - ); - - // get access token - const authHeaders = await cache.tryGet( - 'iwara:authToken', - async () => { - const result = await fetchApi(`${apiqRootUrl}/user/token`, { - method: 'POST', - headers: { - ...apiHeaders, - Authorization: refreshHeaders.authorization, - }, - }); - return { authorization: 'Bearer ' + result.accessToken }; - }, - 60 * 60, - false - ); + }); + return { authorization: 'Bearer ' + result.accessToken }; + }, + 60 * 60, + false + ); - const authedHeaders = { - ...apiHeaders, - Authorization: authHeaders.authorization, - }; + const authedHeaders = { + ...apiHeaders, + Authorization: authHeaders.authorization, + }; - // fetch subscriptions - const [videoResponse, imageResponse] = await Promise.all([ - fetchApi(`${apiqRootUrl}/videos?rating=all&page=0&limit=24&subscribed=true`, { headers: authedHeaders }), - fetchApi(`${apiqRootUrl}/images?rating=all&page=0&limit=24&subscribed=true`, { headers: authedHeaders }), - ]); - - const videoList = videoResponse.results.map((item) => { - const imageUrl = item.file ? `${imageRootUrl}/image/original/${item.file.id}/thumbnail-${item.thumbnail.toString().padStart(2, '0')}.jpg` : ''; - - return { - title: item.title, - author: item.user.name, - link: `${rootUrl}/video/${item.id}`, - category: ['Video', ...(item.tags ? item.tags.map((i) => i.id) : [])], - imageUrl, - pubDate: parseDate(item.createdAt), - private: item.private, - }; - }); + // fetch subscriptions + const [videoResponse, imageResponse] = await Promise.all([ + fetchJson(`${apiRootUrl}/videos?rating=all&page=0&limit=24&subscribed=true`, { headers: authedHeaders }), + fetchJson(`${apiRootUrl}/images?rating=all&page=0&limit=24&subscribed=true`, { headers: authedHeaders }), + ]); - const imageList = imageResponse.results.map((item) => { - const imageUrl = item.thumbnail ? `${imageRootUrl}/image/original/${item.thumbnail.id}/${item.thumbnail.name}` : ''; - return { - title: item.title, - author: item.user.name, - link: `${rootUrl}/image/${item.id}`, - category: ['Image', ...(item.tags ? item.tags.map((i) => i.id) : [])], - imageUrl, - pubDate: parseDate(item.createdAt), - }; - }); + const videoList = videoResponse.results.map((item) => { + const imageUrl = item.file ? `${imageRootUrl}/image/original/${item.file.id}/thumbnail-${item.thumbnail.toString().padStart(2, '0')}.jpg` : ''; - // fulltext - const list = [...videoList, ...imageList]; - // Execute fetches with limited concurrency - const items = await pMap( - list, - (item) => - cache.tryGet(item.link, async () => { - let description = renderSubscriptionImages([item.imageUrl]); - - if (item.private === true) { - description += 'private'; - return { - title: item.title, - author: item.author, - link: item.link, - category: item.category, - pubDate: item.pubDate, - description, - }; - } - - const apiUrl = item.link.replace('www.iwara.tv', 'apiq.iwara.tv'); - const response = await fetchApi(apiUrl, { - headers: authedHeaders, - }); - - description = renderSubscriptionImages(response.files ? response.files.filter((f) => f.type === 'image').map((f) => `${imageRootUrl}/image/original/${f.id}/${f.name}`) : [item.imageUrl]); - - const body = response.body ? md.render(response.body) : ''; - description += body; + return { + title: item.title, + author: item.user.name, + link: `${rootUrl}/video/${item.id}`, + category: ['Video', ...(item.tags ? item.tags.map((i) => i.id) : [])], + imageUrl, + pubDate: parseDate(item.createdAt), + private: item.private, + }; + }); + + const imageList = imageResponse.results.map((item) => { + const imageUrl = item.thumbnail ? `${imageRootUrl}/image/original/${item.thumbnail.id}/${item.thumbnail.name}` : ''; + return { + title: item.title, + author: item.user.name, + link: `${rootUrl}/image/${item.id}`, + category: ['Image', ...(item.tags ? item.tags.map((i) => i.id) : [])], + imageUrl, + pubDate: parseDate(item.createdAt), + }; + }); + // fulltext + const list = [...videoList, ...imageList]; + // Execute fetches with limited concurrency + const items = await pMap( + list, + (item) => + cache.tryGet(item.link, async () => { + let description = renderSubscriptionImages([item.imageUrl]); + + if (item.private === true) { + description += 'private'; return { title: item.title, author: item.author, @@ -200,16 +166,35 @@ async function handler() { pubDate: item.pubDate, description, }; - }), - { concurrency: 5 } - ); + } - return { - title: 'Iwara Subscription', - link: rootUrl, - item: items, - }; - } finally { - await destroy(); - } + const apiUrl = item.link.replace('www.iwara.tv', 'api.iwara.tv'); + const response = await fetchJson(apiUrl, { + headers: authedHeaders, + }); + + // The new API returns `file` (single object) for videos and `files` (array) for images + const files = response.files || (response.file ? [response.file] : []); + description = renderSubscriptionImages(files.filter((f) => f.type === 'image').map((f) => `${imageRootUrl}/image/original/${f.id}/${f.name}`)); + + const body = response.body ? md.render(response.body) : ''; + description += body; + + return { + title: item.title, + author: item.author, + link: item.link, + category: item.category, + pubDate: item.pubDate, + description, + }; + }), + { concurrency: 5 } + ); + + return { + title: 'Iwara Subscription', + link: rootUrl, + item: items, + }; } diff --git a/lib/routes/iwara/utils.ts b/lib/routes/iwara/utils.ts index 90f05e4a3371..0fa2defe3407 100644 --- a/lib/routes/iwara/utils.ts +++ b/lib/routes/iwara/utils.ts @@ -1,6 +1,5 @@ export const rootUrl = 'https://www.iwara.tv'; export const apiRootUrl = 'https://api.iwara.tv'; -export const apiqRootUrl = 'https://apiq.iwara.tv'; export const imageRootUrl = 'https://i.iwara.tv'; export const typeMap = { From 2d7bc0f70f35a036af914627152e5a61763d534d Mon Sep 17 00:00:00 2001 From: FFFold Date: Fri, 17 Jul 2026 21:28:02 +0800 Subject: [PATCH 2/9] fix(route/iwara): use Puppeteer to bypass Cloudflare protection The api.iwara.tv endpoints are protected by Cloudflare challenges, requiring a real browser to bypass. Restore Puppeteer-based approach with updated API URLs and response format. - ranking: use page.evaluate() fetch via browser context - subscriptions: update apiq.iwara.tv -> api.iwara.tv - index: use Puppeteer for videos/images listing --- lib/routes/iwara/index.ts | 33 ++-- lib/routes/iwara/ranking.ts | 41 +++-- lib/routes/iwara/subscriptions.ts | 247 ++++++++++++++++-------------- 3 files changed, 183 insertions(+), 138 deletions(-) diff --git a/lib/routes/iwara/index.ts b/lib/routes/iwara/index.ts index 5bf7702c2288..957256e7dace 100644 --- a/lib/routes/iwara/index.ts +++ b/lib/routes/iwara/index.ts @@ -3,14 +3,10 @@ import type { Route } from '@/types'; import cache from '@/utils/cache'; import ofetch from '@/utils/ofetch'; import { parseDate } from '@/utils/parse-date'; +import { getPlaywrightPage } from '@/utils/playwright'; import { apiRootUrl, parseThumbnail, rootUrl, typeMap } from './utils'; -const apiUrlMap = { - video: `${apiRootUrl}/videos`, - image: `${apiRootUrl}/images`, -}; - export const route: Route = { path: '/users/:username/:type?', example: '/iwara/users/kelpie/video', @@ -22,6 +18,7 @@ export const route: Route = { maintainers: ['Fatpandac'], handler, features: { + requirePuppeteer: true, nsfw: true, }, }; @@ -39,15 +36,31 @@ async function handler(ctx) { }); const id = profile.id; + + const apiUrl = `${apiRootUrl}/${type === 'video' ? 'videos' : 'images'}?user=${id}`; + const list = await cache.tryGet( - `${apiUrlMap[type]}?user=${id}`, + apiUrl, async () => { - const response = await ofetch(`${apiUrlMap[type]}?user=${id}`, { - headers: { - 'user-agent': config.trueUA, + const { page, destroy } = await getPlaywrightPage(rootUrl, { + gotoConfig: { + waitUntil: 'domcontentloaded', }, }); - return response.results; + + try { + const response = await page.evaluate(async (url) => { + const res = await fetch(url); + if (!res.ok) { + throw new Error(`HTTP error! status: ${res.status}`); + } + return res.json(); + }, apiUrl); + + return response.results; + } finally { + await destroy(); + } }, config.cache.routeExpire, false diff --git a/lib/routes/iwara/ranking.ts b/lib/routes/iwara/ranking.ts index 804a872f08c3..739e01678603 100644 --- a/lib/routes/iwara/ranking.ts +++ b/lib/routes/iwara/ranking.ts @@ -1,10 +1,10 @@ import { config } from '@/config'; import type { Route } from '@/types'; import cache from '@/utils/cache'; -import ofetch from '@/utils/ofetch'; import { parseDate } from '@/utils/parse-date'; +import { getPlaywrightPage } from '@/utils/playwright'; -import { apiRootUrl, parseThumbnail, rootUrl, typeMap } from './utils'; +import { parseThumbnail, rootUrl, typeMap } from './utils'; const sortMap = { date: 'Latest', @@ -32,6 +32,7 @@ export const route: Route = { maintainers: ['CaoMeiYouRen233'], handler, features: { + requirePuppeteer: true, nsfw: true, }, radar: [ @@ -52,25 +53,37 @@ async function handler(ctx) { const { type = 'video', sort = 'date', rating = 'ecchi' } = ctx.req.param(); const limit = ctx.req.query('limit') || 32; - const url = `${apiRootUrl}/${type === 'video' ? 'videos' : 'images'}?sort=${sort}&rating=${rating}&limit=${limit}`; + const apiUrl = `https://api.iwara.tv/${type === 'video' ? 'videos' : 'images'}?sort=${sort}&rating=${rating}&limit=${limit}`; const items = await cache.tryGet( `iwara:ranking:${type}:${sort}:${rating}`, async () => { - const response = await ofetch(url, { - headers: { - 'user-agent': config.trueUA, + const { page, destroy } = await getPlaywrightPage(rootUrl, { + gotoConfig: { + waitUntil: 'domcontentloaded', }, }); - return response.results.map((item) => ({ - title: item.title, - author: item.user.name, - link: `${rootUrl}/${type === 'video' ? 'video' : 'image'}/${item.id}${item.slug ? `/${item.slug}` : ''}`, - category: item.tags?.map((i) => i.id) || [], - description: parseThumbnail(type, item), - pubDate: parseDate(item.createdAt), - })); + try { + const response = await page.evaluate(async (url) => { + const res = await fetch(url); + if (!res.ok) { + throw new Error(`HTTP error! status: ${res.status}`); + } + return res.json(); + }, apiUrl); + + return response.results.map((item) => ({ + title: item.title, + author: item.user.name, + link: `${rootUrl}/${type === 'video' ? 'video' : 'image'}/${item.id}${item.slug ? `/${item.slug}` : ''}`, + category: item.tags?.map((i) => i.id) || [], + description: parseThumbnail(type, item), + pubDate: parseDate(item.createdAt), + })); + } finally { + await destroy(); + } }, config.cache.routeExpire, false diff --git a/lib/routes/iwara/subscriptions.ts b/lib/routes/iwara/subscriptions.ts index fd0aa7ee8d78..4b660f85d03f 100644 --- a/lib/routes/iwara/subscriptions.ts +++ b/lib/routes/iwara/subscriptions.ts @@ -5,11 +5,11 @@ import { config } from '@/config'; import ConfigNotFoundError from '@/errors/types/config-not-found'; import type { Route } from '@/types'; import cache from '@/utils/cache'; -import ofetch from '@/utils/ofetch'; import { parseDate } from '@/utils/parse-date'; +import { getPlaywrightPage } from '@/utils/playwright'; import { renderSubscriptionImages } from './templates/subscriptions'; -import { apiRootUrl, imageRootUrl, rootUrl } from './utils'; +import { imageRootUrl, rootUrl } from './utils'; const md = MarkdownIt({ html: true, @@ -40,6 +40,7 @@ export const route: Route = { description: '', }, ], + requirePuppeteer: true, antiCrawler: false, supportBT: false, supportPodcast: false, @@ -68,96 +69,133 @@ async function handler() { const username = config.iwara.username; const password = config.iwara.password; - const fetchJson = (url: string, options: any = {}) => - ofetch(url, { - headers: { - ...apiHeaders, - ...options.headers, - }, - ...(options.body ? { body: JSON.stringify(options.body) } : {}), - ...(options.method ? { method: options.method } : {}), - }); + const apiRoot = 'https://api.iwara.tv'; - // login and get refresh token - const refreshHeaders = await cache.tryGet( - 'iwara:token', - async () => { - const result = await fetchJson(`${apiRootUrl}/user/login`, { - method: 'POST', - headers: apiHeaders, - body: { email: username, password }, - }); - return { authorization: 'Bearer ' + result.token }; - }, - 30 * 24 * 60 * 60, - false - ); - - // get access token - const authHeaders = await cache.tryGet( - 'iwara:authToken', - async () => { - const result = await fetchJson(`${apiRootUrl}/user/token`, { - method: 'POST', - headers: { - ...apiHeaders, - Authorization: refreshHeaders.authorization, - }, - }); - return { authorization: 'Bearer ' + result.accessToken }; + const { page, destroy } = await getPlaywrightPage(rootUrl, { + gotoConfig: { + waitUntil: 'domcontentloaded', }, - 60 * 60, - false - ); + }); - const authedHeaders = { - ...apiHeaders, - Authorization: authHeaders.authorization, - }; + try { + const fetchApi = (url: string, options: any) => + page.evaluate( + async (args) => { + const res = await fetch(args.url, { + method: args.options.method || 'GET', + headers: args.options.headers, + body: args.options.body ? JSON.stringify(args.options.body) : undefined, + }); + if (!res.ok) { + throw new Error(`HTTP error! status: ${res.status}`); + } + return res.json(); + }, + { url, options } + ); + + // login and get refresh token + const refreshHeaders = await cache.tryGet( + 'iwara:token', + async () => { + const result = await fetchApi(`${apiRoot}/user/login`, { + method: 'POST', + headers: apiHeaders, + body: { email: username, password }, + }); + return { authorization: 'Bearer ' + result.token }; + }, + 30 * 24 * 60 * 60, + false + ); + + // get access token + const authHeaders = await cache.tryGet( + 'iwara:authToken', + async () => { + const result = await fetchApi(`${apiRoot}/user/token`, { + method: 'POST', + headers: { + ...apiHeaders, + Authorization: refreshHeaders.authorization, + }, + }); + return { authorization: 'Bearer ' + result.accessToken }; + }, + 60 * 60, + false + ); - // fetch subscriptions - const [videoResponse, imageResponse] = await Promise.all([ - fetchJson(`${apiRootUrl}/videos?rating=all&page=0&limit=24&subscribed=true`, { headers: authedHeaders }), - fetchJson(`${apiRootUrl}/images?rating=all&page=0&limit=24&subscribed=true`, { headers: authedHeaders }), - ]); + const authedHeaders = { + ...apiHeaders, + Authorization: authHeaders.authorization, + }; - const videoList = videoResponse.results.map((item) => { - const imageUrl = item.file ? `${imageRootUrl}/image/original/${item.file.id}/thumbnail-${item.thumbnail.toString().padStart(2, '0')}.jpg` : ''; + // fetch subscriptions + const [videoResponse, imageResponse] = await Promise.all([ + fetchApi(`${apiRoot}/videos?rating=all&page=0&limit=24&subscribed=true`, { headers: authedHeaders }), + fetchApi(`${apiRoot}/images?rating=all&page=0&limit=24&subscribed=true`, { headers: authedHeaders }), + ]); + + const videoList = videoResponse.results.map((item) => { + const imageUrl = item.file ? `${imageRootUrl}/image/original/${item.file.id}/thumbnail-${item.thumbnail.toString().padStart(2, '0')}.jpg` : ''; + + return { + title: item.title, + author: item.user.name, + link: `${rootUrl}/video/${item.id}`, + category: ['Video', ...(item.tags ? item.tags.map((i) => i.id) : [])], + imageUrl, + pubDate: parseDate(item.createdAt), + private: item.private, + }; + }); - return { - title: item.title, - author: item.user.name, - link: `${rootUrl}/video/${item.id}`, - category: ['Video', ...(item.tags ? item.tags.map((i) => i.id) : [])], - imageUrl, - pubDate: parseDate(item.createdAt), - private: item.private, - }; - }); + const imageList = imageResponse.results.map((item) => { + const imageUrl = item.thumbnail ? `${imageRootUrl}/image/original/${item.thumbnail.id}/${item.thumbnail.name}` : ''; + return { + title: item.title, + author: item.user.name, + link: `${rootUrl}/image/${item.id}`, + category: ['Image', ...(item.tags ? item.tags.map((i) => i.id) : [])], + imageUrl, + pubDate: parseDate(item.createdAt), + }; + }); - const imageList = imageResponse.results.map((item) => { - const imageUrl = item.thumbnail ? `${imageRootUrl}/image/original/${item.thumbnail.id}/${item.thumbnail.name}` : ''; - return { - title: item.title, - author: item.user.name, - link: `${rootUrl}/image/${item.id}`, - category: ['Image', ...(item.tags ? item.tags.map((i) => i.id) : [])], - imageUrl, - pubDate: parseDate(item.createdAt), - }; - }); + // fulltext + const list = [...videoList, ...imageList]; + // Execute fetches with limited concurrency + const items = await pMap( + list, + (item) => + cache.tryGet(item.link, async () => { + let description = renderSubscriptionImages([item.imageUrl]); + + if (item.private === true) { + description += 'private'; + return { + title: item.title, + author: item.author, + link: item.link, + category: item.category, + pubDate: item.pubDate, + description, + }; + } + + const apiUrl = item.link.replace('www.iwara.tv', 'api.iwara.tv'); + const response = await fetchApi(apiUrl, { + headers: authedHeaders, + }); + + // The new API returns `file` (single object) for videos and `files` (array) for images + const files = response.files || (response.file ? [response.file] : []); + description = renderSubscriptionImages(files.filter((f) => f.type === 'image').map((f) => `${imageRootUrl}/image/original/${f.id}/${f.name}`)); + + const body = response.body ? md.render(response.body) : ''; + description += body; - // fulltext - const list = [...videoList, ...imageList]; - // Execute fetches with limited concurrency - const items = await pMap( - list, - (item) => - cache.tryGet(item.link, async () => { - let description = renderSubscriptionImages([item.imageUrl]); - - if (item.private === true) { - description += 'private'; return { title: item.title, author: item.author, @@ -166,35 +204,16 @@ async function handler() { pubDate: item.pubDate, description, }; - } - - const apiUrl = item.link.replace('www.iwara.tv', 'api.iwara.tv'); - const response = await fetchJson(apiUrl, { - headers: authedHeaders, - }); + }), + { concurrency: 5 } + ); - // The new API returns `file` (single object) for videos and `files` (array) for images - const files = response.files || (response.file ? [response.file] : []); - description = renderSubscriptionImages(files.filter((f) => f.type === 'image').map((f) => `${imageRootUrl}/image/original/${f.id}/${f.name}`)); - - const body = response.body ? md.render(response.body) : ''; - description += body; - - return { - title: item.title, - author: item.author, - link: item.link, - category: item.category, - pubDate: item.pubDate, - description, - }; - }), - { concurrency: 5 } - ); - - return { - title: 'Iwara Subscription', - link: rootUrl, - item: items, - }; + return { + title: 'Iwara Subscription', + link: rootUrl, + item: items, + }; + } finally { + await destroy(); + } } From c96feedd1261809b7c5be9772dd6a60a60d79102 Mon Sep 17 00:00:00 2001 From: FFFold Date: Fri, 17 Jul 2026 21:36:04 +0800 Subject: [PATCH 3/9] refactor(route/iwara): use apiRootUrl constant and fix review issues --- lib/routes/iwara/ranking.ts | 4 ++-- lib/routes/iwara/subscriptions.ts | 12 +++++------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/lib/routes/iwara/ranking.ts b/lib/routes/iwara/ranking.ts index 739e01678603..780931dbed8b 100644 --- a/lib/routes/iwara/ranking.ts +++ b/lib/routes/iwara/ranking.ts @@ -4,7 +4,7 @@ import cache from '@/utils/cache'; import { parseDate } from '@/utils/parse-date'; import { getPlaywrightPage } from '@/utils/playwright'; -import { parseThumbnail, rootUrl, typeMap } from './utils'; +import { apiRootUrl, parseThumbnail, rootUrl, typeMap } from './utils'; const sortMap = { date: 'Latest', @@ -53,7 +53,7 @@ async function handler(ctx) { const { type = 'video', sort = 'date', rating = 'ecchi' } = ctx.req.param(); const limit = ctx.req.query('limit') || 32; - const apiUrl = `https://api.iwara.tv/${type === 'video' ? 'videos' : 'images'}?sort=${sort}&rating=${rating}&limit=${limit}`; + const apiUrl = `${apiRootUrl}/${type === 'video' ? 'videos' : 'images'}?sort=${sort}&rating=${rating}&limit=${limit}`; const items = await cache.tryGet( `iwara:ranking:${type}:${sort}:${rating}`, diff --git a/lib/routes/iwara/subscriptions.ts b/lib/routes/iwara/subscriptions.ts index 4b660f85d03f..e49a74752039 100644 --- a/lib/routes/iwara/subscriptions.ts +++ b/lib/routes/iwara/subscriptions.ts @@ -9,7 +9,7 @@ import { parseDate } from '@/utils/parse-date'; import { getPlaywrightPage } from '@/utils/playwright'; import { renderSubscriptionImages } from './templates/subscriptions'; -import { imageRootUrl, rootUrl } from './utils'; +import { apiRootUrl, imageRootUrl, rootUrl } from './utils'; const md = MarkdownIt({ html: true, @@ -69,8 +69,6 @@ async function handler() { const username = config.iwara.username; const password = config.iwara.password; - const apiRoot = 'https://api.iwara.tv'; - const { page, destroy } = await getPlaywrightPage(rootUrl, { gotoConfig: { waitUntil: 'domcontentloaded', @@ -98,7 +96,7 @@ async function handler() { const refreshHeaders = await cache.tryGet( 'iwara:token', async () => { - const result = await fetchApi(`${apiRoot}/user/login`, { + const result = await fetchApi(`${apiRootUrl}/user/login`, { method: 'POST', headers: apiHeaders, body: { email: username, password }, @@ -113,7 +111,7 @@ async function handler() { const authHeaders = await cache.tryGet( 'iwara:authToken', async () => { - const result = await fetchApi(`${apiRoot}/user/token`, { + const result = await fetchApi(`${apiRootUrl}/user/token`, { method: 'POST', headers: { ...apiHeaders, @@ -133,8 +131,8 @@ async function handler() { // fetch subscriptions const [videoResponse, imageResponse] = await Promise.all([ - fetchApi(`${apiRoot}/videos?rating=all&page=0&limit=24&subscribed=true`, { headers: authedHeaders }), - fetchApi(`${apiRoot}/images?rating=all&page=0&limit=24&subscribed=true`, { headers: authedHeaders }), + fetchApi(`${apiRootUrl}/videos?rating=all&page=0&limit=24&subscribed=true`, { headers: authedHeaders }), + fetchApi(`${apiRootUrl}/images?rating=all&page=0&limit=24&subscribed=true`, { headers: authedHeaders }), ]); const videoList = videoResponse.results.map((item) => { From 0c1db180e4345875fb0c8c5c027199ed902114df Mon Sep 17 00:00:00 2001 From: FFFold Date: Fri, 17 Jul 2026 22:07:07 +0800 Subject: [PATCH 4/9] fix(route/iwara): add request-type filtering to Puppeteer calls --- lib/routes/iwara/index.ts | 6 ++++++ lib/routes/iwara/ranking.ts | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/lib/routes/iwara/index.ts b/lib/routes/iwara/index.ts index 957256e7dace..eb20c289192f 100644 --- a/lib/routes/iwara/index.ts +++ b/lib/routes/iwara/index.ts @@ -43,6 +43,12 @@ async function handler(ctx) { apiUrl, async () => { const { page, destroy } = await getPlaywrightPage(rootUrl, { + onBeforeLoad: async (page) => { + await page.route('**/*', (route) => { + const type = route.request().resourceType(); + ['document', 'script', 'xhr', 'fetch'].includes(type) ? route.continue() : route.abort(); + }); + }, gotoConfig: { waitUntil: 'domcontentloaded', }, diff --git a/lib/routes/iwara/ranking.ts b/lib/routes/iwara/ranking.ts index 780931dbed8b..f0a36f4dfe9e 100644 --- a/lib/routes/iwara/ranking.ts +++ b/lib/routes/iwara/ranking.ts @@ -59,6 +59,12 @@ async function handler(ctx) { `iwara:ranking:${type}:${sort}:${rating}`, async () => { const { page, destroy } = await getPlaywrightPage(rootUrl, { + onBeforeLoad: async (page) => { + await page.route('**/*', (route) => { + const type = route.request().resourceType(); + ['document', 'script', 'xhr', 'fetch'].includes(type) ? route.continue() : route.abort(); + }); + }, gotoConfig: { waitUntil: 'domcontentloaded', }, From 563be5903dee48dca54654c60fe9465b36816cde Mon Sep 17 00:00:00 2001 From: FFFold Date: Fri, 17 Jul 2026 22:53:04 +0800 Subject: [PATCH 5/9] fix(route/iwara): skip page navigation for Puppeteer calls ranking and index routes only need a browser context to make fetch() calls via page.evaluate(). Adding noGoto:true skips the page.goto() navigation, avoiding the 30s scheduleClose timeout issue where the browser gets closed before navigation completes. --- lib/routes/iwara/index.ts | 4 +--- lib/routes/iwara/ranking.ts | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/lib/routes/iwara/index.ts b/lib/routes/iwara/index.ts index eb20c289192f..fccb070ba57c 100644 --- a/lib/routes/iwara/index.ts +++ b/lib/routes/iwara/index.ts @@ -43,15 +43,13 @@ async function handler(ctx) { apiUrl, async () => { const { page, destroy } = await getPlaywrightPage(rootUrl, { + noGoto: true, onBeforeLoad: async (page) => { await page.route('**/*', (route) => { const type = route.request().resourceType(); ['document', 'script', 'xhr', 'fetch'].includes(type) ? route.continue() : route.abort(); }); }, - gotoConfig: { - waitUntil: 'domcontentloaded', - }, }); try { diff --git a/lib/routes/iwara/ranking.ts b/lib/routes/iwara/ranking.ts index f0a36f4dfe9e..c36a2e3dbf25 100644 --- a/lib/routes/iwara/ranking.ts +++ b/lib/routes/iwara/ranking.ts @@ -59,15 +59,13 @@ async function handler(ctx) { `iwara:ranking:${type}:${sort}:${rating}`, async () => { const { page, destroy } = await getPlaywrightPage(rootUrl, { + noGoto: true, onBeforeLoad: async (page) => { await page.route('**/*', (route) => { const type = route.request().resourceType(); ['document', 'script', 'xhr', 'fetch'].includes(type) ? route.continue() : route.abort(); }); }, - gotoConfig: { - waitUntil: 'domcontentloaded', - }, }); try { From 343680d4e8e99b23f2b0e4ceaf39a4b006783d7d Mon Sep 17 00:00:00 2001 From: FFFold Date: Fri, 17 Jul 2026 23:00:28 +0800 Subject: [PATCH 6/9] fix(route/iwara): restore page navigation with longer close timeout Revert noGoto change - page navigation is needed to establish a valid origin for fetch() calls (CORS). Increase closeTimeout to 90s to accommodate slow environments where navigation takes longer. --- lib/routes/iwara/index.ts | 5 ++++- lib/routes/iwara/ranking.ts | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/routes/iwara/index.ts b/lib/routes/iwara/index.ts index fccb070ba57c..a67701e7a67b 100644 --- a/lib/routes/iwara/index.ts +++ b/lib/routes/iwara/index.ts @@ -43,13 +43,16 @@ async function handler(ctx) { apiUrl, async () => { const { page, destroy } = await getPlaywrightPage(rootUrl, { - noGoto: true, + closeTimeout: 90 * 1000, onBeforeLoad: async (page) => { await page.route('**/*', (route) => { const type = route.request().resourceType(); ['document', 'script', 'xhr', 'fetch'].includes(type) ? route.continue() : route.abort(); }); }, + gotoConfig: { + waitUntil: 'domcontentloaded', + }, }); try { diff --git a/lib/routes/iwara/ranking.ts b/lib/routes/iwara/ranking.ts index c36a2e3dbf25..9cb17fd2e40e 100644 --- a/lib/routes/iwara/ranking.ts +++ b/lib/routes/iwara/ranking.ts @@ -59,13 +59,16 @@ async function handler(ctx) { `iwara:ranking:${type}:${sort}:${rating}`, async () => { const { page, destroy } = await getPlaywrightPage(rootUrl, { - noGoto: true, + closeTimeout: 90 * 1000, onBeforeLoad: async (page) => { await page.route('**/*', (route) => { const type = route.request().resourceType(); ['document', 'script', 'xhr', 'fetch'].includes(type) ? route.continue() : route.abort(); }); }, + gotoConfig: { + waitUntil: 'domcontentloaded', + }, }); try { From b9cb5fb2d8a4b2d8bbbc08224c0aefcbfe3f7627 Mon Sep 17 00:00:00 2001 From: FFFold Date: Sat, 18 Jul 2026 07:43:05 +0800 Subject: [PATCH 7/9] fix(route/iwara): keep video thumbnail when detail API returns no image files Video detail API returns file.type as 'video', not 'image', so the .filter(f => f.type === 'image') produces an empty array and drops the thumbnail. Fall back to the list thumbnail when no image files are found. --- lib/routes/iwara/subscriptions.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/routes/iwara/subscriptions.ts b/lib/routes/iwara/subscriptions.ts index e49a74752039..90c0a88be3f5 100644 --- a/lib/routes/iwara/subscriptions.ts +++ b/lib/routes/iwara/subscriptions.ts @@ -189,7 +189,8 @@ async function handler() { // The new API returns `file` (single object) for videos and `files` (array) for images const files = response.files || (response.file ? [response.file] : []); - description = renderSubscriptionImages(files.filter((f) => f.type === 'image').map((f) => `${imageRootUrl}/image/original/${f.id}/${f.name}`)); + const imageFiles = files.filter((f) => f.type === 'image'); + description = imageFiles.length > 0 ? renderSubscriptionImages(imageFiles.map((f) => `${imageRootUrl}/image/original/${f.id}/${f.name}`)) : renderSubscriptionImages([item.imageUrl]); const body = response.body ? md.render(response.body) : ''; description += body; From 2507a0ea2be3bd8ef849e65ca4156a51c7a36402 Mon Sep 17 00:00:00 2001 From: FFFold Date: Mon, 24 Aug 2026 18:46:43 +0800 Subject: [PATCH 8/9] fix(route/iwara): resolve review comments and refine cache - Fetch user profile through the same Playwright session as the list request - Rename route resourceType variable to avoid shadowing the route type param - Simplify subscription image description rendering - Include limit in ranking cache key to prevent mixing cached results --- lib/routes/iwara/index.ts | 120 ++++++++++++++++++------------ lib/routes/iwara/ranking.ts | 6 +- lib/routes/iwara/subscriptions.ts | 2 +- 3 files changed, 75 insertions(+), 53 deletions(-) diff --git a/lib/routes/iwara/index.ts b/lib/routes/iwara/index.ts index a67701e7a67b..45a4337ac30c 100644 --- a/lib/routes/iwara/index.ts +++ b/lib/routes/iwara/index.ts @@ -1,12 +1,28 @@ import { config } from '@/config'; import type { Route } from '@/types'; import cache from '@/utils/cache'; -import ofetch from '@/utils/ofetch'; import { parseDate } from '@/utils/parse-date'; -import { getPlaywrightPage } from '@/utils/playwright'; +import { getPlaywrightPage, type Page } from '@/utils/playwright'; import { apiRootUrl, parseThumbnail, rootUrl, typeMap } from './utils'; +const openPage = async () => { + const { page, destroy } = await getPlaywrightPage(rootUrl, { + closeTimeout: 90 * 1000, + onBeforeLoad: async (page) => { + await page.route('**/*', (route) => { + const resourceType = route.request().resourceType(); + ['document', 'script', 'xhr', 'fetch'].includes(resourceType) ? route.continue() : route.abort(); + }); + }, + gotoConfig: { + waitUntil: 'domcontentloaded', + }, + }); + + return { page, destroy }; +}; + export const route: Route = { path: '/users/:username/:type?', example: '/iwara/users/kelpie/video', @@ -26,37 +42,41 @@ export const route: Route = { async function handler(ctx) { const { username, type = 'video' } = ctx.req.param(); - const profile = await cache.tryGet(`${apiRootUrl}/profile/${username}`, async () => { - const response = await ofetch(`${apiRootUrl}/profile/${username}`, { - headers: { - 'user-agent': config.trueUA, - }, - }); - return response.user; - }); + let page: Page | undefined; + let destroy: (() => Promise) | undefined; - const id = profile.id; + const openPageIfNeeded = async (): Promise => { + if (!page || !destroy) { + const opened = await openPage(); + page = opened.page; + destroy = opened.destroy; + } + return page; + }; - const apiUrl = `${apiRootUrl}/${type === 'video' ? 'videos' : 'images'}?user=${id}`; + try { + const profile = await cache.tryGet(`${apiRootUrl}/profile/${username}`, async () => { + const currentPage = await openPageIfNeeded(); + const response = await currentPage.evaluate(async (url) => { + const res = await fetch(url); + if (!res.ok) { + throw new Error(`HTTP error! status: ${res.status}`); + } + return res.json(); + }, `${apiRootUrl}/profile/${username}`); - const list = await cache.tryGet( - apiUrl, - async () => { - const { page, destroy } = await getPlaywrightPage(rootUrl, { - closeTimeout: 90 * 1000, - onBeforeLoad: async (page) => { - await page.route('**/*', (route) => { - const type = route.request().resourceType(); - ['document', 'script', 'xhr', 'fetch'].includes(type) ? route.continue() : route.abort(); - }); - }, - gotoConfig: { - waitUntil: 'domcontentloaded', - }, - }); + return response.user; + }); + + const id = profile.id; - try { - const response = await page.evaluate(async (url) => { + const apiUrl = `${apiRootUrl}/${type === 'video' ? 'videos' : 'images'}?user=${id}`; + + const list = await cache.tryGet( + apiUrl, + async () => { + const currentPage = await openPageIfNeeded(); + const response = await currentPage.evaluate(async (url) => { const res = await fetch(url); if (!res.ok) { throw new Error(`HTTP error! status: ${res.status}`); @@ -65,26 +85,28 @@ async function handler(ctx) { }, apiUrl); return response.results; - } finally { - await destroy(); - } - }, - config.cache.routeExpire, - false - ); + }, + config.cache.routeExpire, + false + ); - const items = list.map((item) => ({ - title: item.title, - author: username, - link: `${rootUrl}/${type}/${item.id}${item.slug ? `/${item.slug}` : ''}`, - category: item.tags?.map((i) => i.id) || [], - description: parseThumbnail(type, item), - pubDate: parseDate(item.createdAt), - })); + const items = list.map((item) => ({ + title: item.title, + author: username, + link: `${rootUrl}/${type}/${item.id}${item.slug ? `/${item.slug}` : ''}`, + category: item.tags?.map((i) => i.id) || [], + description: parseThumbnail(type, item), + pubDate: parseDate(item.createdAt), + })); - return { - title: `${username}'s iwara - ${typeMap[type]}`, - link: `${rootUrl}/users/${username}`, - item: items, - }; + return { + title: `${username}'s iwara - ${typeMap[type]}`, + link: `${rootUrl}/users/${username}`, + item: items, + }; + } finally { + if (destroy) { + await destroy(); + } + } } diff --git a/lib/routes/iwara/ranking.ts b/lib/routes/iwara/ranking.ts index 9cb17fd2e40e..8164f6f24ac4 100644 --- a/lib/routes/iwara/ranking.ts +++ b/lib/routes/iwara/ranking.ts @@ -56,14 +56,14 @@ async function handler(ctx) { const apiUrl = `${apiRootUrl}/${type === 'video' ? 'videos' : 'images'}?sort=${sort}&rating=${rating}&limit=${limit}`; const items = await cache.tryGet( - `iwara:ranking:${type}:${sort}:${rating}`, + `iwara:ranking:${type}:${sort}:${rating}:${limit}`, async () => { const { page, destroy } = await getPlaywrightPage(rootUrl, { closeTimeout: 90 * 1000, onBeforeLoad: async (page) => { await page.route('**/*', (route) => { - const type = route.request().resourceType(); - ['document', 'script', 'xhr', 'fetch'].includes(type) ? route.continue() : route.abort(); + const resourceType = route.request().resourceType(); + ['document', 'script', 'xhr', 'fetch'].includes(resourceType) ? route.continue() : route.abort(); }); }, gotoConfig: { diff --git a/lib/routes/iwara/subscriptions.ts b/lib/routes/iwara/subscriptions.ts index 90c0a88be3f5..6a7a6b9e809e 100644 --- a/lib/routes/iwara/subscriptions.ts +++ b/lib/routes/iwara/subscriptions.ts @@ -190,7 +190,7 @@ async function handler() { // The new API returns `file` (single object) for videos and `files` (array) for images const files = response.files || (response.file ? [response.file] : []); const imageFiles = files.filter((f) => f.type === 'image'); - description = imageFiles.length > 0 ? renderSubscriptionImages(imageFiles.map((f) => `${imageRootUrl}/image/original/${f.id}/${f.name}`)) : renderSubscriptionImages([item.imageUrl]); + description = renderSubscriptionImages(imageFiles.length > 0 ? imageFiles.map((f) => `${imageRootUrl}/image/original/${f.id}/${f.name}`) : [item.imageUrl]); const body = response.body ? md.render(response.body) : ''; description += body; From f8debf7eb9ca516f5ec61814d720f7bffa0144b2 Mon Sep 17 00:00:00 2001 From: FFFold Date: Mon, 24 Aug 2026 18:58:16 +0800 Subject: [PATCH 9/9] refactor(route/iwara): extract shared Playwright and JSON helpers - Add fetchJsonInPage helper for in-page JSON API calls - Add getIwaraPage helper for shared Playwright page configuration - Replace duplicated page.evaluate and getPlaywrightPage code across routes --- lib/routes/iwara/index.ts | 39 +++++-------------------------------- lib/routes/iwara/ranking.ts | 24 +++-------------------- lib/routes/iwara/utils.ts | 25 ++++++++++++++++++++++++ 3 files changed, 33 insertions(+), 55 deletions(-) diff --git a/lib/routes/iwara/index.ts b/lib/routes/iwara/index.ts index 45a4337ac30c..4de332035d51 100644 --- a/lib/routes/iwara/index.ts +++ b/lib/routes/iwara/index.ts @@ -2,26 +2,9 @@ import { config } from '@/config'; import type { Route } from '@/types'; import cache from '@/utils/cache'; import { parseDate } from '@/utils/parse-date'; -import { getPlaywrightPage, type Page } from '@/utils/playwright'; +import type { Page } from '@/utils/playwright'; -import { apiRootUrl, parseThumbnail, rootUrl, typeMap } from './utils'; - -const openPage = async () => { - const { page, destroy } = await getPlaywrightPage(rootUrl, { - closeTimeout: 90 * 1000, - onBeforeLoad: async (page) => { - await page.route('**/*', (route) => { - const resourceType = route.request().resourceType(); - ['document', 'script', 'xhr', 'fetch'].includes(resourceType) ? route.continue() : route.abort(); - }); - }, - gotoConfig: { - waitUntil: 'domcontentloaded', - }, - }); - - return { page, destroy }; -}; +import { apiRootUrl, fetchJsonInPage, getIwaraPage, parseThumbnail, rootUrl, typeMap } from './utils'; export const route: Route = { path: '/users/:username/:type?', @@ -47,7 +30,7 @@ async function handler(ctx) { const openPageIfNeeded = async (): Promise => { if (!page || !destroy) { - const opened = await openPage(); + const opened = await getIwaraPage(); page = opened.page; destroy = opened.destroy; } @@ -57,13 +40,7 @@ async function handler(ctx) { try { const profile = await cache.tryGet(`${apiRootUrl}/profile/${username}`, async () => { const currentPage = await openPageIfNeeded(); - const response = await currentPage.evaluate(async (url) => { - const res = await fetch(url); - if (!res.ok) { - throw new Error(`HTTP error! status: ${res.status}`); - } - return res.json(); - }, `${apiRootUrl}/profile/${username}`); + const response = await fetchJsonInPage(currentPage, `${apiRootUrl}/profile/${username}`); return response.user; }); @@ -76,13 +53,7 @@ async function handler(ctx) { apiUrl, async () => { const currentPage = await openPageIfNeeded(); - const response = await currentPage.evaluate(async (url) => { - const res = await fetch(url); - if (!res.ok) { - throw new Error(`HTTP error! status: ${res.status}`); - } - return res.json(); - }, apiUrl); + const response = await fetchJsonInPage(currentPage, apiUrl); return response.results; }, diff --git a/lib/routes/iwara/ranking.ts b/lib/routes/iwara/ranking.ts index 8164f6f24ac4..0a8b42db8cb8 100644 --- a/lib/routes/iwara/ranking.ts +++ b/lib/routes/iwara/ranking.ts @@ -2,9 +2,8 @@ import { config } from '@/config'; import type { Route } from '@/types'; import cache from '@/utils/cache'; import { parseDate } from '@/utils/parse-date'; -import { getPlaywrightPage } from '@/utils/playwright'; -import { apiRootUrl, parseThumbnail, rootUrl, typeMap } from './utils'; +import { apiRootUrl, fetchJsonInPage, getIwaraPage, parseThumbnail, rootUrl, typeMap } from './utils'; const sortMap = { date: 'Latest', @@ -58,27 +57,10 @@ async function handler(ctx) { const items = await cache.tryGet( `iwara:ranking:${type}:${sort}:${rating}:${limit}`, async () => { - const { page, destroy } = await getPlaywrightPage(rootUrl, { - closeTimeout: 90 * 1000, - onBeforeLoad: async (page) => { - await page.route('**/*', (route) => { - const resourceType = route.request().resourceType(); - ['document', 'script', 'xhr', 'fetch'].includes(resourceType) ? route.continue() : route.abort(); - }); - }, - gotoConfig: { - waitUntil: 'domcontentloaded', - }, - }); + const { page, destroy } = await getIwaraPage(); try { - const response = await page.evaluate(async (url) => { - const res = await fetch(url); - if (!res.ok) { - throw new Error(`HTTP error! status: ${res.status}`); - } - return res.json(); - }, apiUrl); + const response = await fetchJsonInPage(page, apiUrl); return response.results.map((item) => ({ title: item.title, diff --git a/lib/routes/iwara/utils.ts b/lib/routes/iwara/utils.ts index 0fa2defe3407..8f0062aff49f 100644 --- a/lib/routes/iwara/utils.ts +++ b/lib/routes/iwara/utils.ts @@ -1,7 +1,32 @@ +import { getPlaywrightPage, type Page } from '@/utils/playwright'; + export const rootUrl = 'https://www.iwara.tv'; export const apiRootUrl = 'https://api.iwara.tv'; export const imageRootUrl = 'https://i.iwara.tv'; +export const getIwaraPage = () => + getPlaywrightPage(rootUrl, { + closeTimeout: 90 * 1000, + onBeforeLoad: async (page) => { + await page.route('**/*', (route) => { + const resourceType = route.request().resourceType(); + ['document', 'script', 'xhr', 'fetch'].includes(resourceType) ? route.continue() : route.abort(); + }); + }, + gotoConfig: { + waitUntil: 'domcontentloaded', + }, + }); + +export const fetchJsonInPage = (page: Page, url: string) => + page.evaluate(async (u) => { + const res = await fetch(u); + if (!res.ok) { + throw new Error(`HTTP error! status: ${res.status}`); + } + return res.json(); + }, url); + export const typeMap = { video: 'Videos', image: 'Images',