From 80da61c844b7a5a11bdccc37b1b0e71a3aabf2e2 Mon Sep 17 00:00:00 2001 From: Tony Date: Sun, 23 Jun 2024 03:54:45 +0800 Subject: [PATCH] feat(route): myfans (#15970) --- lib/routes/fansly/post.ts | 3 +- lib/routes/fansly/tag.ts | 3 +- lib/routes/fansly/utils.ts | 9 +-- lib/routes/myfans/namespace.ts | 6 ++ lib/routes/myfans/post.ts | 64 +++++++++++++++++ lib/routes/myfans/templates/post.art | 8 +++ lib/routes/myfans/types.ts | 104 +++++++++++++++++++++++++++ lib/routes/myfans/utils.ts | 39 ++++++++++ 8 files changed, 228 insertions(+), 8 deletions(-) create mode 100644 lib/routes/myfans/namespace.ts create mode 100644 lib/routes/myfans/post.ts create mode 100644 lib/routes/myfans/templates/post.art create mode 100644 lib/routes/myfans/types.ts create mode 100644 lib/routes/myfans/utils.ts diff --git a/lib/routes/fansly/post.ts b/lib/routes/fansly/post.ts index 26661e122..c4a8f726b 100644 --- a/lib/routes/fansly/post.ts +++ b/lib/routes/fansly/post.ts @@ -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) => ({ diff --git a/lib/routes/fansly/tag.ts b/lib/routes/fansly/tag.ts index b2fcdf946..aadd62a29 100644 --- a/lib/routes/fansly/tag.ts +++ b/lib/routes/fansly/tag.ts @@ -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) => { diff --git a/lib/routes/fansly/utils.ts b/lib/routes/fansly/utils.ts index 91f47ca22..f30ec3b56 100644 --- a/lib/routes/fansly/utils.ts +++ b/lib/routes/fansly/utils.ts @@ -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, diff --git a/lib/routes/myfans/namespace.ts b/lib/routes/myfans/namespace.ts new file mode 100644 index 000000000..16abda56b --- /dev/null +++ b/lib/routes/myfans/namespace.ts @@ -0,0 +1,6 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'myfans', + url: 'myfans.jp', +}; diff --git a/lib/routes/myfans/post.ts b/lib/routes/myfans/post.ts new file mode 100644 index 000000000..fb1b6ea32 --- /dev/null +++ b/lib/routes/myfans/post.ts @@ -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', '
')), + 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, + }; +} diff --git a/lib/routes/myfans/templates/post.art b/lib/routes/myfans/templates/post.art new file mode 100644 index 000000000..a4a138169 --- /dev/null +++ b/lib/routes/myfans/templates/post.art @@ -0,0 +1,8 @@ +{{ if postImages }} + {{ each postImages img }} +
+ {{ /each }} +{{ /if }} +{{ if body }} + {{@ body }} +{{ /if }} diff --git a/lib/routes/myfans/types.ts b/lib/routes/myfans/types.ts new file mode 100644 index 000000000..82863e5a2 --- /dev/null +++ b/lib/routes/myfans/types.ts @@ -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; +} diff --git a/lib/routes/myfans/utils.ts b/lib/routes/myfans/utils.ts new file mode 100644 index 000000000..df49d22a5 --- /dev/null +++ b/lib/routes/myfans/utils.ts @@ -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(`${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; + +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; +};