rsshub
Version:
Make RSS Great Again!
312 lines (293 loc) • 6.97 kB
JavaScript
import { n as init_esm_shims, t as __dirname } from "./esm-shims-CzJ_djXG.mjs";
import "./config-C37vj7VH.mjs";
import "./dist-BInvbO1W.mjs";
import "./logger-Czu8UMNd.mjs";
import { t as ofetch_default } from "./ofetch-BIyrKU3Y.mjs";
import { t as parseDate } from "./parse-date-BrP7mxXf.mjs";
import { t as cache_default } from "./cache-Bo__VnGm.mjs";
import { t as art } from "./render-BQo6B4tL.mjs";
import path from "node:path";
import { load } from "cheerio";
//#region lib/routes/fantube/utils.ts
init_esm_shims();
const baseUrl = "https://www.fantube.tokyo";
const getCreatorFragment = (username) => cache_default.tryGet(`fantube:creator:${username}`, async () => {
const $ = load(await ofetch_default(`${baseUrl}/r18/creator/${username}`, { headers: { cookie: "fantube-ageVerified=1;" } }));
const selfPushString = JSON.parse($("script:contains(\"creatorFragment\")").text().match(/^self\.__next_f\.push\((.+?)\)$/)?.[1] || "{}");
return JSON.parse(selfPushString[1].slice(2)).find((d) => d?.hasOwnProperty("children")).children.find((child) => Object.values(child).includes("div")).find((c) => c?.hasOwnProperty("children")).children.find((c) => c?.hasOwnProperty("creatorFragment")).creatorFragment;
});
const getCreatorPostReelList = (identifier, limit) => cache_default.tryGet(`fantube:creatorPostReelList:${identifier}:${limit}`, async () => {
return (await ofetch_default("https://api.prd.fantube.tokyo/graphql", {
headers: { Referer: baseUrl },
body: JSON.stringify({
query: `query CreatorPostReelList($identifier: String!, $first: Int, $after: String, $last: Int, $before: String) {
posts(
where: {status: {equals: PUBLISHED}, creator: {is: {identifier: {equals: $identifier}}}}
orderBy: [{pinnedAt: {nulls: last, sort: desc}}, {order: asc}, {createdAt: desc}, {id: desc}]
first: $first
after: $after
last: $last
before: $before
) {
nodes {
...PostSwiper_Post
}
pageInfo {
hasNextPage
endCursor
hasPreviousPage
startCursor
}
}
}
fragment PostSwiper_Post on Post {
id
title
isFavorite
favoritesCount
...PostSwiperSlide_Post
}
fragment PostSwiperSlide_Post on Post {
id
type
title
price
creator {
displayName
}
...PostVideoElement_Post
...PostImageElement_Post
}
fragment PostVideoElement_Post on Post {
id
title
contentData {
... on PostVideoType {
__typename
videoUrl
isSample
noSample
durationSeconds
}
}
isFavorite
sampleVideoId
thumbnailUrl
creator {
displayName
}
...PostInfo_Post
...VideoControlIcons_Post
...PurchaseWrapper_Post
}
fragment PostInfo_Post on Post {
title
description
publishStartAt
price
isBuyEnabled
...Profile_Post
}
fragment Profile_Post on Post {
id
creator {
id
isSelf
identifier
displayName
avatarImageUrl
following
}
}
fragment VideoControlIcons_Post on Post {
id
isMine
pinnedAt
favoritesCount
...PostComment_Post
}
fragment PostComment_Post on Post {
id
isMine
canComment
comments(
where: {OR: [{parentPostComment: {is: {isDeleted: {equals: false}}}}, {parentPostCommentId: {equals: null}}], isDeleted: {equals: false}}
) {
totalCount
}
...PostCommentReplyDrawer_Post
}
fragment PostCommentReplyDrawer_Post on Post {
id
isMine
canComment
}
fragment PurchaseWrapper_Post on Post {
id
title
price
creator {
displayName
}
...PostPurchaseDialog_Post
...PostPurchaseSingleDialog_Post
}
fragment PostPurchaseDialog_Post on Post {
id
isBuyEnabled
price
thumbnailUrl
title
planPosts(
orderBy: [{plan: {deleteRequestAt: {sort: desc, nulls: first}}}, {plan: {isRecommended: desc}}, {plan: {price: asc}}]
) {
nodes {
plan {
id
title
price
...PlanSwiper_Plan
}
}
}
creator {
displayName
}
...PostPurchaseSingleDialog_Post
}
fragment PlanSwiper_Plan on Plan {
id
...PlanSwiperItem_Plan
}
fragment PlanSwiperItem_Plan on Plan {
id
title
price
isArchive
isRecommended
deleteRequestAt
isSubscribing
subscriptionCloseAt
capacity
subscribersCount
planPosts(
where: {post: {is: {status: {equals: PUBLISHED}}}}
first: 7
orderBy: [{createdAt: desc}]
) {
nodes {
post {
id
thumbnailUrl
title
}
}
totalCount
}
...PlanUnavailableNote_Plan
}
fragment PlanUnavailableNote_Plan on Plan {
capacity
subscribersCount
subscriptionCloseAt
deleteRequestAt
}
fragment PostPurchaseSingleDialog_Post on Post {
id
price
thumbnailUrl
title
isBuyEnabled
}
fragment PostImageElement_Post on Post {
id
title
contentData {
__typename
... on PostImageType {
encrypted
imageUrls
count
}
}
isFavorite
creator {
displayName
}
...PostInfo_Post
...ImageControlIcons_Post
...PurchaseWrapper_Post
}
fragment ImageControlIcons_Post on Post {
id
isMine
pinnedAt
favoritesCount
...PostComment_Post
}`,
variables: {
identifier,
first: limit,
after: ""
},
operationName: "CreatorPostReelList"
}),
method: "POST"
})).data.posts.nodes;
});
//#endregion
//#region lib/routes/fantube/creator.ts
const route = {
path: "/r18/creator/:identifier",
categories: ["multimedia"],
example: "/fantube/r18/creator/miyuu",
parameters: { identifier: "User handle" },
features: {
requireConfig: false,
requirePuppeteer: false,
antiCrawler: false,
supportBT: false,
supportPodcast: false,
supportScihub: false
},
radar: [{ source: ["www.fantube.tokyo/r18/creator/:identifier"] }],
name: "User Posts",
maintainers: ["TonyRL"],
handler
};
const render = ({ description, thumbnailUrl, sampleVideoId, imageUrls }) => art(path.join(__dirname, "templates/post-7ca176ac.art"), {
description,
thumbnailUrl,
sampleVideoId,
imageUrls
});
async function handler(ctx) {
const { identifier } = ctx.req.param();
const limit = Number.parseInt(ctx.req.query("limit") || 18, 10);
const creatorInfo = await getCreatorFragment(identifier);
const items = (await getCreatorPostReelList(identifier, limit)).map((p) => ({
title: p.title.replaceAll("\n", " ").trim(),
description: render({
description: p.description,
thumbnailUrl: p.thumbnailUrl,
sampleVideoId: p.sampleVideoId,
imageUrls: p.contentData?.imageUrls || []
}),
link: `${baseUrl}/r18/post/${p.id}?creator=${identifier}`,
author: p.creator.displayName,
pubDate: parseDate(p.publishStartAt),
image: p.thumbnailUrl
}));
return {
title: `${creatorInfo.displayName}のプロフィール|クリエイターページ|FANTUBE(ファンチューブ)`,
link: `${baseUrl}/r18/creator/${identifier}`,
description: creatorInfo.description,
image: creatorInfo.avatarImageUrl,
icon: creatorInfo.avatarImageUrl,
logo: creatorInfo.avatarImageUrl,
language: "ja",
item: items
};
}
//#endregion
export { route };