diff --git a/docs/live.md b/docs/live.md
index 2a289d5da..1738d1ad0 100644
--- a/docs/live.md
+++ b/docs/live.md
@@ -32,6 +32,12 @@ pageClass: routes
+## 抖音直播
+
+### 直播间开播
+
+
+
## 斗鱼直播
### 直播间开播
diff --git a/docs/social-media.md b/docs/social-media.md
index b62b654e1..5fd4d21c0 100644
--- a/docs/social-media.md
+++ b/docs/social-media.md
@@ -875,6 +875,10 @@ YouTube 官方亦有提供频道 RSS,形如
+### 直播
+
+见 [#抖音直播](/live.html#dou-yin-zhi-bo)
+
## 豆瓣
### 正在上映的电影
diff --git a/lib/v2/douyin/hashtag.js b/lib/v2/douyin/hashtag.js
index b3d325bca..576a8c08c 100644
--- a/lib/v2/douyin/hashtag.js
+++ b/lib/v2/douyin/hashtag.js
@@ -1,51 +1,9 @@
const cheerio = require('cheerio');
const { parseDate } = require('@/utils/parse-date');
const { art } = require('@/utils/render');
-const path = require('path');
const config = require('@/config').value;
const { fallback, queryToBoolean } = require('@/utils/readable-social');
-
-const templates = {
- desc: path.join(__dirname, 'templates/desc.art'),
- cover: path.join(__dirname, 'templates/cover.art'),
- embed: path.join(__dirname, 'templates/embed.art'),
- iframe: path.join(__dirname, 'templates/iframe.art'),
-};
-
-const resolveUrl = (url, tls = true, forceResolve = false) => {
- if (!url) {
- return '';
- }
- if (url.startsWith('//')) {
- return (tls ? 'https:' : 'http:') + url;
- }
- if (forceResolve && !url.match(/^https?:\/\//)) {
- return (tls ? 'https://' : 'http://') + url;
- }
- return url;
-};
-
-const proxyVideo = (url, proxy) => {
- if (!(url && proxy)) {
- return url + '';
- }
- if (proxy.includes('?')) {
- if (!proxy.endsWith('=')) {
- proxy += '=';
- }
- return proxy + encodeURIComponent(url);
- } else {
- if (!proxy.endsWith('/')) {
- proxy += '/';
- }
- return proxy + url;
- }
-};
-
-const getOriginAvatar = (url) =>
- resolveUrl(url)
- .replace(/^(.*\.douyinpic\.com\/).*(\/aweme-avatar\/)([^?]*)(\?.*)?$/, '$1origin$2$3')
- .replace(/~\w+_\d+x\d+/g, '');
+const { templates, resolveUrl, proxyVideo, getOriginAvatar } = require('./utils');
module.exports = async (ctx) => {
const { cid } = ctx.params;
diff --git a/lib/v2/douyin/live.js b/lib/v2/douyin/live.js
new file mode 100644
index 000000000..b3a139d7a
--- /dev/null
+++ b/lib/v2/douyin/live.js
@@ -0,0 +1,68 @@
+const config = require('@/config').value;
+const { getOriginAvatar } = require('./utils');
+
+module.exports = async (ctx) => {
+ const { rid } = ctx.params;
+ if (isNaN(rid)) {
+ throw Error('Invalid room ID. Room ID should be a number.');
+ }
+
+ const pageUrl = `https://live.douyin.com/${rid}`;
+
+ const renderData = await ctx.cache.tryGet(
+ `douyin:live:${rid}`,
+ async () => {
+ const browser = await require('@/utils/puppeteer')();
+ const page = await browser.newPage();
+ await page.setRequestInterception(true);
+ page.on('request', (request) => {
+ request.resourceType() === 'document' ? request.continue() : request.abort();
+ });
+ await page.goto(pageUrl, {
+ waitUntil: 'domcontentloaded',
+ });
+ const data = await page.evaluate(() => renderData);
+ browser.close();
+
+ return data;
+ },
+ config.cache.routeExpire,
+ false
+ );
+
+ if (renderData.routeInitialProps.status_code !== '0') {
+ throw `Status code ${renderData.routeInitialProps.status_code}: ${renderData.routeInitialProps.error.title}`;
+ }
+
+ const roomInfo = renderData.initialState.roomStore.roomInfo;
+ const nickname = roomInfo.anchor.nickname;
+ const userAvatar = roomInfo.anchor.avatar_thumb.url_list[0];
+
+ const items = [];
+ if (roomInfo.roomId) {
+ if (roomInfo.room.status === 2) {
+ items.push({
+ title: `开播:${roomInfo.room.title}`,
+ link: pageUrl,
+ author: nickname,
+ guid: roomInfo.roomId, // roomId is unique for each live event
+ });
+ } else if (roomInfo.room.status === 4) {
+ items.push({
+ title: `当前直播已结束,期待下一场:${roomInfo.room.title}`,
+ link: `https://www.douyin.com/user/${roomInfo.anchor.sec_uid}`,
+ author: nickname,
+ guid: roomInfo.roomId,
+ });
+ }
+ }
+
+ ctx.state.data = {
+ title: `${nickname}的抖音直播间 - 抖音直播`,
+ description: `欢迎来到${nickname}的抖音直播间,${nickname}与大家一起记录美好生活 - 抖音直播`,
+ image: getOriginAvatar(userAvatar),
+ link: pageUrl,
+ item: items,
+ allowEmpty: true,
+ };
+};
diff --git a/lib/v2/douyin/radar.js b/lib/v2/douyin/radar.js
index 9f438c9d2..af92f6938 100644
--- a/lib/v2/douyin/radar.js
+++ b/lib/v2/douyin/radar.js
@@ -15,5 +15,13 @@ module.exports = {
target: '/douyin/user/:uid',
},
],
+ live: [
+ {
+ title: '直播间开播',
+ docs: 'https://docs.rsshub.app/live.html#dou-yin-zhi-bo',
+ source: '/:rid',
+ target: '/douyin/live/:rid',
+ },
+ ],
},
};
diff --git a/lib/v2/douyin/router.js b/lib/v2/douyin/router.js
index 6faf82f43..c7cfd8837 100644
--- a/lib/v2/douyin/router.js
+++ b/lib/v2/douyin/router.js
@@ -1,4 +1,5 @@
module.exports = function (router) {
router.get('/hashtag/:cid/:routeParams?', require('./hashtag'));
+ router.get('/live/:rid', require('./live'));
router.get('/user/:uid/:routeParams?', require('./user'));
};
diff --git a/lib/v2/douyin/user.js b/lib/v2/douyin/user.js
index fccae1a84..6dd7d7fd5 100644
--- a/lib/v2/douyin/user.js
+++ b/lib/v2/douyin/user.js
@@ -1,51 +1,9 @@
const cheerio = require('cheerio');
const { parseDate } = require('@/utils/parse-date');
const { art } = require('@/utils/render');
-const path = require('path');
const config = require('@/config').value;
const { fallback, queryToBoolean } = require('@/utils/readable-social');
-
-const templates = {
- desc: path.join(__dirname, 'templates/desc.art'),
- cover: path.join(__dirname, 'templates/cover.art'),
- embed: path.join(__dirname, 'templates/embed.art'),
- iframe: path.join(__dirname, 'templates/iframe.art'),
-};
-
-const resolveUrl = (url, tls = true, forceResolve = false) => {
- if (!url) {
- return '';
- }
- if (url.startsWith('//')) {
- return (tls ? 'https:' : 'http:') + url;
- }
- if (forceResolve && !url.match(/^https?:\/\//)) {
- return (tls ? 'https://' : 'http://') + url;
- }
- return url;
-};
-
-const proxyVideo = (url, proxy) => {
- if (!(url && proxy)) {
- return url + '';
- }
- if (proxy.includes('?')) {
- if (!proxy.endsWith('=')) {
- proxy += '=';
- }
- return proxy + encodeURIComponent(url);
- } else {
- if (!proxy.endsWith('/')) {
- proxy += '/';
- }
- return proxy + url;
- }
-};
-
-const getOriginAvatar = (url) =>
- resolveUrl(url)
- .replace(/^(.*\.douyinpic\.com\/).*(\/aweme-avatar\/)([^?]*)(\?.*)?$/, '$1origin$2$3')
- .replace(/~\w+_\d+x\d+/g, '');
+const { templates, resolveUrl, proxyVideo, getOriginAvatar } = require('./utils');
module.exports = async (ctx) => {
const uid = ctx.params.uid;
diff --git a/lib/v2/douyin/utils.js b/lib/v2/douyin/utils.js
new file mode 100644
index 000000000..8725f7dbd
--- /dev/null
+++ b/lib/v2/douyin/utils.js
@@ -0,0 +1,50 @@
+const path = require('path');
+
+const templates = {
+ desc: path.join(__dirname, 'templates/desc.art'),
+ cover: path.join(__dirname, 'templates/cover.art'),
+ embed: path.join(__dirname, 'templates/embed.art'),
+ iframe: path.join(__dirname, 'templates/iframe.art'),
+};
+
+const resolveUrl = (url, tls = true, forceResolve = false) => {
+ if (!url) {
+ return '';
+ }
+ if (url.startsWith('//')) {
+ return (tls ? 'https:' : 'http:') + url;
+ }
+ if (forceResolve && !url.match(/^https?:\/\//)) {
+ return (tls ? 'https://' : 'http://') + url;
+ }
+ return url;
+};
+
+const proxyVideo = (url, proxy) => {
+ if (!(url && proxy)) {
+ return url + '';
+ }
+ if (proxy.includes('?')) {
+ if (!proxy.endsWith('=')) {
+ proxy += '=';
+ }
+ return proxy + encodeURIComponent(url);
+ } else {
+ if (!proxy.endsWith('/')) {
+ proxy += '/';
+ }
+ return proxy + url;
+ }
+};
+
+const getOriginAvatar = (url) =>
+ resolveUrl(url)
+ .replace(/^(.*\.douyinpic\.com\/).*(\/aweme-avatar\/)([^?]*)(\?.*)?$/, '$1origin$2$3')
+ .replace(/~\w+_\d+x\d+/g, '');
+
+module.exports = {
+ templates,
+ resolveUrl,
+ proxyVideo,
+ getOriginAvatar,
+};