diff --git a/docs/multimedia.md b/docs/multimedia.md
index d24353617..79b3c53f9 100644
--- a/docs/multimedia.md
+++ b/docs/multimedia.md
@@ -1656,9 +1656,55 @@ Tiny Tiny RSS 会给所有 iframe 元素添加 `sandbox="allow-scripts"` 属性
## 云听
-### 电台节目
+### 专辑
-
+
+
+如果订阅 [中国相声榜](https://www.radio.cn/pc-portal/sanji/detail.html?columnId=15682090498666),其 URL 为 ,可以得到 `columnId` 为 `15682090498666`
+
+所以对应路由为 [`/radio/album/15682090498666`](https://rsshub.app/radio/album/15682090498666)
+
+::: tip 提示
+
+部分专辑不适用该路由,此时可以尝试 [节目](#yun-ting-jie-mu) 路由
+
+:::
+
+
+
+### 节目
+
+
+
+如果订阅 [共和国追梦人](http://www.radio.cn/pc-portal/sanji/detail.html?columnId=1552135),其 URL 为 ,可以得到 `columnId` 为 `1552135`
+
+所以对应路由为 [`/radio/1552135`](https://rsshub.app/radio/1552135)
+
+::: tip 提示
+
+该路由仅适用于更新时间较早的电台节目,如 [共和国追梦人](http://www.radio.cn/pc-portal/sanji/detail.html?columnId=1552135)
+
+与适用于 [专辑](#yun-ting-zhuan-ji) 路由的专辑其 `columnId` 长度相比,它们的 `columnId` 长度较短
+
+:::
+
+
+
+### 直播
+
+
+
+如果订阅 [新闻和报纸摘要](http://www.radio.cn/pc-portal/sanji/zhibo\_2.html?name=1395528),其 URL 为 ,可以得到 `name` 为 `1395528`
+
+所以对应路由为 [`/radio/zhibo/1395528`](https://rsshub.app/radio/zhibo/1395528)
+
+::: tip 提示
+
+查看更多电台直播节目,可前往 [电台直播](http://www.radio.cn/pc-portal/erji/radioStation.html)
+
+:::
+
+
## 中国高清网
diff --git a/lib/v2/radio/album.js b/lib/v2/radio/album.js
new file mode 100644
index 000000000..f6aec28a8
--- /dev/null
+++ b/lib/v2/radio/album.js
@@ -0,0 +1,96 @@
+const got = require('@/utils/got');
+const { parseDate } = require('@/utils/parse-date');
+const { art } = require('@/utils/render');
+const path = require('path');
+const CryptoJS = require('crypto-js');
+
+module.exports = async (ctx) => {
+ const KEY = 'f0fc4c668392f9f9a447e48584c214ee';
+
+ const id = ctx.params.id;
+ const size = ctx.query.limit ?? '100';
+
+ const params = `albumId=${id}&pageNo=0&pageSize=${size}`;
+
+ const rootUrl = 'https://www.radio.cn';
+ const apiRootUrl = 'https://ytmsout.radio.cn';
+ const detailRootUrl = 'https://ytapi.radio.cn';
+
+ const iconUrl = `${rootUrl}/pc-portal/image/icon_32.jpg`;
+ const currentUrl = `${rootUrl}/pc-portal/sanji/detail.html?columnId=${id}`;
+ const apiUrl = `${apiRootUrl}/web/appSingle/pageByAlbum?${params}`;
+ const detailUrl = `${detailRootUrl}/ytsrv/srv/wifimusicbox/demand/detail`;
+
+ const detailResponse = await got({
+ method: 'post',
+ url: detailUrl,
+ form: {
+ pageIndex: '0',
+ sortType: '1',
+ mobileId: '',
+ providerCode: '25010',
+ pid: id,
+ paySongFlag: '1',
+ richText: '1',
+ h5flag: '1',
+ },
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ version: '4.0.0',
+ providerCode: '25010',
+ equipmentSource: 'WEB',
+ },
+ });
+
+ const data = detailResponse.data;
+
+ const timestamp = new Date().getTime();
+ const sign = CryptoJS.MD5(`${params}×tamp=${timestamp}&key=${KEY}`).toString().toUpperCase();
+
+ const response = await got({
+ method: 'get',
+ url: apiUrl,
+ headers: {
+ sign,
+ timestamp,
+ 'Content-Type': 'application/json',
+ equipmentId: '0000',
+ platformCode: 'WEB',
+ },
+ });
+
+ const items = response.data.data.data.map((item) => {
+ let enclosure_url = item.playUrlHigh ?? item.playUrlLow;
+ enclosure_url = /\.m3u8$/.test(enclosure_url) ? item.downloadUrl : enclosure_url;
+
+ const enclosure_type = `audio/${enclosure_url.match(/\.(\w+)$/)[1]}`;
+
+ return {
+ guid: item.id,
+ title: item.name,
+ link: enclosure_url,
+ description: art(path.join(__dirname, 'templates/description.art'), {
+ description: item.des,
+ enclosure_url,
+ enclosure_type,
+ }),
+ author: data.anchorpersons,
+ pubDate: parseDate(item.publishTime),
+ enclosure_url,
+ enclosure_type,
+ enclosure_length: item.fileSize,
+ itunes_duration: item.duration,
+ itunes_item_image: iconUrl,
+ };
+ });
+
+ ctx.state.data = {
+ title: `云听 - ${data.columnName}`,
+ link: currentUrl,
+ item: items,
+ image: iconUrl,
+ itunes_author: data.anchorpersons,
+ description: data.descriptions ?? data.descriptionSimple,
+ itunes_category: data.atypeInfo.map((c) => c.categoryName).join(),
+ };
+};
diff --git a/lib/v2/radio/index.js b/lib/v2/radio/index.js
index 9e6f6d43a..182c4c9ed 100644
--- a/lib/v2/radio/index.js
+++ b/lib/v2/radio/index.js
@@ -5,12 +5,12 @@ const path = require('path');
module.exports = async (ctx) => {
const id = ctx.params.id;
+ const size = ctx.query.limit ?? '100';
- const rootUrl = 'http://www.radio.cn';
- const currentUrl = `${rootUrl}/pc-portal/sanji/detail.html?columnId=${id}`;
-
+ const rootUrl = 'https://www.radio.cn';
const apiRootUrl = 'http://tacc.radio.cn';
- const apiUrl = `${apiRootUrl}/pcpages/odchannelpages?od_id=${id}&start=1&rows=50`;
+ const apiUrl = `${apiRootUrl}/pcpages/odchannelpages?od_id=${id}&start=1&rows=${size}`;
+ const currentUrl = `${rootUrl}/pc-portal/sanji/detail.html?columnId=${id}`;
const response = await got({
method: 'get',
@@ -18,24 +18,31 @@ module.exports = async (ctx) => {
});
if (/^\(.*\)$/.test(response.data)) {
- response.data = JSON.parse(response.data)[0];
+ response.data = JSON.parse(response.data.match(/^\((.*)\)$/)[1]);
}
const data = response.data.data;
- const items = data.program.map((item) => ({
- guid: item.id,
- title: item.name,
- link: item.streams[0].url,
- description: `${item.description}${art(path.join(__dirname, 'templates/description.art'), {
- streams: item.streams,
- })}`,
- pubDate: parseDate(item.onlinetime),
- enclosure_url: item.streams[0].url,
- enclosure_type: 'audio/x-m4a',
- itunes_duration: item.duration,
- itunes_item_image: data.odchannel.imageUrl[0].url,
- }));
+ const items = data.program.map((item) => {
+ const enclosure_url = item.streams[0].url;
+ const enclosure_type = `audio/${enclosure_url.match(/\.(\w+)$/)[1]}`;
+
+ return {
+ guid: item.id,
+ title: item.name,
+ link: item.streams[0].url,
+ description: art(path.join(__dirname, 'templates/description.art'), {
+ description: item.description,
+ enclosure_url,
+ enclosure_type,
+ }),
+ pubDate: parseDate(item.onlinetime),
+ enclosure_url,
+ enclosure_type,
+ itunes_duration: item.duration,
+ itunes_item_image: data.odchannel.imageUrl[0].url,
+ };
+ });
ctx.state.data = {
title: `云听 - ${data.odchannel.name}`,
diff --git a/lib/v2/radio/maintainer.js b/lib/v2/radio/maintainer.js
index fac8655d8..cafcf1fd9 100644
--- a/lib/v2/radio/maintainer.js
+++ b/lib/v2/radio/maintainer.js
@@ -1,3 +1,5 @@
module.exports = {
+ '/album/:id': ['nczitzk'],
+ '/zhibo/:id': ['nczitzk'],
'/:id': ['kt286', 'nczitzk'],
};
diff --git a/lib/v2/radio/radar.js b/lib/v2/radio/radar.js
index fd7428fd9..93f72c909 100644
--- a/lib/v2/radio/radar.js
+++ b/lib/v2/radio/radar.js
@@ -3,11 +3,23 @@ module.exports = {
_name: '云听',
'.': [
{
- title: '电台节目',
- docs: 'https://docs.rsshub.app/multimedia.html#yun-ting-dian-tai-jie-mu',
- source: ['/pc-portal/sanji/detail.html', '/'],
+ title: '专辑',
+ docs: 'https://docs.rsshub.app/multimedia.html#yun-ting-zhuan-ji',
+ source: ['/pc-portal/sanji/detail.html', '/share/albumDetail', '/'],
+ target: (params, url) => `/radio/album/${new URL(url).searchParams.get('columnId')}`,
+ },
+ {
+ title: '节目',
+ docs: 'https://docs.rsshub.app/multimedia.html#yun-ting-jie-mu',
+ source: ['/pc-portal/sanji/detail.html', '/share/albumDetail', '/'],
target: (params, url) => `/radio/${new URL(url).searchParams.get('columnId')}`,
},
+ {
+ title: '直播',
+ docs: 'https://docs.rsshub.app/multimedia.html#yun-ting-zhi-bo',
+ source: ['/pc-portal/sanji/zhibo_2.html', '/'],
+ target: (params, url) => `/radio/zhibo/${new URL(url).searchParams.get('name')}`,
+ },
],
},
};
diff --git a/lib/v2/radio/radio.js b/lib/v2/radio/radio.js
deleted file mode 100644
index 4534cd253..000000000
--- a/lib/v2/radio/radio.js
+++ /dev/null
@@ -1,50 +0,0 @@
-const got = require('@/utils/got');
-
-module.exports = async (ctx) => {
- const channelname = ctx.params.channelname; // 专辑id
- const name = ctx.params.name; // 专辑id
-
- const api = `http://tacc.radio.cn/pcpages/searchs/livehistory?channelname=${channelname}&name=${name}&start=1&rows=20`;
- const response = await got.get(api);
-
- const playList = response.data.passprogram;
- let title = '';
- let itunes_category = '';
- let items = [];
- if (playList && playList.length > 0) {
- const first = playList[0];
- title = `${first.channel_name} ${first.name}`;
- itunes_category = first.channel_name;
-
- items = playList.map((item) => {
- const single = {
- title: `${item.name} ${item.broadcast_date}`,
- link: 'http://' + item.stream_domain1 + item.stream_url1,
- description: `
-
- `,
- pubDate: item.update_time,
- guid: item.id,
- itunes_item_image: 'http://www.radio.cn/pc-portal/image/icon_32.jpg',
- enclosure_url: 'http://' + item.stream_domain1 + item.stream_url1,
- itunes_duration: item.resource_length,
- enclosure_type: 'audio/x-m4a',
- };
- return single;
- });
- }
-
- ctx.state.data = {
- title,
- link: `http://www.radio.cn/pc-portal/sanji/zhibo_2.html?channelname=${channelname}&name=${name}&title=radio`,
- description: title,
- image: 'http://www.radio.cn/pc-portal/image/icon_32.jpg',
- itunes_author: 'radio.cn',
- itunes_category,
- item: items,
- };
-};
diff --git a/lib/v2/radio/router.js b/lib/v2/radio/router.js
index 641c23df3..dd022b028 100644
--- a/lib/v2/radio/router.js
+++ b/lib/v2/radio/router.js
@@ -1,3 +1,5 @@
module.exports = function (router) {
+ router.get('/album/:id', require('./album'));
+ router.get('/zhibo/:id', require('./zhibo'));
router.get('/:id', require('./index'));
};
diff --git a/lib/v2/radio/templates/description.art b/lib/v2/radio/templates/description.art
index 6325c012a..995d8dd8d 100644
--- a/lib/v2/radio/templates/description.art
+++ b/lib/v2/radio/templates/description.art
@@ -1,5 +1,8 @@
+{{ if description }}
+
{{ description }}
+{{ /if }}
+{{ if enclosure_url && enclosure_type }}
\ No newline at end of file
+
+
+{{ /if }}
\ No newline at end of file
diff --git a/lib/v2/radio/zhibo.js b/lib/v2/radio/zhibo.js
new file mode 100644
index 000000000..ea4a3540e
--- /dev/null
+++ b/lib/v2/radio/zhibo.js
@@ -0,0 +1,70 @@
+const got = require('@/utils/got');
+const { parseDate } = require('@/utils/parse-date');
+const { art } = require('@/utils/render');
+const path = require('path');
+const CryptoJS = require('crypto-js');
+
+module.exports = async (ctx) => {
+ const KEY = 'f0fc4c668392f9f9a447e48584c214ee';
+
+ const id = ctx.params.id;
+ const size = ctx.query.limit ?? '100';
+
+ const params = `columnId=${id}&pageNo=0&pageSize=${size}`;
+
+ const rootUrl = 'https://www.radio.cn';
+ const apiRootUrl = 'https://ytmsout.radio.cn';
+
+ const iconUrl = `${rootUrl}/pc-portal/image/icon_32.jpg`;
+ const currentUrl = `${rootUrl}/pc-portal/sanji/zhibo_2.html?name=${id}`;
+ const apiUrl = `${apiRootUrl}/web/appProgram/pageByColumn?${params}`;
+
+ const timestamp = new Date().getTime();
+ const sign = CryptoJS.MD5(`${params}×tamp=${timestamp}&key=${KEY}`).toString().toUpperCase();
+
+ const response = await got({
+ method: 'get',
+ url: apiUrl,
+ headers: {
+ sign,
+ timestamp,
+ 'Content-Type': 'application/json',
+ equipmentId: '0000',
+ platformCode: 'WEB',
+ },
+ });
+
+ const data = response.data.data.data;
+
+ const items = data.map((item) => {
+ let enclosure_url = item.playUrlHigh ?? item.playUrlLow;
+ enclosure_url = /\.m3u8$/.test(enclosure_url) ? item.downloadUrl : enclosure_url;
+
+ const enclosure_type = `audio/${enclosure_url.match(/\.(\w+)$/)[1]}`;
+
+ return {
+ guid: item.id,
+ title: item.name,
+ link: enclosure_url,
+ description: art(path.join(__dirname, 'templates/description.art'), {
+ description: item.des,
+ enclosure_url,
+ enclosure_type,
+ }),
+ pubDate: parseDate(item.startTime),
+ enclosure_url,
+ enclosure_type,
+ itunes_duration: item.durationStr,
+ itunes_item_image: iconUrl,
+ };
+ });
+
+ ctx.state.data = {
+ title: `云听 - ${data[0].name}`,
+ link: currentUrl,
+ item: items,
+ image: iconUrl,
+ itunes_author: 'radio.cn',
+ description: data[0].des,
+ };
+};