feat(route): add 云听专辑 & 直播 (#10926)

* feat(route): add 云听专辑 & 直播

* fix: wrong enclosure_url

* update lib/v2/radio/album.js

Co-authored-by: Tony <TonyRL@users.noreply.github.com>

* update lib/v2/radio/album.js

Co-authored-by: Tony <TonyRL@users.noreply.github.com>

* update lib/v2/radio/album.js

Co-authored-by: Tony <TonyRL@users.noreply.github.com>

* update lib/v2/radio/index.js
This commit is contained in:
Ethan Shen 2022-09-29 21:42:58 +08:00 committed by GitHub
parent 5af05ea618
commit d1b4a05105
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 265 additions and 77 deletions

View File

@ -1656,9 +1656,55 @@ Tiny Tiny RSS 会给所有 iframe 元素添加 `sandbox="allow-scripts"` 属性
## 云听
### 电台节目
### 专辑
<Route author="kt286 nczitzk" example="/radio/1552135" path="/radio/:id" :paramsDesc="['节目ID可在对应专辑页面的 URL 中找到']" supportPodcast="1"/>
<Route author="nczitzk" example="/radio/album/15682090498666" path="/radio/album/:id" :paramsDesc="['专辑 id可在对应专辑页面的 URL 中找到']" supportPodcast="1">
如果订阅 [中国相声榜](https://www.radio.cn/pc-portal/sanji/detail.html?columnId=15682090498666),其 URL 为 <https://www.radio.cn/pc-portal/sanji/detail.html?columnId=15682090498666>,可以得到 `columnId``15682090498666`
所以对应路由为 [`/radio/album/15682090498666`](https://rsshub.app/radio/album/15682090498666)
::: tip 提示
部分专辑不适用该路由,此时可以尝试 [节目](#yun-ting-jie-mu) 路由
:::
</Route>
### 节目
<Route author="kt286 nczitzk" example="/radio/1552135" path="/radio/:id" :paramsDesc="['专辑 id可在对应专辑页面的 URL 中找到']" supportPodcast="1">
如果订阅 [共和国追梦人](http://www.radio.cn/pc-portal/sanji/detail.html?columnId=1552135),其 URL 为 <https://www.radio.cn/pc-portal/sanji/detail.html?columnId=1552135>,可以得到 `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` 长度较短
:::
</Route>
### 直播
<Route author="nczitzk" example="/radio/zhibo/1395528" path="/radio/zhibo/:id" :paramsDesc="['直播 id可在对应点播页面的 URL 中找到']" supportPodcast="1">
如果订阅 [新闻和报纸摘要](http://www.radio.cn/pc-portal/sanji/zhibo\_2.html?name=1395528),其 URL 为 <http://www.radio.cn/pc-portal/sanji/zhibo_2.html?name=1395528>,可以得到 `name``1395528`
所以对应路由为 [`/radio/zhibo/1395528`](https://rsshub.app/radio/zhibo/1395528)
::: tip 提示
查看更多电台直播节目,可前往 [电台直播](http://www.radio.cn/pc-portal/erji/radioStation.html)
:::
</Route>
## 中国高清网

96
lib/v2/radio/album.js Normal file
View File

@ -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}&timestamp=${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(),
};
};

View File

@ -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}`,

View File

@ -1,3 +1,5 @@
module.exports = {
'/album/:id': ['nczitzk'],
'/zhibo/:id': ['nczitzk'],
'/:id': ['kt286', 'nczitzk'],
};

View File

@ -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')}`,
},
],
},
};

View File

@ -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: `
<audio controls="controls">
<source src="http://${item.stream_domain1}${item.stream_url1}" type="audio/x-m4a">
<source src="http://${item.stream_domain2}${item.stream_url2}" type="audio/x-m4a">
<source src="http://${item.stream_domain3}${item.stream_url3}" type="audio/x-m4a">
</audio>
`,
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,
};
};

View File

@ -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'));
};

View File

@ -1,5 +1,8 @@
{{ if description }}
<p>{{ description }}</p>
{{ /if }}
{{ if enclosure_url && enclosure_type }}
<audio controls="controls">
{{each streams stream}}
<source src="{{ stream.url }}" type="audio/x-m4a">
{{/each}}
</audio>
<source src="{{ enclosure_url }}" type="{{ enclosure_type }}">
</audio>
{{ /if }}

70
lib/v2/radio/zhibo.js Normal file
View File

@ -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}&timestamp=${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,
};
};