feat(route): myfans (#15970)

This commit is contained in:
Tony 2024-06-23 03:54:45 +08:00 committed by GitHub
parent 4789b53ef3
commit 80da61c844
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 228 additions and 8 deletions

View File

@ -1,5 +1,4 @@
import { Route } from '@/types';
import cache from '@/utils/cache';
import { parseDate } from '@/utils/parse-date';
import { getAccountByUsername, getTimelineByAccountId, parseDescription, baseUrl } from './utils';
@ -29,7 +28,7 @@ export const route: Route = {
async function handler(ctx) {
const username = ctx.req.param('username');
const account = await getAccountByUsername(username, cache.tryGet);
const account = await getAccountByUsername(username);
const timeline = await getTimelineByAccountId(account.id);
const items = timeline.posts.map((post) => ({

View File

@ -1,5 +1,4 @@
import { Route } from '@/types';
import cache from '@/utils/cache';
import { parseDate } from '@/utils/parse-date';
import { getTagId, getTagSuggestion, findAccountById, parseDescription, baseUrl, icon } from './utils';
@ -29,7 +28,7 @@ export const route: Route = {
async function handler(ctx) {
const tag = ctx.req.param('tag');
const tagId = await getTagId(tag, cache.tryGet);
const tagId = await getTagId(tag);
const suggestion = await getTagSuggestion(tagId);
const items = suggestion.aggregationData?.posts.map((post) => {

View File

@ -5,6 +5,7 @@ import got from '@/utils/got';
import path from 'node:path';
import { art } from '@/utils/render';
import InvalidParameterError from '@/errors/types/invalid-parameter';
import cache from '@/utils/cache';
const apiBaseUrl = 'https://apiv3.fansly.com';
const baseUrl = 'https://fansly.com';
@ -18,8 +19,8 @@ const findAccountById = (accountId, accounts) => {
};
};
const getAccountByUsername = (username, tryGet) =>
tryGet(`fansly:account:${username.toLowerCase()}`, async () => {
const getAccountByUsername = (username) =>
cache.tryGet(`fansly:account:${username.toLowerCase()}`, async () => {
const { data: accountResponse } = await got(`${apiBaseUrl}/api/v1/account`, {
searchParams: {
usernames: username,
@ -48,8 +49,8 @@ const getTimelineByAccountId = async (accountId) => {
return timeline.response;
};
const getTagId = (tag, tryGet) =>
tryGet(`fansly:tag:${tag.toLowerCase()}`, async () => {
const getTagId = (tag) =>
cache.tryGet(`fansly:tag:${tag.toLowerCase()}`, async () => {
const { data: tagResponse } = await got(`${apiBaseUrl}/api/v1/contentdiscovery/media/tag`, {
searchParams: {
tag,

View File

@ -0,0 +1,6 @@
import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: 'myfans',
url: 'myfans.jp',
};

64
lib/routes/myfans/post.ts Normal file
View File

@ -0,0 +1,64 @@
import { Route } from '@/types';
import { parseDate } from '@/utils/parse-date';
import { showByUsername, getPostByAccountId, baseUrl } from './utils';
import path from 'node:path';
import { art } from '@/utils/render';
import { getCurrentPath } from '@/utils/helpers';
const __dirname = getCurrentPath(import.meta.url);
export const route: Route = {
path: '/user/:username',
categories: ['multimedia'],
example: '/myfans/user/secret_japan',
parameters: { username: 'User handle' },
features: {
requireConfig: false,
requirePuppeteer: false,
antiCrawler: false,
supportBT: false,
supportPodcast: false,
supportScihub: false,
},
radar: [
{
source: ['myfans.jp/:username', 'myfans.jp/:language/:username'],
},
],
name: 'User Posts',
maintainers: ['TonyRL'],
handler,
};
const render = (postImages, body) =>
art(path.join(__dirname, 'templates', 'post.art'), {
postImages,
body,
});
async function handler(ctx) {
const { username } = ctx.req.param();
const account = await showByUsername(username);
const posts = await getPostByAccountId(account.id);
const items = posts.map((p) => ({
title: p.body?.replaceAll('\r\n', ' ').trim().split(' ')[0],
description: render(p.post_images, p.body?.replaceAll('\r\n', '<br>')),
pubDate: parseDate(p.published_at),
link: `${baseUrl}/posts/${p.id}`,
author: p.user.name,
}));
return {
title: `${account.name} (@${account.username})`,
link: `${baseUrl}/${account.username}`,
description: `${account.posts_count} Post ${account.likes_count} Like ${account.followers_count}
Followers ${account.followings_count} Follow ${account.about.replaceAll('\r\n', ' ')}`,
image: account.avatar_url,
icon: account.avatar_url,
logo: account.avatar_url,
language: 'ja-JP',
item: items,
};
}

View File

@ -0,0 +1,8 @@
{{ if postImages }}
{{ each postImages img }}
<img src="{{ img.file_url }}"><br>
{{ /each }}
{{ /if }}
{{ if body }}
{{@ body }}
{{ /if }}

104
lib/routes/myfans/types.ts Normal file
View File

@ -0,0 +1,104 @@
export interface UserProfile {
about: string;
active: boolean;
avatar_url: string;
banner_url: string;
id: string | null;
is_following: boolean;
likes_count: number;
name: string;
username: string;
is_official_creator: boolean;
is_official: boolean;
label: null;
back_number_post_images_count: number;
back_number_post_videos_count: number;
cant_receive_message: boolean;
current_back_number_plan: null;
followers_count: number;
followings_count: number;
has_approved_user_identification: boolean;
is_bought_back_number: boolean;
is_followed: boolean;
is_subscribed: boolean;
limited_posts_count: number;
link_instagram_id: string;
link_instagram_url: null;
link_tiktok_id: string;
link_tiktok_url: null;
link_twitter_id: string;
link_twitter_url: string;
link_youtube_url: string;
post_images_count: number;
post_videos_count: number;
posts_count: number;
sns_link1: string;
sns_link2: string;
}
export interface Post {
id: string;
kind: string;
status: string;
status_label: null;
body: string | null;
bookmarked: boolean;
humanized_publish_start_at: string;
deleted_at_i18n: null;
liked: boolean;
likes_count: number;
user: UserProfile;
post_images: {
file_url: string;
square_thumbnail_url: string;
raw_image_height: number;
raw_image_width: number;
}[];
visible: boolean;
publish_end_at: null;
published_at: string;
publish_start_at: string | null;
pinned_at: string | null;
attachment: null;
plan: null;
current_single_plan: {
id: string;
amount: number;
auto_message_body: string;
} | null;
plans: {
id: string;
product_name: string;
monthly_price: number;
status: null;
is_limited_access: boolean;
disallow_new_subscriber: boolean;
active_discount: {
id: string;
discount_rate: number;
start_at: string | null;
end_at: string | null;
limited_number: null;
status: string;
} | null;
}[];
video_processing: boolean | null;
video_duration: {
hours: string | null;
minutes: string;
seconds: string;
} | null;
free: boolean;
limited: boolean;
available: boolean;
metadata: {
video: {
duration: number;
resolutions: string[];
};
image: {
count: number;
};
};
thumbnail_url: string;
}

View File

@ -0,0 +1,39 @@
import ofetch from '@/utils/ofetch';
import InvalidParameterError from '@/errors/types/invalid-parameter';
import cache from '@/utils/cache';
import { Post, UserProfile } from './types';
const apiBaseUrl = 'https://api.myfans.jp';
export const baseUrl = 'https://myfans.jp';
const headers = {
'google-ga-data': 'event328',
};
export const showByUsername = (username: string) =>
cache.tryGet(`myfans:account:${username}`, async () => {
const accountInfo = await ofetch<UserProfile>(`${apiBaseUrl}/api/v2/users/show_by_username`, {
headers,
query: {
username,
},
});
if (!accountInfo.id) {
throw new InvalidParameterError('This creator does not exist.');
}
return accountInfo;
}) as Promise<UserProfile>;
export const getPostByAccountId = async (accountId) => {
const post = await ofetch(`${apiBaseUrl}/api/v2/users/${accountId}/posts`, {
headers,
query: {
sort_key: 'publish_start_at',
page: 1,
},
});
return post.data as Promise<Post[]>;
};