feat(route): fansly (#14458)

This commit is contained in:
Tony 2024-02-15 02:51:21 +08:00 committed by GitHub
parent 6eef6cdcdc
commit c13ae21755
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 286 additions and 0 deletions

View File

@ -0,0 +1,4 @@
module.exports = {
'/tag/:tag': ['TonyRL'],
'/user/:username': ['TonyRL'],
};

29
lib/v2/fansly/post.js Normal file
View File

@ -0,0 +1,29 @@
const { parseDate } = require('@/utils/parse-date');
const { getAccountByUsername, getTimelineByAccountId, parseDescription, baseUrl } = require('./utils');
module.exports = async (ctx) => {
const { username } = ctx.params;
const account = await getAccountByUsername(username, ctx.cache.tryGet);
const timeline = await getTimelineByAccountId(account.id);
const items = timeline.posts.map((post) => ({
title: post.content.split('\n')[0],
description: parseDescription(post, timeline),
pubDate: parseDate(post.createdAt, 'X'),
link: `${baseUrl}/post/${post.id}`,
author: `${account.displayName ?? account.username} (@${account.username})`,
}));
ctx.state.data = {
title: `${account.displayName ?? account.username} (@${account.username}) - Fansly`,
link: `${baseUrl}/${account.username}`,
description: account.about.replaceAll('\n', ' '),
image: account.banner.locations[0].location,
icon: account.avatar.locations[0].location,
logo: account.avatar.locations[0].location,
language: 'en',
allowEmpty: true,
item: items,
};
};

19
lib/v2/fansly/radar.js Normal file
View File

@ -0,0 +1,19 @@
module.exports = {
'fansly.com': {
_name: 'Fansly',
'.': [
{
title: 'User Timeline',
docs: 'https://docs.rsshub.app/routes/social-media.html#fansly',
source: ['/:username/posts', '/:username/media'],
target: '/fansly/user/:username',
},
{
title: 'Hashtag',
docs: 'https://docs.rsshub.app/routes/social-media.html#fansly',
source: ['/explore/tag/:tag'],
target: '/fansly/tag/:tag',
},
],
},
};

4
lib/v2/fansly/router.js Normal file
View File

@ -0,0 +1,4 @@
module.exports = (router) => {
router.get('/tag/:tag', require('./tag'));
router.get('/user/:username', require('./post'));
};

30
lib/v2/fansly/tag.js Normal file
View File

@ -0,0 +1,30 @@
const { parseDate } = require('@/utils/parse-date');
const { getTagId, getTagSuggestion, findAccountById, parseDescription, baseUrl, icon } = require('./utils');
module.exports = async (ctx) => {
const { tag } = ctx.params;
const tagId = await getTagId(tag, ctx.cache.tryGet);
const suggestion = await getTagSuggestion(tagId);
const items = suggestion.aggregationData?.posts.map((post) => {
const account = findAccountById(post.accountId, suggestion.aggregationData.accounts);
return {
title: post.content.split('\n')[0],
description: parseDescription(post, suggestion.aggregationData),
pubDate: parseDate(post.createdAt, 'X'),
link: `${baseUrl}/post/${post.id}`,
author: `${account.displayName ?? account.username} (@${account.username})`,
};
});
ctx.state.data = {
title: `#${tag} - Fansly`,
link: `${baseUrl}/explore/tag/${tag}`,
image: icon,
icon,
logo: icon,
language: 'en',
item: items,
};
};

View File

@ -0,0 +1,8 @@
{{ if poster && src }}
<video controls preload="none" poster="{{ poster.location }}">
<source src="{{ src.location }}" type="video/mp4">
</video>
{{ else if src }}
<img src="{{ src.location }}">
{{ /if }}
<br>

View File

@ -0,0 +1,4 @@
<b>{{ title }}</b><br>
{{ each options option }}
{{ option.voteCount }}/{{ version }} {{ option.title }}<br>
{{ /each }}

View File

@ -0,0 +1,2 @@
{{ label }}<br>
{{ currentPercentage }}% ${{ currentAmount / 1000 }} / ${{ goalAmount / 1000 }}

176
lib/v2/fansly/utils.js Normal file
View File

@ -0,0 +1,176 @@
const got = require('@/utils/got');
const path = require('path');
const { art } = require('@/utils/render');
const apiBaseUrl = 'https://apiv3.fansly.com';
const baseUrl = 'https://fansly.com';
const icon = `${baseUrl}/assets/images/icons/apple-touch-icon.png`;
const findAccountById = (accountId, accounts) => {
const account = accounts.find((account) => account.id === accountId);
return {
displayName: account.displayName,
username: account.username,
};
};
const getAccountByUsername = (username, tryGet) =>
tryGet(`fansly:account:${username.toLowerCase()}`, async () => {
const { data: accountResponse } = await got(`${apiBaseUrl}/api/v1/account`, {
searchParams: {
usernames: username,
'ngsw-bypass': true,
},
});
if (!accountResponse.response.length) {
throw new Error('This profile or page does not exist.');
}
return accountResponse.response[0];
});
const getTimelineByAccountId = async (accountId) => {
const { data: timeline } = await got(`${apiBaseUrl}/api/v1/timelinenew/${accountId}`, {
searchParams: {
before: 0,
after: 0,
wallId: '',
contentSearch: '',
'ngsw-bypass': true,
},
});
return timeline.response;
};
const getTagId = (tag, tryGet) =>
tryGet(`fansly:tag:${tag.toLowerCase()}`, async () => {
const { data: tagResponse } = await got(`${apiBaseUrl}/api/v1/contentdiscovery/media/tag`, {
searchParams: {
tag,
'ngsw-bypass': true,
},
});
if (!tagResponse.response.mediaOfferSuggestionTag) {
throw new Error("Couldn't find this hashtag.");
}
return tagResponse.response.mediaOfferSuggestionTag.id;
});
const getTagSuggestion = async (tagId) => {
const { data: suggestionResponse } = await got(`${apiBaseUrl}/api/v1/contentdiscovery/media/suggestionsnew`, {
searchParams: {
before: 0,
after: 0,
tagIds: tagId,
limit: 25,
offset: 0,
'ngsw-bypass': true,
},
});
return suggestionResponse.response;
};
const parseDescription = (post, aggregationData) => post.content.replaceAll('\n', '<br>') + '<br>' + parseAttachments(post.attachments, aggregationData);
const parseAttachments = (attachments, aggregationData) =>
attachments
.map((attachment) => {
switch (attachment.contentType) {
case 1:
// single media
return parseMedia(attachment.contentId, aggregationData.accountMedia);
case 2: {
// media bundle
let attachments = '';
const bundle = aggregationData.accountMediaBundles.find((bundle) => bundle.id === attachment.contentId);
for (const mediaId of bundle.accountMediaIds) {
attachments += parseMedia(mediaId, aggregationData.accountMedia);
}
return attachments;
}
case 8: {
// aggregated post (repost)
let attachments = '<br><br>';
const repost = aggregationData.aggregatedPosts.find((post) => post.id === attachment.contentId) || aggregationData.posts.find((post) => post.id === attachment.contentId);
attachments += parseDescription(repost, aggregationData);
return attachments;
}
case 7100:
return renderTipGoal(attachment.contentId, aggregationData.tipGoals);
case 32001:
// unknown
return '';
case 42001:
return renderPoll(attachment.contentId, aggregationData.polls);
default:
throw new Error(`Unhandled attachment type: ${attachment.contentType} for post ${attachment.postId}`);
}
})
.join('');
const parseMedia = (contentId, accountMedia) => {
const media = accountMedia.find((media) => media.id === contentId);
if (!media) {
return '';
}
return renderMedia(media.preview ?? media.media);
};
const renderMedia = (media) => {
switch (media.mimetype) {
case 'image/gif':
case 'image/jpeg':
case 'image/png':
case 'video/mp4':
case 'audio/mp4':
return art(path.join(__dirname, 'templates/media.art'), {
poster: media.mimetype === 'video/mp4' ? media.variants[0].locations[0] : null,
src: media.locations[0],
});
default:
throw new Error(`Unhandled media type: ${media.mimetype}`);
}
};
const renderPoll = (pollId, polls) => {
const poll = polls.find((poll) => poll.id === pollId);
return art(path.join(__dirname, 'templates/poll.art'), {
title: poll.question,
options: poll.options,
version: poll.version,
});
};
const renderTipGoal = (tipGoalId, tipGoals) => {
const goal = tipGoals.find((goal) => goal.id === tipGoalId);
return art(path.join(__dirname, 'templates/tip-goal.art'), {
label: goal.label,
description: goal.description,
currentAmount: goal.currentAmount,
goalAmount: goal.goalAmount,
currentPercentage: goal.currentPercentage,
});
};
module.exports = {
findAccountById,
baseUrl,
icon,
getAccountByUsername,
getTimelineByAccountId,
getTagId,
getTagSuggestion,
parseAttachments,
parseDescription,
parseMedia,
renderMedia,
renderPoll,
renderTipGoal,
};

View File

@ -413,6 +413,16 @@
<Route author="DIYgod" path="/disqus/posts/:forum" example="/disqus/posts/diygod-me" paramsDesc={['forum, disqus name of the target website']} />
## Fansly {#fansly}
### User Timeline {#fansly-user-timeline}
<Route author="TonyRL" example="/fansly/user/AeriGoMoo" path="/fansly/user/:username" paramsDesc={['User ID']} radar="1" />
### Hashtag {#fansly-hashtag}
<Route author="TonyRL" example="/fansly/tag/free" path="/fansly/tag/:tag" paramsDesc={['Hashtag']} radar="1" />
## Fur Affinity {#fur-affinity}
### Home {#fur-affinity-home}