diff --git a/docs/install/README.md b/docs/install/README.md index 0adcbab068..ebcf42a91c 100644 --- a/docs/install/README.md +++ b/docs/install/README.md @@ -560,3 +560,7 @@ RSSHub 支持使用访问密钥 / 码,白名单和黑名单三种方式进行 - `NGA_PASSPORT_UID`: 对应 cookie 中的 `ngaPassportUid`. - `NGA_PASSPORT_CID`: 对应 cookie 中的 `ngaPassportCid`. + +- 喜马拉雅 + + - `XIMALAYA_TOKEN`: 对应 cookie 中的 `1&_token`,获取方式:1. 登陆喜马拉雅网页版 2. 查找名称为`1&_token`的`cookie`,其内容即为`XIMALAYA_TOKEN`的值(即在`cookie` 中查找 `1&_token=***;`,并设置 `XIMALAYA_TOKEN = ***`) diff --git a/docs/multimedia.md b/docs/multimedia.md index f2d128e227..258200e441 100644 --- a/docs/multimedia.md +++ b/docs/multimedia.md @@ -675,12 +675,12 @@ pageClass: routes ### 专辑 - + ::: warning 注意 专辑 id 是跟在**分类拼音**后的那个 id, 不要输成某集的 id 了 -**付费内容不可收听,但可使用非播客软件 (例如 Inoreader) 获取更新** +**付费内容需要登陆才能收听,详情见部署页面的配置模块** 目前支持泛用型播客订阅的[输出格式](https://docs.rsshub.app/#输出格式)中标明的格式只有 rss 支持,也就是说你**只能使用**以下类型的链接来订阅播客: diff --git a/lib/config.js b/lib/config.js index 5b875cc0cc..3c8d13b03e 100644 --- a/lib/config.js +++ b/lib/config.js @@ -163,6 +163,9 @@ const calculateValue = () => { newrank: { cookie: envs.NEWRANK_COOKIE, }, + ximalaya: { + token: envs.XIMALAYA_TOKEN, + }, }; }; calculateValue(); diff --git a/lib/routes/ximalaya/album.js b/lib/routes/ximalaya/album.js index 1630041f14..861fe19f68 100644 --- a/lib/routes/ximalaya/album.js +++ b/lib/routes/ximalaya/album.js @@ -1,6 +1,8 @@ const got = require('@/utils/got'); +const { getUrl, getRandom16 } = require('./utils'); const baseUrl = 'http://www.ximalaya.com'; const got_ins = got.extend({}); +const config = require('@/config').value; // Find category from: https://help.apple.com/itc/podcasts_connect/?lang=en#/itc9267a2f12 const categoryDict = { @@ -14,9 +16,6 @@ const categoryDict = { module.exports = async (ctx) => { const id = ctx.params.id; // 专辑id - if (id === '31879246') { - throw 'Forbidden'; - } const isAll = ctx.params.all ? true : false; const pageSize = isAll ? 200 : 30; const AlbumInfoApi = `https://www.ximalaya.com/revision/album?albumId=${id}`; // 专辑数据的API @@ -34,8 +33,9 @@ module.exports = async (ctx) => { const album_intro = albuminfo.richIntro; // 专辑介绍 const album_desc = author_intro + '
' + album_intro; const albumUrl = '/' + classify + '/' + id + '/'; // 某分类的链接 - const ispaid = albuminfo.isPaid; // 是否需要付费 const isAsc = AlbumInfoResponse.data.data.tracksInfo.sort === 0; + const token = config.ximalaya.token; + const RandomToken = getRandom16(8) + '-' + getRandom16(4) + '-' + getRandom16(4) + '-' + getRandom16(4) + '-' + getRandom16(12); const TrackInfoApi = `http://mobile.ximalaya.com/mobile/v1/album/track/?albumId=${id}&isAsc=${!isAsc}&pageSize=${pageSize}&pageId=`; const TrackInfoResponse = await got_ins.get(TrackInfoApi + 1); @@ -54,6 +54,42 @@ module.exports = async (ctx) => { } } + await Promise.all( + playList.map(async (item) => { + const link = baseUrl + albumUrl + item.trackId; + const TrackRichInfoApi = `https://mobile.ximalaya.com/mobile-track/richIntro?trackId=${item.trackId}`; + const TrackRichInfoResponse = await got({ + method: 'get', + url: TrackRichInfoApi, + }); + item.desc = TrackRichInfoResponse.data.richIntro; + if (!item.desc) { + item.desc = '请在网页查看声音简介:' + `${link}`; + } + }) + ); + + if (token) { + await Promise.all( + playList.map(async (item) => { + const TrackPayInfoApi = `https://mpay.ximalaya.com/mobile/track/pay/${item.trackId}/?device=pc`; + const TrackPayInfoResponse = await got({ + method: 'get', + url: TrackPayInfoApi, + headers: { + 'user-agent': 'ting_6.7.9(GM1900,Android29)', + cookie: `1&_device=android&${RandomToken}&6.7.9;1&_token=${token}`, + }, + }); + if (TrackPayInfoResponse.data.ep) { + item.playPathAacv224 = getUrl(TrackPayInfoResponse.data); + } else if (TrackPayInfoResponse.data.msg) { + item.desc += '
' + TrackPayInfoResponse.data.msg; + } + }) + ); + } + const resultItems = await Promise.all( playList.map(async (item) => { const title = item.title; @@ -63,11 +99,10 @@ module.exports = async (ctx) => { const pubDate = new Date(item.createdAt).toUTCString(); const enclosure_length = item.duration; // 时间长度:单位(秒) const enclosure_url = item.playPathAacv224; + let desc = item.desc; - let desc = '请在网页查看声音简介:' + `${link}`; - - if (ispaid) { - desc = ' [该内容需付费,请打开网页收听] ' + '
' + desc; + if (!enclosure_url) { + desc = ' [该内容需付费,请打开网页收听] ' + `
${link}` + desc; } const resultItem = { diff --git a/lib/routes/ximalaya/utils.js b/lib/routes/ximalaya/utils.js new file mode 100644 index 0000000000..ec3b8642af --- /dev/null +++ b/lib/routes/ximalaya/utils.js @@ -0,0 +1,256 @@ +const crypto = require('crypto'); +const getParams = (ep) => { + const a1 = 'xkt3a41psizxrh9l'; + const a = [ + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + 62, + -1, + -1, + -1, + 63, + 52, + 53, + 54, + 55, + 56, + 57, + 58, + 59, + 60, + 61, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + -1, + -1, + -1, + -1, + -1, + -1, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 42, + 43, + 44, + 45, + 46, + 47, + 48, + 49, + 50, + 51, + -1, + -1, + -1, + -1, + -1, + ]; + let o = ep.length; + let r = 0; + let n, e; + const a2 = []; + while (r < o) { + e = a[255 & ep[r].charCodeAt()]; + r += 1; + while (r < o && -1 === e) { + e = a[255 & ep[r].charCodeAt()]; + r += 1; + } + + if (-1 === e) { + break; + } + n = a[255 & ep[r].charCodeAt()]; + r += 1; + while (r < o && -1 === n) { + n = a[255 & ep[r].charCodeAt()]; + r += 1; + } + if (-1 === n) { + break; + } + a2.push((e << 2) | ((48 & n) >> 4)); + e = 255 & ep[r].charCodeAt(); + r += 1; + if (61 === e) { + break; + } + e = a[e]; + while (r < o && -1 === e) { + e = 255 & ep[r].charCodeAt(); + if (61 === e) { + break; + } + e = a[e]; + } + if (-1 === e) { + break; + } + a2.push(((15 & n) << 4) | ((60 & e) >> 2)); + n = 255 & ep[r].charCodeAt(); + r += 1; + if (61 === n) { + break; + } + n = a[n]; + while (r < o && -1 === n) { + n = 255 & ep[r].charCodeAt(); + if (61 === n) { + break; + } + n = a[n]; + } + if (-1 === n) { + break; + } + a2.push(((3 & e) << 6) | n); + } + + const r1 = Array.from(Array(256), (v, i) => i); + + let i = ''; + o = 0; + for (let a = 0; a < 256; a++) { + o = (o + r1[a] + a1[a % a1.length].charCodeAt()) % 256; + [r1[a], r1[o]] = [r1[o], r1[a]]; + } + + let a3 = 0; + o = 0; + for (let u = 0; u < a2.length; u++) { + a3 = (a3 + 1) % 256; + o = (o + r1[a3]) % 256; + [r1[a3], r1[o]] = [r1[o], r1[a3]]; + i += String.fromCharCode(a2[u] ^ r1[(r1[a3] + r1[o]) % 256]); + } + i = i.split('-'); + return { + sign: i[1], + buy_key: i[0], + token: i[2], + timestamp: i[3], + }; +}; + +const getPath = (seed, fileId) => { + let t = String.raw`abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/\:._-1234567890`; + let cgStr = ''; + const l = t.length; + let o; + const idKey = fileId.split('*'); + for (let i = 0; i < l; i++) { + seed = (211 * seed + 30031) % 65536; + o = parseInt((seed * t.length) / 65536); + cgStr += t[o]; + t = t.split(t[o]).join(''); + } + + const url = idKey.map((id) => cgStr[id]).join(''); + return url; +}; + +const getUrl = (r) => { + const params = getParams(r.ep); + const paramsArray = []; + params.duration = r.duration; + Object.keys(params).forEach((key) => params[key] && paramsArray.push(`${key}=${params[key]}`)); + const url = 'https://audiopay.cos.xmcdn.com/download/' + r.apiVersion + '/' + getPath(r.seed, r.fileId) + '?' + paramsArray.join('&'); + return url; +}; + +const getRandom16 = (len) => + crypto + .randomBytes(Math.ceil(len / 2)) + .toString('hex') + .slice(0, len); + +module.exports = { + getUrl, + getRandom16, +};