feat(route): fansly (#14458)
This commit is contained in:
parent
6eef6cdcdc
commit
c13ae21755
|
|
@ -0,0 +1,4 @@
|
|||
module.exports = {
|
||||
'/tag/:tag': ['TonyRL'],
|
||||
'/user/:username': ['TonyRL'],
|
||||
};
|
||||
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
@ -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',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
module.exports = (router) => {
|
||||
router.get('/tag/:tag', require('./tag'));
|
||||
router.get('/user/:username', require('./post'));
|
||||
};
|
||||
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
<b>{{ title }}</b><br>
|
||||
{{ each options option }}
|
||||
{{ option.voteCount }}/{{ version }} {{ option.title }}<br>
|
||||
{{ /each }}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
{{ label }}<br>
|
||||
{{ currentPercentage }}% ${{ currentAmount / 1000 }} / ${{ goalAmount / 1000 }}
|
||||
|
|
@ -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,
|
||||
};
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
Loading…
Reference in New Issue