feat(route): myfans (#15970)
This commit is contained in:
parent
4789b53ef3
commit
80da61c844
|
|
@ -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) => ({
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
import type { Namespace } from '@/types';
|
||||
|
||||
export const namespace: Namespace = {
|
||||
name: 'myfans',
|
||||
url: 'myfans.jp',
|
||||
};
|
||||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
{{ if postImages }}
|
||||
{{ each postImages img }}
|
||||
<img src="{{ img.file_url }}"><br>
|
||||
{{ /each }}
|
||||
{{ /if }}
|
||||
{{ if body }}
|
||||
{{@ body }}
|
||||
{{ /if }}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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[]>;
|
||||
};
|
||||
Loading…
Reference in New Issue