176 lines
5.5 KiB
TypeScript
176 lines
5.5 KiB
TypeScript
import { load } from 'cheerio';
|
|
|
|
import { config } from '@/config';
|
|
import NotFoundError from '@/errors/types/not-found';
|
|
import { renderUserEmbed } from '@/routes/tiktok/templates/user';
|
|
import type { DataItem, Route } from '@/types';
|
|
import cache from '@/utils/cache';
|
|
import ofetch from '@/utils/ofetch';
|
|
import { getPlaywrightPage } from '@/utils/playwright';
|
|
|
|
export const route: Route = {
|
|
path: '/profile/:id/:type?/:functionalFlag?',
|
|
categories: ['social-media'],
|
|
example: '/picuki/profile/linustech',
|
|
parameters: {
|
|
id: 'Tiktok user id (without @)',
|
|
type: {
|
|
description: 'Type of profile page',
|
|
options: [
|
|
{
|
|
value: 'profile',
|
|
label: 'Profile Page',
|
|
},
|
|
{
|
|
value: 'story',
|
|
label: 'Story Page',
|
|
},
|
|
],
|
|
default: 'profile',
|
|
},
|
|
functionalFlag: {
|
|
description: 'Functional flag for video embedding',
|
|
options: [
|
|
{
|
|
value: '0',
|
|
label: 'Off, only show video poster as an image',
|
|
},
|
|
{
|
|
value: '1',
|
|
label: 'On',
|
|
},
|
|
],
|
|
default: '1',
|
|
},
|
|
},
|
|
features: {
|
|
requireConfig: false,
|
|
requirePuppeteer: true,
|
|
antiCrawler: true,
|
|
supportBT: false,
|
|
supportPodcast: false,
|
|
supportScihub: false,
|
|
},
|
|
radar: [
|
|
{
|
|
source: ['www.picuki.com/profile/:id'],
|
|
target: '/profile/:id',
|
|
},
|
|
{
|
|
source: ['www.picuki.com/story/:id'],
|
|
target: '/profile/:id/story',
|
|
},
|
|
],
|
|
name: 'User Profile - Picuki',
|
|
maintainers: ['hoilc', 'Rongronggg9', 'devinmugen', 'NekoAria'],
|
|
handler,
|
|
};
|
|
|
|
async function handler(ctx) {
|
|
const id = ctx.req.param('id');
|
|
const type = ctx.req.param('type') ?? 'profile';
|
|
const functionalFlag = ctx.req.param('functionalFlag') ?? '1';
|
|
const useIframe = functionalFlag !== '0';
|
|
|
|
const baseUrl = 'https://www.picuki.com';
|
|
const profileUrl = `${baseUrl}/${type === 'story' ? 'story' : 'profile'}/${id}`;
|
|
|
|
const data = (await cache.tryGet(`picuki:${type}:${id}`, async () => {
|
|
let response;
|
|
try {
|
|
response = await ofetch(profileUrl, {
|
|
headers: {
|
|
'User-Agent': config.trueUA,
|
|
},
|
|
});
|
|
} catch (error) {
|
|
if (error.status === 403) {
|
|
const { page, destroy } = await getPlaywrightPage(profileUrl, {
|
|
onBeforeLoad: async (page) => {
|
|
const expectResourceTypes = new Set(['document', 'script', 'xhr', 'fetch']);
|
|
await page.route('**/*', (route) => {
|
|
const request = route.request();
|
|
expectResourceTypes.has(request.resourceType()) ? route.continue() : route.abort();
|
|
});
|
|
},
|
|
});
|
|
await page.waitForSelector('.content');
|
|
response = await page.content();
|
|
await destroy();
|
|
} else {
|
|
throw new NotFoundError(error.message);
|
|
}
|
|
}
|
|
|
|
const $ = load(response);
|
|
|
|
if ($('.posts-empty').length) {
|
|
throw new Error($('.posts-empty').text().trim() || 'No posts found');
|
|
}
|
|
if ($('.error-p').length) {
|
|
throw new Error($('.error-p span').text().trim() || 'Profile not found');
|
|
}
|
|
|
|
const username = $('.profile-info .username').text().trim();
|
|
|
|
const items = $('.posts-video .posts__video-item .posts__video-item-a')
|
|
.toArray()
|
|
.map((item) => {
|
|
const $item = $(item);
|
|
const videoId = $item.attr('href')?.split('/').pop();
|
|
const img = $item.find('img');
|
|
return {
|
|
title: img.attr('alt') || '',
|
|
author: username,
|
|
renderData: {
|
|
poster: img.attr('src'),
|
|
source: `${baseUrl}/player/${videoId}`,
|
|
id: videoId,
|
|
},
|
|
link: `${baseUrl}/media/${videoId}`,
|
|
guid: `https://www.tiktok.com/@${id}/video/${videoId}`,
|
|
};
|
|
});
|
|
|
|
return {
|
|
title: $('head title').text(),
|
|
description: $('.posts-current').text().trim(),
|
|
image: $('.profile-image').attr('src'),
|
|
items,
|
|
};
|
|
})) as {
|
|
title: string;
|
|
description: string;
|
|
image: string;
|
|
items: Array<{
|
|
title: string;
|
|
author: string;
|
|
renderData: {
|
|
poster: string;
|
|
source: string;
|
|
id: string;
|
|
};
|
|
link: string;
|
|
guid: string;
|
|
}>;
|
|
};
|
|
|
|
const items: DataItem[] = data.items.map((item) => ({
|
|
...item,
|
|
description: renderUserEmbed({
|
|
poster: item.renderData.poster,
|
|
source: item.renderData.source,
|
|
useIframe,
|
|
id: item.renderData.id,
|
|
}),
|
|
}));
|
|
|
|
return {
|
|
title: data.title,
|
|
link: profileUrl,
|
|
image: data.image,
|
|
description: data.description,
|
|
item: items,
|
|
};
|
|
}
|