Skip to content

Commit

Permalink
feat(route): patreon (#17621)
Browse files Browse the repository at this point in the history
* feat(route): patreon

* fix: typo

* fix: typo
  • Loading branch information
TonyRL authored Nov 18, 2024
1 parent 437e7ca commit d9ca0d0
Show file tree
Hide file tree
Showing 5 changed files with 565 additions and 0 deletions.
6 changes: 6 additions & 0 deletions lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,9 @@ export type Config = {
notion: {
key?: string;
};
patreon: {
sessionId?: string;
};
pianyuan: {
cookie?: string;
};
Expand Down Expand Up @@ -670,6 +673,9 @@ const calculateValue = () => {
notion: {
key: envs.NOTION_TOKEN,
},
patreon: {
sessionId: envs.PATREON_SESSION_ID,
},
pianyuan: {
cookie: envs.PIANYUAN_COOKIE,
},
Expand Down
126 changes: 126 additions & 0 deletions lib/routes/patreon/feed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { Route } from '@/types';
import { CreatorData, MediaRelation, PostData } from './types';

import cache from '@/utils/cache';
import ofetch from '@/utils/ofetch';
import * as cheerio from 'cheerio';
import { parseDate } from '@/utils/parse-date';
import path from 'node:path';
import { getCurrentPath } from '@/utils/helpers';
import { art } from '@/utils/render';
import { config } from '@/config';

const __dirname = getCurrentPath(import.meta.url);

export const route: Route = {
path: '/:creator',
categories: ['new-media'],
example: '/patreon/straightupsisters',
parameters: { creator: 'Patreon creator id, can be found in the url' },
features: {
requireConfig: [
{
name: 'PATREON_SESSION_ID',
optional: true,
description: 'The value of the session_id cookie after logging in to Patreon, required to access paid posts',
},
],
},
radar: [
{
source: ['patreon.com/:creator'],
},
],
name: 'Home',
maintainers: ['TonyRL'],
handler,
};

async function handler(ctx) {
const { creator } = ctx.req.param();

const baseUrl = 'https://www.patreon.com';
const link = `${baseUrl}/${creator}`;

const creatorData = (await cache.tryGet(`patreon:creator:${creator}`, async () => {
const response = await ofetch(link);

const $ = cheerio.load(response);
const nextData = JSON.parse($('#__NEXT_DATA__').text());
const bootstrapEnvelope = nextData.props.pageProps.bootstrapEnvelope;

return {
meta: bootstrapEnvelope.meta,
id: bootstrapEnvelope.pageBootstrap.campaign.data.id,
attributes: bootstrapEnvelope.pageBootstrap.campaign.data.attributes,
};
})) as CreatorData;

if (!creatorData.id) {
throw new Error('Creator not found');
}

let headers = {};
if (config.patreon?.sessionId) {
headers = {
Cookie: `session_id=${config.patreon.sessionId}`,
};
}

const posts = await ofetch<PostData>('https://www.patreon.com/api/posts', {
headers,
query: {
include:
'campaign,access_rules,access_rules.tier.null,attachments_media,audio,audio_preview.null,drop,images,media,native_video_insights,poll.choices,poll.current_user_responses.user,poll.current_user_responses.choice,poll.current_user_responses.poll,user,user_defined_tags,ti_checks,video.null,content_unlock_options.product_variant.null',
'fields[campaign]': 'currency,show_audio_post_download_links,avatar_photo_url,avatar_photo_image_urls,earnings_visibility,is_nsfw,is_monthly,name,url',
'fields[post]':
'change_visibility_at,comment_count,commenter_count,content,created_at,current_user_can_comment,current_user_can_delete,current_user_can_report,current_user_can_view,current_user_comment_disallowed_reason,current_user_has_liked,embed,image,insights_last_updated_at,is_paid,like_count,meta_image_url,min_cents_pledged_to_view,monetization_ineligibility_reason,post_file,post_metadata,published_at,patreon_url,post_type,pledge_url,preview_asset_type,thumbnail,thumbnail_url,teaser_text,title,upgrade_url,url,was_posted_by_campaign_owner,has_ti_violation,moderation_status,post_level_suspension_removal_date,pls_one_liners_by_category,video,video_preview,view_count,content_unlock_options,is_new_to_current_user,watch_state',
'fields[post_tag]': 'tag_type,value',
'fields[user]': 'image_url,full_name,url',
'fields[access_rule]': 'access_rule_type,amount_cents',
'fields[media]': 'id,image_urls,display,download_url,metadata,file_name',
'fields[native_video_insights]': 'average_view_duration,average_view_pct,has_preview,id,last_updated_at,num_views,preview_views,video_duration',
'fields[content-unlock-option]': 'content_unlock_type',
'fields[product-variant]': 'price_cents,currency_code,checkout_url,is_hidden,published_at_datetime,content_type,orders_count,access_metadata',
'filter[campaign_id]': creatorData.id,
'filter[contains_exclusive_posts]': true,
'filter[is_draft]': false,
sort: '-published_at',
'json-api-use-default-includes': false,
'json-api-version': '1.0',
},
});

const items = posts.data.map(({ attributes, relationships }) => {
for (const [key, value] of Object.entries(relationships)) {
if (value.data) {
relationships[key] = Array.isArray(value.data) ? value.data.map((item) => posts.included.find((i) => i.id === item.id)) : posts.included.find((i) => i.id === value.data.id);
}
}
if (attributes.video_preview) {
relationships.video_preview = posts.included.find((i) => Number.parseInt(i.id) === attributes.video_preview.media_id) as unknown as MediaRelation;
}

return {
title: attributes.title,
description: art(path.join(__dirname, 'templates/description.art'), {
attributes,
relationships,
included: posts.included,
}),
link: attributes.url,
pubDate: parseDate(attributes.published_at),
image: attributes.thumbnail?.url ?? attributes.image.url,
category: relationships.user_defined_tags?.map((tag) => tag.attributes.value),
};
});

return {
title: creatorData.meta.title,
description: creatorData.meta.desc,
link,
image: creatorData.attributes.avatar_photo_url,
item: items,
allowEmpty: true,
};
}
7 changes: 7 additions & 0 deletions lib/routes/patreon/namespace.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { Namespace } from '@/types';

export const namespace: Namespace = {
name: 'Patreon',
url: 'www.patreon.com',
lang: 'en',
};
39 changes: 39 additions & 0 deletions lib/routes/patreon/templates/description.art
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{{ if attributes.post_type === 'image_file' }}
{{ each attributes.post_metadata.image_order mediaIdStr }}
{{ set img = included.find((i) => i.id === mediaIdStr) }}
{{ if img }}
<img src="{{ img.attributes.image_urls.original }}" alt="{{ img.attributes.file_name }}"><br>
{{ /if }}
{{ /each }}

{{ else if attributes.post_type === 'video_external_file' }}
{{ if attributes.video_preview }}
<video controls preload="metadata" poster="{{ attributes.image.url }}">
<source src="{{ relationships.video_preview.attributes.download_url }}" type="video/mp4">
</video><br>
{{ /if }}

{{ else if attributes.post_type === 'audio_file' || attributes.post_type === 'podcast' }}
<img src="{{ attributes.thumbnail.url }}"><br>
{{ set url = relationships.audio.attributes.download_url || relationships.audio_preview.attributes.download_url }}
<audio controls preload="metadata">
<source src="{{ url }}" type="audio/mpeg">
</audio><br>

{{ else if attributes.post_type === 'video_embed' || attributes.post_type === 'link' }}
<img src="{{ attributes.image.url }}"><br>

{{ else }}
Post type: "{{ attributes.post_type }}" is not supported. <br>

{{ /if }}

{{ if attributes.content || attributes.teaser_text }}
{{@ attributes.content || attributes.teaser_text }}
{{ /if }}

{{ if relationships.attachments_media }}
{{ each relationships.attachments_media media }}
<a href="{{ media.attributes.download_url }}">{{ media.attributes.file_name }}</a><br>
{{ /each }}
{{ /if }}
Loading

0 comments on commit d9ca0d0

Please sign in to comment.