feat(route): youtube community (#11986)
* feat(route): youtube community * fix: add docs
This commit is contained in:
parent
6002eb6e11
commit
3c1aefc49f
|
|
@ -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"/>
|
||||
|
|
|
|||
|
|
@ -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"/>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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'));
|
||||
|
|
|
|||
|
|
@ -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 }}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in New Issue