feat(route): youtube community (#11986)

* feat(route): youtube community

* fix: add docs
This commit is contained in:
Tony 2023-02-28 07:44:25 -11:00 committed by GitHub
parent 6002eb6e11
commit 3c1aefc49f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 94 additions and 5 deletions

View File

@ -548,6 +548,10 @@ YouTube provides official RSS feeds for channels, for instance <https://www.yout
<RouteEn author="HenryQW" path="/youtube/playlist/:id/:embed?" example="/youtube/playlist/PLqQ1RwlxOgeLTJ1f3fNMSwhjVgaWKo_9Z" :paramsDesc="['YouTube playlist id', 'Default to embed the video, set to any value to disable embedding']" radar="1" rssbud="1"/>
### Community
<RouteEn author="TonyRL" path="/youtube/community/:handle" example="/youtube/community/@JFlaMusic" :paramsDesc="['YouTube handles or channel id']" radar="1" rssbud="1"/>
### Subscriptions
<RouteEn author="TonyRL" path="/youtube/subscriptions/:embed?" example="/youtube/subscriptions" :paramsDesc="['Default to embed the video, set to any value to disable embedding']" selfhost="1" radar="1" rssbud="1"/>

View File

@ -929,6 +929,10 @@ YouTube 官方亦有提供频道 RSS形如 <https://www.youtube.com/feeds/vid
<Route author="HenryQW" example="/youtube/playlist/PLqQ1RwlxOgeLTJ1f3fNMSwhjVgaWKo_9Z" path="/youtube/playlist/:id/:disableEmbed?" :paramsDesc="['播放列表 id', '默认为开启内嵌视频,任意值为关闭']" radar="1" rssbud="1"/>
### 社群
<Route author="TonyRL" path="/youtube/community/:handle" example="/youtube/community/@JFlaMusic" :paramsDesc="['YouTube 帐号代码或频道 id']" radar="1" rssbud="1"/>
### 订阅列表
<Route author="TonyRL" path="/youtube/subscriptions/:embed?" example="/youtube/subscriptions" :paramsDesc="['默认为开启内嵌视频,任意值为关闭']" selfhost="1" radar="1" rssbud="1"/>

View File

@ -4,14 +4,13 @@ const { parseDate } = require('@/utils/parse-date');
module.exports = async (ctx) => {
if (!config.youtube || !config.youtube.key) {
throw 'YouTube RSS is disabled due to the lack of <a href="https://docs.rsshub.app/install/#pei-zhi-bu-fen-rss-mo-kuai-pei-zhi">relevant config</a>';
throw Error('YouTube RSS is disabled due to the lack of <a href="https://docs.rsshub.app/install/#pei-zhi-bu-fen-rss-mo-kuai-pei-zhi">relevant config</a>');
}
const id = ctx.params.id;
const embed = !ctx.params.embed;
// taken from https://webapps.stackexchange.com/a/101153
if (!/^UC[\w-]{21}[AQgw]$/.test(id)) {
throw `Invalid YouTube channel ID. \nYou may want to use <a href="/youtube/user/${id}">/youtube/user/${id}</a> instead.`;
if (!utils.isYouTubeChannelId(id)) {
throw Error(`Invalid YouTube channel ID. \nYou may want to use <code>/youtube/user/:id</code> instead.`);
}
const playlistId = (await utils.getChannelWithId(id, 'contentDetails', ctx.cache)).data.items[0].contentDetails.relatedPlaylists.uploads;

View File

@ -0,0 +1,56 @@
const got = require('@/utils/got');
const cheerio = require('cheerio');
const { parseRelativeDate } = require('@/utils/parse-date');
const { art } = require('@/utils/render');
const path = require('path');
const { isYouTubeChannelId } = require('./utils');
module.exports = async (ctx) => {
const { handle } = ctx.params;
let urlPath = handle;
if (isYouTubeChannelId(handle)) {
urlPath = `channel/${handle}`;
}
const { data: response } = await got(`https://www.youtube.com/${urlPath}/community`);
const $ = cheerio.load(response);
const ytInitialData = JSON.parse(
$('script')
.text()
.match(/ytInitialData = ({.*?});/)[1]
);
const channelMetadata = ytInitialData.metadata.channelMetadataRenderer;
const username = channelMetadata.title;
const communityTab = ytInitialData.contents.twoColumnBrowseResultsRenderer.tabs.find((tab) => tab.tabRenderer.endpoint.commandMetadata.webCommandMetadata.url.endsWith('/community'));
const list = communityTab.tabRenderer.content.sectionListRenderer.contents[0].itemSectionRenderer.contents;
if (list[0].messageRenderer) {
throw Error(list[0].messageRenderer.text.runs[0].text);
}
const items = list
.filter((i) => i.backstagePostThreadRenderer)
.map((item) => {
const post = item.backstagePostThreadRenderer.post.backstagePostRenderer;
const media = post.backstageAttachment?.postMultiImageRenderer?.images.map((i) => i.backstageImageRenderer.image.thumbnails.pop()) ?? [post.backstageAttachment?.backstageImageRenderer?.image.thumbnails.pop()];
return {
title: post.contentText.runs[0].text,
description: art(path.join(__dirname, 'templates', 'community.art'), {
runs: post.contentText.runs,
media,
}),
link: `https://www.youtube.com/post/${post.postId}`,
author: post.authorText.runs[0].text,
pubDate: parseRelativeDate(post.publishedTimeText.runs[0].text.split('(')[0]),
};
});
ctx.state.data = {
title: `${username} - Community - YouTube`,
link: channelMetadata.channelUrl,
description: channelMetadata.description,
item: items,
};
};

View File

@ -28,6 +28,12 @@ module.exports = {
source: '/c/:id',
target: '/youtube/c/:id',
},
{
title: '社群',
docs: 'https://docs.rsshub.app/social-media.html#youtube',
source: ['/channel/:handle/community', '/channel/:handle', '/:handle/community', '/:handle/featured', '/:handle'],
target: (params) => (params.handle.startsWith('@') || params.handle.startsWith('UC') ? `/youtube/community/${params.handle}` : ''),
},
{
title: '播放列表',
docs: 'https://docs.rsshub.app/social-media.html#youtube',

View File

@ -2,6 +2,7 @@ module.exports = (router) => {
router.get('/c/:username/:embed?', require('./custom'));
router.get('/channel/:id/:embed?', require('./channel'));
router.get('/charts/:category?/:country?/:embed?', require('./charts'));
router.get('/community/:handle', require('./community'));
router.get('/playlist/:id/:embed?', require('./playlist'));
router.get('/subscriptions/:embed?', require('./subscriptions'));
router.get('/user/:username/:embed?', require('./user'));

View File

@ -0,0 +1,17 @@
{{ if runs }}
{{ each runs run }}
{{ if run.navigationEndpoint }}
{{ set url = run.navigationEndpoint.commandMetadata.webCommandMetadata.url }}
<a href="{{ url.startsWith('https://') ? url : `https://www.youtube.com${url}` }}">{{ run.text }}</a>
{{ else }}
{{@ run.text.replace(/\n/g, '<br>') }}
{{ /if }}
{{ /each }}
{{ /if }}
{{ if media }}
<br>
{{ each media i }}
{{ if i?.url }}<img src="{{ i.url }}">{{ /if }}
{{ /each }}
{{ /if }}

View File

@ -6,7 +6,7 @@ const cheerio = require('cheerio');
module.exports = async (ctx) => {
if (!config.youtube || !config.youtube.key) {
throw 'YouTube RSS is disabled due to the lack of <a href="https://docs.rsshub.app/install/#pei-zhi-bu-fen-rss-mo-kuai-pei-zhi">relevant config</a>';
throw Error('YouTube RSS is disabled due to the lack of <a href="https://docs.rsshub.app/install/#pei-zhi-bu-fen-rss-mo-kuai-pei-zhi">relevant config</a>');
}
const username = ctx.params.username;
const embed = !ctx.params.embed;

View File

@ -136,6 +136,8 @@ const youtubeUtils = {
}
return res;
},
// taken from https://webapps.stackexchange.com/a/101153
isYouTubeChannelId: (id) => /^UC[\w-]{21}[AQgw]$/.test(id),
};
module.exports = youtubeUtils;