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, + }; +};