Skip to content
Open
95 changes: 54 additions & 41 deletions lib/routes/iwara/index.ts
Original file line number Diff line number Diff line change
@@ -1,15 +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 type { Page } from '@/utils/playwright';

import { apiRootUrl, parseThumbnail, rootUrl, typeMap } from './utils';

const apiUrlMap = {
video: `${apiRootUrl}/videos`,
image: `${apiRootUrl}/images`,
};
import { apiRootUrl, fetchJsonInPage, getIwaraPage, parseThumbnail, rootUrl, typeMap } from './utils';

export const route: Route = {
path: '/users/:username/:type?',
Expand All @@ -22,49 +17,67 @@ export const route: Route = {
maintainers: ['Fatpandac'],
handler,
features: {
requirePuppeteer: true,
nsfw: true,
},
};

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,
},
let page: Page | undefined;
let destroy: (() => Promise<void>) | undefined;

const openPageIfNeeded = async (): Promise<Page> => {
if (!page || !destroy) {
const opened = await getIwaraPage();
page = opened.page;
destroy = opened.destroy;
}
return page;
};

try {
const profile = await cache.tryGet(`${apiRootUrl}/profile/${username}`, async () => {
const currentPage = await openPageIfNeeded();
const response = await fetchJsonInPage(currentPage, `${apiRootUrl}/profile/${username}`);

return response.user;
});
return response.user;
});

const id = profile.id;
const list = await cache.tryGet(
`${apiUrlMap[type]}?user=${id}`,
async () => {
const response = await ofetch(`${apiUrlMap[type]}?user=${id}`, {
headers: {
'user-agent': config.trueUA,
},
});
return response.results;
},
config.cache.routeExpire,
false
);
const id = profile.id;

const items = list.map((item) => ({
title: item.title,
author: username,
link: `${rootUrl}/${type}/${item.id}/${item.slug}`,
category: item.tags.map((i) => i.id),
description: parseThumbnail(type, item),
pubDate: parseDate(item.createdAt),
}));
const apiUrl = `${apiRootUrl}/${type === 'video' ? 'videos' : 'images'}?user=${id}`;

return {
title: `${username}'s iwara - ${typeMap[type]}`,
link: `${rootUrl}/users/${username}`,
item: items,
};
const list = await cache.tryGet(
apiUrl,
async () => {
const currentPage = await openPageIfNeeded();
const response = await fetchJsonInPage(currentPage, apiUrl);

return response.results;
},
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),
}));

return {
title: `${username}'s iwara - ${typeMap[type]}`,
link: `${rootUrl}/users/${username}`,
item: items,
};
} finally {
if (destroy) {
await destroy();
}
}
}
22 changes: 5 additions & 17 deletions lib/routes/iwara/ranking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -53,26 +52,15 @@ 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 = `${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(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 { page, destroy } = await getIwaraPage();

try {
const content = await page.evaluate(() => document.querySelector('pre')?.textContent || document.body.textContent);
const response = JSON.parse(content || '{}');
const response = await fetchJsonInPage(page, apiUrl);

return response.results.map((item) => ({
title: item.title,
Expand Down
17 changes: 10 additions & 7 deletions lib/routes/iwara/subscriptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ 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,
Expand Down Expand Up @@ -96,7 +96,7 @@ async function handler() {
const refreshHeaders = await cache.tryGet(
'iwara:token',
async () => {
const result = await fetchApi(`${apiqRootUrl}/user/login`, {
const result = await fetchApi(`${apiRootUrl}/user/login`, {
method: 'POST',
headers: apiHeaders,
body: { email: username, password },
Expand All @@ -111,7 +111,7 @@ async function handler() {
const authHeaders = await cache.tryGet(
'iwara:authToken',
async () => {
const result = await fetchApi(`${apiqRootUrl}/user/token`, {
const result = await fetchApi(`${apiRootUrl}/user/token`, {
method: 'POST',
headers: {
...apiHeaders,
Expand All @@ -131,8 +131,8 @@ async function handler() {

// 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 }),
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) => {
Expand Down Expand Up @@ -182,12 +182,15 @@ async function handler() {
};
}

const apiUrl = item.link.replace('www.iwara.tv', 'apiq.iwara.tv');
const apiUrl = item.link.replace('www.iwara.tv', 'api.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]);
// 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 = 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;
Expand Down
26 changes: 25 additions & 1 deletion lib/routes/iwara/utils.ts
Original file line number Diff line number Diff line change
@@ -1,8 +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 apiqRootUrl = 'https://apiq.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',
Expand Down