From ccf362db41c73264e26e4e918c188901366a5aae Mon Sep 17 00:00:00 2001 From: lyswhut Date: Thu, 23 Apr 2020 12:50:46 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E8=99=BE=E7=B1=B3=E9=9F=B3?= =?UTF-8?q?=E6=BA=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- postcss.config.js | 2 +- publish/changeLog.md | 1 + src/common/config.js | 3 + src/main/events/index.js | 2 +- src/main/index.js | 17 +- src/main/rendererEvents/index.js | 3 + src/main/rendererEvents/updateSetting.js | 7 + src/main/rendererEvents/xm_verify.js | 43 ++++ src/renderer/App.vue | 9 + src/renderer/components/core/Player.vue | 5 +- .../components/material/DownloadModal.vue | 3 + .../material/DownloadMultipleModal.vue | 2 +- src/renderer/components/material/Modal.vue | 2 +- src/renderer/components/material/SongList.vue | 2 +- src/renderer/components/material/Tab.vue | 1 + .../components/material/xmVerifyModal.vue | 50 ++++ .../lang/cns/material/download_modal.json | 2 +- .../lang/cns/material/xm_verify_modal.json | 3 + src/renderer/lang/cns/store/state.json | 2 + .../lang/cnt/material/download_modal.json | 2 +- .../lang/cnt/material/xm_verify_modal.json | 3 + src/renderer/lang/cnt/store/state.json | 2 + .../lang/en/material/download_modal.json | 2 +- .../lang/en/material/xm_verify_modal.json | 3 + src/renderer/lang/en/store/state.json | 2 + src/renderer/store/modules/download.js | 2 +- src/renderer/utils/music/api-source.js | 4 + src/renderer/utils/music/index.js | 6 + src/renderer/utils/music/utils.js | 4 +- src/renderer/utils/music/xm/api-test.js | 20 ++ src/renderer/utils/music/xm/hotSearch.js | 19 ++ src/renderer/utils/music/xm/index.js | 31 +++ src/renderer/utils/music/xm/leaderboard.js | 147 ++++++++++++ src/renderer/utils/music/xm/lyric.js | 80 +++++++ src/renderer/utils/music/xm/musicSearch.js | 114 +++++++++ src/renderer/utils/music/xm/songList.js | 219 ++++++++++++++++++ src/renderer/utils/music/xm/util.js | 119 ++++++++++ src/renderer/views/Leaderboard.vue | 2 + src/renderer/views/Search.vue | 2 +- src/renderer/views/SongList.vue | 3 + 40 files changed, 931 insertions(+), 14 deletions(-) create mode 100644 src/main/rendererEvents/updateSetting.js create mode 100644 src/main/rendererEvents/xm_verify.js create mode 100644 src/renderer/components/material/xmVerifyModal.vue create mode 100644 src/renderer/lang/cns/material/xm_verify_modal.json create mode 100644 src/renderer/lang/cnt/material/xm_verify_modal.json create mode 100644 src/renderer/lang/en/material/xm_verify_modal.json create mode 100644 src/renderer/utils/music/xm/api-test.js create mode 100644 src/renderer/utils/music/xm/hotSearch.js create mode 100644 src/renderer/utils/music/xm/index.js create mode 100644 src/renderer/utils/music/xm/leaderboard.js create mode 100644 src/renderer/utils/music/xm/lyric.js create mode 100644 src/renderer/utils/music/xm/musicSearch.js create mode 100644 src/renderer/utils/music/xm/songList.js create mode 100644 src/renderer/utils/music/xm/util.js diff --git a/postcss.config.js b/postcss.config.js index 50a10650..456a2009 100644 --- a/postcss.config.js +++ b/postcss.config.js @@ -17,7 +17,7 @@ module.exports = { 'top', 'left', 'bottom', 'right', 'border-radius', ], - selectorBlackList: ['html'], + selectorBlackList: ['html', 'ignore-to-rem'], replace: true, mediaQuery: false, minPixelValue: 0, diff --git a/publish/changeLog.md b/publish/changeLog.md index acff5343..5527ef9d 100644 --- a/publish/changeLog.md +++ b/publish/changeLog.md @@ -5,6 +5,7 @@ - 新增托盘设置,默认关闭,可到设置开启,感谢@LasyIsLazy提交的PR - 新增打开酷狗源用户歌单 - 新增使用协议 +- 新增虾米音源 ### 优化 diff --git a/src/common/config.js b/src/common/config.js index cb00130e..126009bb 100644 --- a/src/common/config.js +++ b/src/common/config.js @@ -36,4 +36,7 @@ module.exports = { fontSize: '18px', }, ], + navigationUrlWhiteList: [ + /^https:\/\/www\.xiami\.com/, + ], } diff --git a/src/main/events/index.js b/src/main/events/index.js index 15015415..1185fa7c 100644 --- a/src/main/events/index.js +++ b/src/main/events/index.js @@ -2,4 +2,4 @@ global.lx_event = {} const Tray = require('./tray') -if (!global.lx_event.setting) global.lx_event.tray = new Tray() +if (!global.lx_event.tray) global.lx_event.tray = new Tray() diff --git a/src/main/index.js b/src/main/index.js index 58a83eb5..149c94c8 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -21,11 +21,13 @@ app.on('second-instance', (event, argv, cwd) => { }) const isDev = global.isDev = process.env.NODE_ENV !== 'production' +const { navigationUrlWhiteList } = require('../common/config') app.on('web-contents-created', (event, contents) => { contents.on('will-navigate', (event, navigationUrl) => { if (isDev) return console.log('navigation to url:', navigationUrl) - event.preventDefault() + if (!navigationUrlWhiteList.some(url => url.test(navigationUrl))) return event.preventDefault() + console.log('navigation to url:', navigationUrl) }) contents.on('new-window', async(event, navigationUrl) => { event.preventDefault() @@ -33,6 +35,19 @@ app.on('web-contents-created', (event, contents) => { console.log(navigationUrl) await shell.openExternal(navigationUrl) }) + contents.on('will-attach-webview', (event, webPreferences, params) => { + // Strip away preload scripts if unused or verify their location is legitimate + delete webPreferences.preload + delete webPreferences.preloadURL + + // Disable Node.js integration + webPreferences.nodeIntegration = false + + // Verify URL being loaded + if (!navigationUrlWhiteList.some(url => url.test(params.src))) { + event.preventDefault() + } + }) }) // https://github.com/electron/electron/issues/22691 diff --git a/src/main/rendererEvents/index.js b/src/main/rendererEvents/index.js index a555a5bd..b290ac41 100644 --- a/src/main/rendererEvents/index.js +++ b/src/main/rendererEvents/index.js @@ -12,3 +12,6 @@ require('./getCacheSize') require('./setIgnoreMouseEvent') require('./getEnvParams') require('./tray') +require('./updateSetting') + +require('./xm_verify') diff --git a/src/main/rendererEvents/updateSetting.js b/src/main/rendererEvents/updateSetting.js new file mode 100644 index 00000000..4fecf8d1 --- /dev/null +++ b/src/main/rendererEvents/updateSetting.js @@ -0,0 +1,7 @@ +const { mainOn } = require('../../common/ipc') + + +mainOn('updateAppSetting', (event, setting) => { + if (!setting) return + global.appSetting = setting +}) diff --git a/src/main/rendererEvents/xm_verify.js b/src/main/rendererEvents/xm_verify.js new file mode 100644 index 00000000..b7692a8b --- /dev/null +++ b/src/main/rendererEvents/xm_verify.js @@ -0,0 +1,43 @@ +const { BrowserView } = require('electron') +const { mainHandle } = require('../../common/ipc') +const { getWindowSizeInfo } = require('../utils') + +let view + +const closeView = async() => { + if (!view) return + // await view.webContents.session.clearCache() + if (global.mainWindow) global.mainWindow.removeBrowserView(view) + await view.webContents.session.clearStorageData() + view.destroy() + view = null +} + +mainHandle('xm_verify_open', (event, url) => new Promise((resolve, reject) => { + if (!global.mainWindow) return reject(new Error('mainwindow is undefined')) + if (view) view.destroy() + + view = new BrowserView() + view.webContents.on('did-finish-load', () => { + if (/punish\?/.test(view.webContents.getURL())) return + let ses = view.webContents.session + ses.cookies.get({ name: 'x5sec' }) + .then(async([x5sec]) => { + await closeView() + if (!x5sec) return reject(new Error('get x5sec failed')) + resolve(x5sec.value) + }).catch(async err => { + await closeView() + reject(err) + }) + }) + global.mainWindow.setBrowserView(view) + const windowSizeInfo = getWindowSizeInfo(global.appSetting) + view.setBounds({ x: (windowSizeInfo.width - 360) / 2, y: ((windowSizeInfo.height - 320 + 52) / 2), width: 360, height: 320 }) + view.webContents.loadURL(url) +})) + +mainHandle('xm_verify_close', async() => { + await closeView() +}) + diff --git a/src/renderer/App.vue b/src/renderer/App.vue index 32f84a7e..9cc4d5f5 100644 --- a/src/renderer/App.vue +++ b/src/renderer/App.vue @@ -8,6 +8,7 @@ core-icons material-pact-modal(v-show="!setting.isAgreePact || globalObj.isShowPact") material-version-modal(v-show="version.showModal") + material-xm-verify-modal(v-show="globalObj.xm.isShowVerify" :show="globalObj.xm.isShowVerify" :bg-close="false" @close="handleXMVerifyModalClose") #container(v-else :class="theme") core-aside#left #right @@ -17,6 +18,7 @@ core-icons material-pact-modal(v-show="!setting.isAgreePact || globalObj.isShowPact") material-version-modal(v-show="version.showModal") + material-xm-verify-modal(v-show="globalObj.xm.isShowVerify" :show="globalObj.xm.isShowVerify" :bg-close="false" @close="handleXMVerifyModalClose") + + + diff --git a/src/renderer/lang/cns/material/download_modal.json b/src/renderer/lang/cns/material/download_modal.json index acb09bbb..3afdbef9 100644 --- a/src/renderer/lang/cns/material/download_modal.json +++ b/src/renderer/lang/cns/material/download_modal.json @@ -1,5 +1,5 @@ { - "btn_tip": "腾讯、网易音源仅支持下载128k音质", + "btn_tip": "腾讯、网易音源仅支持下载128k音质\n虾米音源不支持下载无损音质", "lossless": "无损音质", "high_quality": "高品音质", "normal": "普通音质" diff --git a/src/renderer/lang/cns/material/xm_verify_modal.json b/src/renderer/lang/cns/material/xm_verify_modal.json new file mode 100644 index 00000000..82003b0e --- /dev/null +++ b/src/renderer/lang/cns/material/xm_verify_modal.json @@ -0,0 +1,3 @@ +{ + "title": "虾米音乐校验" +} diff --git a/src/renderer/lang/cns/store/state.json b/src/renderer/lang/cns/store/state.json index 05bf9ff8..310b0074 100644 --- a/src/renderer/lang/cns/store/state.json +++ b/src/renderer/lang/cns/store/state.json @@ -15,6 +15,7 @@ "source_tx": "企鹅音乐", "source_wy": "网易音乐", "source_mg": "咪咕音乐", + "source_xm": "虾米音乐", "source_bd": "百度音乐", "source_all": "聚合搜索", @@ -24,6 +25,7 @@ "source_alias_tx": "小秋音乐", "source_alias_wy": "小芸音乐", "source_alias_mg": "小蜜音乐", + "source_alias_xm": "小霞音乐", "source_alias_bd": "小杜音乐", "source_alias_all": "聚合大会" diff --git a/src/renderer/lang/cnt/material/download_modal.json b/src/renderer/lang/cnt/material/download_modal.json index cd873c2e..0931c525 100644 --- a/src/renderer/lang/cnt/material/download_modal.json +++ b/src/renderer/lang/cnt/material/download_modal.json @@ -1,5 +1,5 @@ { - "btn_tip": "騰訊、網易音源僅支持下載128k音質", + "btn_tip": "騰訊、網易、音源僅支持下載128k音質\n蝦米音源不支持下載無損音質", "lossless": "無損音質", "high_quality": "高品音質", "normal": "普通音質" diff --git a/src/renderer/lang/cnt/material/xm_verify_modal.json b/src/renderer/lang/cnt/material/xm_verify_modal.json new file mode 100644 index 00000000..1bfd5ceb --- /dev/null +++ b/src/renderer/lang/cnt/material/xm_verify_modal.json @@ -0,0 +1,3 @@ +{ + "title": "蝦米音樂校驗" +} diff --git a/src/renderer/lang/cnt/store/state.json b/src/renderer/lang/cnt/store/state.json index 4e6f68c7..49fbab2d 100644 --- a/src/renderer/lang/cnt/store/state.json +++ b/src/renderer/lang/cnt/store/state.json @@ -14,6 +14,7 @@ "source_tx": "企鵝音樂", "source_wy": "網易音樂", "source_mg": "咪咕音樂", + "source_xm": "蝦米音樂", "source_bd": "百度音樂", "source_all": "聚合搜索", "source_alias_kw": "小蝸音樂", @@ -22,5 +23,6 @@ "source_alias_wy": "小芸音樂", "source_alias_mg": "小蜜音樂", "source_alias_bd": "小杜音樂", + "source_alias_xm": "小霞音樂", "source_alias_all": "聚合大會" } diff --git a/src/renderer/lang/en/material/download_modal.json b/src/renderer/lang/en/material/download_modal.json index 364d0268..dbded1ba 100644 --- a/src/renderer/lang/en/material/download_modal.json +++ b/src/renderer/lang/en/material/download_modal.json @@ -1,5 +1,5 @@ { - "btn_tip": "Tencent and NetEase sources only support 128k audio quality", + "btn_tip": "Tencent and NetEase only support 128k audio quality\nXiami sources does not support 128k audio quality", "lossless": "Lossless", "high_quality": "High Quality", "normal": "Normal" diff --git a/src/renderer/lang/en/material/xm_verify_modal.json b/src/renderer/lang/en/material/xm_verify_modal.json new file mode 100644 index 00000000..d19f82aa --- /dev/null +++ b/src/renderer/lang/en/material/xm_verify_modal.json @@ -0,0 +1,3 @@ +{ + "title": "Xiami music verify" +} diff --git a/src/renderer/lang/en/store/state.json b/src/renderer/lang/en/store/state.json index 93538104..9dc31705 100644 --- a/src/renderer/lang/en/store/state.json +++ b/src/renderer/lang/en/store/state.json @@ -15,6 +15,7 @@ "source_tx": "Tencent", "source_wy": "Netease", "source_mg": "Migu", + "source_xm": "Xiami", "source_bd": "Baidu", "source_all": "Aggregated", @@ -23,6 +24,7 @@ "source_alias_tx": "TX Music", "source_alias_wy": "WY Music", "source_alias_mg": "MG Music", + "source_alias_xm": "XM Music", "source_alias_bd": "BD Music", "source_alias_all": "Aggregated" diff --git a/src/renderer/store/modules/download.js b/src/renderer/store/modules/download.js index bb99d801..f54723c3 100644 --- a/src/renderer/store/modules/download.js +++ b/src/renderer/store/modules/download.js @@ -123,7 +123,7 @@ const downloadLyric = (downloadInfo, filePath) => { ? Promise.resolve(downloadInfo.musicInfo.lrc) : music[downloadInfo.musicInfo.source].getLyric(downloadInfo.musicInfo).promise promise.then(lrc => { - if (lrc) saveLrc(filePath.replace(/(mp3|flac|ape)$/, 'lrc'), lrc) + if (lrc) saveLrc(filePath.replace(/(mp3|flac|ape|wav)$/, 'lrc'), lrc) }) } diff --git a/src/renderer/utils/music/api-source.js b/src/renderer/utils/music/api-source.js index e1163037..71cc84fb 100644 --- a/src/renderer/utils/music/api-source.js +++ b/src/renderer/utils/music/api-source.js @@ -5,6 +5,7 @@ import kg_api_test from './kg/api-test' import wy_api_test from './wy/api-test' import bd_api_test from './bd/api-test' import mg_api_test from './mg/api-test' +import xm_api_test from './xm/api-test' // import kw_api_internal from './kw/api-internal' // import tx_api_internal from './tx/api-internal' // import kg_api_internal from './kg/api-internal' @@ -18,6 +19,7 @@ const apis = { wy_api_test, bd_api_test, mg_api_test, + xm_api_test, // kw_api_internal, // tx_api_internal, // kg_api_internal, @@ -50,6 +52,8 @@ export default source => { return getAPI('bd') case 'mg': return getAPI('mg') + case 'xm': + return getAPI('xm') default: return getAPI('kw') } diff --git a/src/renderer/utils/music/index.js b/src/renderer/utils/music/index.js index 375488c7..e8733624 100644 --- a/src/renderer/utils/music/index.js +++ b/src/renderer/utils/music/index.js @@ -4,6 +4,7 @@ import tx from './tx' import wy from './wy' import mg from './mg' import bd from './bd' +import xm from './xm' const sources = { sources: [ { @@ -26,6 +27,10 @@ const sources = { name: '咪咕音乐', id: 'mg', }, + { + name: '虾米音乐', + id: 'xm', + }, { name: '百度音乐', id: 'bd', @@ -37,6 +42,7 @@ const sources = { wy, mg, bd, + xm, } export default { ...sources, diff --git a/src/renderer/utils/music/utils.js b/src/renderer/utils/music/utils.js index 04f47a0f..6bba9104 100644 --- a/src/renderer/utils/music/utils.js +++ b/src/renderer/utils/music/utils.js @@ -6,13 +6,15 @@ import crypto from 'crypto' * @param {*} type */ -const types = ['flac', 'ape', '320k', '192k', '128k'] +const types = ['flac', 'wav', 'ape', '320k', '192k', '128k'] export const getMusicType = (info, type) => { switch (info.source) { // case 'kg': case 'wy': case 'tx': return '128k' + case 'xm': + if (type == 'wav') type = '320k' } const rangeType = types.slice(types.indexOf(type)) for (const type of rangeType) { diff --git a/src/renderer/utils/music/xm/api-test.js b/src/renderer/utils/music/xm/api-test.js new file mode 100644 index 00000000..c9881164 --- /dev/null +++ b/src/renderer/utils/music/xm/api-test.js @@ -0,0 +1,20 @@ +import { httpFetch } from '../../request' +import { requestMsg } from '../../message' +import { headers, timeout } from '../options' + +const api_test = { + getMusicUrl(songInfo, type) { + const requestObj = httpFetch(`http://ts.tempmusic.tk/url/xm/${songInfo.songmid}/${type}`, { + method: 'get', + timeout, + headers, + family: 4, + }) + requestObj.promise = requestObj.promise.then(({ body }) => { + return body.code === 0 ? Promise.resolve({ type, url: body.data }) : Promise.reject(new Error(requestMsg.fail)) + }) + return requestObj + }, +} + +export default api_test diff --git a/src/renderer/utils/music/xm/hotSearch.js b/src/renderer/utils/music/xm/hotSearch.js new file mode 100644 index 00000000..3dbd67c0 --- /dev/null +++ b/src/renderer/utils/music/xm/hotSearch.js @@ -0,0 +1,19 @@ +import { xmRequest } from './util' + +export default { + _requestObj: null, + async getList(retryNum = 0) { + if (this._requestObj) this._requestObj.cancelHttp() + if (retryNum > 2) return Promise.reject(new Error('try max num')) + + const _requestObj = xmRequest('/api/search/getHotSearchWords') + const { body, statusCode } = await _requestObj.promise + // console.log(body) + if (statusCode != 200 || body.code !== 'SUCCESS') return this.getList(++retryNum) + // // console.log(body, statusCode) + return { source: 'xm', list: this.filterList(body.result.data.hotWords) } + }, + filterList(rawList) { + return rawList.map(item => item.word) + }, +} diff --git a/src/renderer/utils/music/xm/index.js b/src/renderer/utils/music/xm/index.js new file mode 100644 index 00000000..5a3a3ec6 --- /dev/null +++ b/src/renderer/utils/music/xm/index.js @@ -0,0 +1,31 @@ +import api_source from '../api-source' +import leaderboard from './leaderboard' +import songList from './songList' +import musicSearch from './musicSearch' +// import pic from './pic' +import lyric from './lyric' +import hotSearch from './hotSearch' +import { closeVerifyModal } from './util' + +const xm = { + songList, + musicSearch, + leaderboard, + hotSearch, + closeVerifyModal, + getMusicUrl(songInfo, type) { + return api_source('xm').getMusicUrl(songInfo, type) + }, + getLyric(songInfo) { + return lyric.getLyric(songInfo) + }, + getPic(songInfo) { + return Promise.reject(new Error('fail')) + // return pic.getPic(songInfo) + }, + // init() { + // getToken() + // }, +} + +export default xm diff --git a/src/renderer/utils/music/xm/leaderboard.js b/src/renderer/utils/music/xm/leaderboard.js new file mode 100644 index 00000000..2a9fcdd2 --- /dev/null +++ b/src/renderer/utils/music/xm/leaderboard.js @@ -0,0 +1,147 @@ +import { xmRequest } from './util' +import { formatPlayTime, sizeFormate } from '../../index' +// import jshtmlencode from 'js-htmlencode' + +export default { + limit: 200, + list: [ + { + id: 'xmrgb', + name: '热歌榜', + bangid: '103', + }, + { + id: 'xmxgb', + name: '新歌榜', + bangid: '102', + }, + { + id: 'xmrcb', + name: '原创榜', + bangid: '104', + }, + { + id: 'xmdyb', + name: '抖音榜', + bangid: '332', + }, + { + id: 'xmkgb', + name: 'K歌榜', + bangid: '306', + }, + { + id: 'xmfxb', + name: '分享榜', + bangid: '307', + }, + { + id: 'xmrdtlb', + name: '讨论榜', + bangid: '331', + }, + { + id: 'xmgdslb', + name: '歌单榜', + bangid: '305', + }, + { + id: 'xmpjrgb', + name: '趴间榜', + bangid: '327', + }, + { + id: 'xmysysb', + name: '影视榜', + bangid: '324', + }, + ], + requestObj: null, + getData(id) { + if (this.requestObj) this.requestObj.cancelHttp() + this.requestObj = xmRequest('/api/billboard/getBillboardDetail', { billboardId: id }) + return this.requestObj.promise + }, + getSinger(singers) { + let arr = [] + singers.forEach(singer => { + arr.push(singer.artistName) + }) + return arr.join('、') + }, + filterData(rawList) { + // console.log(rawList) + let ids = new Set() + const list = [] + rawList.forEach(songData => { + if (!songData) return + if (ids.has(songData.songId)) return + ids.add(songData.songId) + + const types = [] + const _types = {} + let size = null + for (const item of songData.purviewRoleVOs) { + if (!item.filesize) continue + size = sizeFormate(item.filesize) + switch (item.quality) { + case 's': + types.push({ type: 'wav', size }) + _types.wav = { + size, + } + break + case 'h': + types.push({ type: '320k', size }) + _types['320k'] = { + size, + } + break + case 'l': + types.push({ type: '128k', size }) + _types['128k'] = { + size, + } + break + } + } + types.reverse() + + list.push({ + singer: this.getSinger(songData.singerVOs), + name: songData.songName, + albumName: songData.albumName, + albumId: songData.albumId, + source: 'xm', + interval: formatPlayTime(parseInt(songData.length / 1000)), + songmid: songData.songId, + img: songData.albumLogo || songData.albumLogoS, + lrc: null, + lrcUrl: songData.lyricInfo && songData.lyricInfo.lyricFile, + types, + _types, + typeUrl: {}, + }) + }) + + return list + }, + getList(id, page, retryNum = 0) { + if (++retryNum > 3) return Promise.reject(new Error('try max num')) + let type = this.list.find(s => s.id === id) + if (!type) return Promise.reject() + return this.getData(type.bangid).then(({ statusCode, body }) => { + if (statusCode !== 200 || body.code !== 'SUCCESS') return this.getList(id, page, retryNum) + // console.log(body) + const list = this.filterData(body.result.data.billboard.songs) + + return { + total: parseInt(body.result.data.billboard.attributeMap.item_size), + list, + limit: this.limit, + page, + source: 'xm', + } + }) + }, +} diff --git a/src/renderer/utils/music/xm/lyric.js b/src/renderer/utils/music/xm/lyric.js new file mode 100644 index 00000000..4f83c57f --- /dev/null +++ b/src/renderer/utils/music/xm/lyric.js @@ -0,0 +1,80 @@ +import { httpGet, httpFetch } from '../../request' +import { xmRequest } from './util' + +export default { + failTime: 0, + expireTime: 60 * 1000 * 1000, + getLyricFile_1(url, retryNum = 0) { + if (retryNum > 5) return Promise.reject('歌词获取失败') + let requestObj = httpFetch(url) + requestObj.promise = requestObj.promise.then(({ body, statusCode }) => { + if (statusCode !== 200) { + let tryRequestObj = this.getLyric(url, ++retryNum) + requestObj.cancelHttp = tryRequestObj.cancelHttp.bind(tryRequestObj) + return tryRequestObj.promise + } + return body + }) + return requestObj + }, + getLyricFile_2(url, retryNum = 0) { + if (retryNum > 5) return Promise.reject('歌词获取失败') + return new Promise((resolve, reject) => { + httpGet(url, { + headers: { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36', + referer: 'https://www.xiami.com', + }, + }, function(err, resp, body) { + if (err || resp.statusCode !== 200) return this.getLyricFile(url, ++retryNum).then(resolve).catch(reject) + return resolve(body) + }) + }) + }, + getLyricUrl_1(songInfo, retryNum = 0) { + if (retryNum > 2) return Promise.reject('歌词获取失败') + let requestObj = xmRequest('/api/lyric/getSongLyrics', { songId: songInfo.songmid }) + requestObj.promise = requestObj.promise.then(({ statusCode, body }) => { + if (statusCode !== 200) { + let tryRequestObj = this.getLyricUrl_1(songInfo, ++retryNum) + requestObj.cancelHttp = tryRequestObj.cancelHttp.bind(tryRequestObj) + return tryRequestObj.promise + } + if (body.code !== 'SUCCESS') { + this.failTime = Date.now() + let tryRequestObj = this.getLyricUrl_2(songInfo) + requestObj.cancelHttp = tryRequestObj.cancelHttp.bind(tryRequestObj) + return tryRequestObj.promise + } + if (!body.result.data.lyrics.length) return Promise.reject(new Error('未找到歌词')) + let lrc = body.result.data.lyrics.find(lyric => /\.lrc$/.test(lyric.lyricUrl)) + return lrc ? lrc.content : Promise.reject(new Error('未找到歌词')) + }) + return requestObj + }, + getLyricUrl_2(songInfo, retryNum = 0) { + if (retryNum > 2) return Promise.reject('歌词获取失败') + // https://github.com/listen1/listen1_chrome_extension/blob/2587e627d23a85e490628acc0b3c9b534bc8323d/js/provider/xiami.js#L149 + let requestObj = httpFetch(`https://emumo.xiami.com/song/playlist/id/${songInfo.songmid}/object_name/default/object_id/0/cat/json`, { + headers: { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36', + referer: 'https://www.xiami.com', + }, + }) + requestObj.promise = requestObj.promise.then(({ statusCode, body }) => { + if (statusCode !== 200 || !body.status) { + let tryRequestObj = this.getLyricUrl_2(songInfo, ++retryNum) + requestObj.cancelHttp = tryRequestObj.cancelHttp.bind(tryRequestObj) + return tryRequestObj.promise + } + let url = body.data.trackList[0].lyric_url + if (!url) return Promise.reject(new Error('未找到歌词')) + return this.getLyricFile_2(/^http:/.test(url) ? url : ('http:' + url)) + }) + return requestObj + }, + getLyric(songInfo) { + if (songInfo.lrcUrl && /\.lrc$/.test(songInfo.lrcUrl)) return this.getLyricFile_1(songInfo.lrcUrl) + return Date.now() - this.failTime > this.expireTime ? this.getLyricUrl_1(songInfo) : this.getLyricUrl_2(songInfo) + }, +} diff --git a/src/renderer/utils/music/xm/musicSearch.js b/src/renderer/utils/music/xm/musicSearch.js new file mode 100644 index 00000000..33311d58 --- /dev/null +++ b/src/renderer/utils/music/xm/musicSearch.js @@ -0,0 +1,114 @@ +// import '../../polyfill/array.find' +// import jshtmlencode from 'js-htmlencode' +import { xmRequest } from './util' +import { formatPlayTime, sizeFormate } from '../../index' +// import { debug } from '../../utils/env' +// import { formatSinger } from './util' +// "cdcb72dc3eba41cb5bc4267f09183119_xmMain_/api/list/collect_{"pagingVO":{"page":1,"pageSize":60},"dataType":"system"}" +let searchRequest +export default { + limit: 30, + total: 0, + page: 0, + allPage: 1, + musicSearch(str, page) { + if (searchRequest && searchRequest.cancelHttp) searchRequest.cancelHttp() + searchRequest = xmRequest('/api/search/searchSongs', { + key: str, + pagingVO: { + page: page, + pageSize: this.limit, + }, + }) + return searchRequest.promise.then(({ body }) => body) + }, + getSinger(singers) { + let arr = [] + singers.forEach(singer => { + arr.push(singer.artistName) + }) + return arr.join('、') + }, + handleResult(rawData) { + // console.log(rawData) + let ids = new Set() + const list = [] + rawData.forEach(songData => { + if (!songData) return + if (ids.has(songData.songId)) return + ids.add(songData.songId) + + const types = [] + const _types = {} + let size = null + for (const item of songData.purviewRoleVOs) { + if (!item.filesize) continue + size = sizeFormate(item.filesize) + switch (item.quality) { + case 's': + types.push({ type: 'wav', size }) + _types.wav = { + size, + } + break + case 'h': + types.push({ type: '320k', size }) + _types['320k'] = { + size, + } + break + case 'l': + types.push({ type: '128k', size }) + _types['128k'] = { + size, + } + break + } + } + types.reverse() + + list.push({ + singer: this.getSinger(songData.singerVOs), + name: songData.songName, + albumName: songData.albumName, + albumId: songData.albumId, + source: 'xm', + interval: formatPlayTime(parseInt(songData.length / 1000)), + songmid: songData.songId, + img: songData.albumLogo || songData.albumLogoS, + lrc: null, + lrcUrl: songData.lyricInfo && songData.lyricInfo.lyricFile, + types, + _types, + typeUrl: {}, + }) + }) + return list + }, + search(str, page = 1, { limit } = {}, retryNum = 0) { + if (++retryNum > 3) return Promise.reject(new Error('try max num')) + if (limit != null) this.limit = limit + // http://newlyric.kuwo.cn/newlyric.lrc?62355680 + return this.musicSearch(str, page).then(result => { + // console.log(result) + if (!result) return this.search(str, page, { limit }, retryNum) + if (result.code !== 'SUCCESS') return this.search(str, page, { limit }, retryNum) + // const songResultData = result.data || { songs: [], total: 0 } + + let list = this.handleResult(result.result.data.songs) + if (list == null) return this.search(str, page, { limit }, retryNum) + + this.total = parseInt(result.result.data.pagingVO.count) + this.page = page + this.allPage = Math.ceil(this.total / this.limit) + + return Promise.resolve({ + list, + allPage: this.allPage, + limit: this.limit, + total: this.total, + source: 'xm', + }) + }).catch(() => this.search(str, page, { limit }, retryNum)) + }, +} diff --git a/src/renderer/utils/music/xm/songList.js b/src/renderer/utils/music/xm/songList.js new file mode 100644 index 00000000..d93c4b36 --- /dev/null +++ b/src/renderer/utils/music/xm/songList.js @@ -0,0 +1,219 @@ +import { xmRequest } from './util' +import { sizeFormate, formatPlayTime } from '../../index' + +export default { + _requestObj_tags: null, + _requestObj_list: null, + _requestObj_listDetail: null, + limit_list: 36, + limit_song: 100000, + successCode: 'SUCCESS', + sortList: [ + { + name: '推荐', + id: 'system', + }, + { + name: '精选', + id: 'recommend', + }, + { + name: '最热', + id: 'hot', + }, + { + name: '最新', + id: 'new', + }, + ], + regExps: { + // https://www.xiami.com/collect/1138092824?action=play + listDetailLink: /^.+\/collect\/(\d+)(?:\s\(.*|\?.*|&.*$|#.*$|$)/, + }, + tagsUrl: '/api/collect/getRecommendTags', + songListUrl: '/api/list/collect', + songListDetailUrl: '/api/collect/initialize', + getSongListData(sortId, tagId, page) { + if (tagId == null) { + return { pagingVO: { page, pageSize: this.limit_list }, dataType: sortId } + } + switch (sortId) { + case 'system': + case 'recommend': + sortId = 'hot' + } + return { pagingVO: { page, pageSize: this.limit_list }, dataType: sortId, key: tagId } + }, + + /** + * 格式化播放数量 + * @param {*} num + */ + formatPlayCount(num) { + if (num > 100000000) return parseInt(num / 10000000) / 10 + '亿' + if (num > 10000) return parseInt(num / 1000) / 10 + '万' + return num + }, + getSinger(singers) { + let arr = [] + singers.forEach(singer => { + arr.push(singer.artistName) + }) + return arr.join('、') + }, + + getListDetail(id, page, tryNum = 0) { // 获取歌曲列表内的音乐 + if (this._requestObj_listDetail) this._requestObj_listDetail.cancelHttp() + if (tryNum > 2) return Promise.reject(new Error('try max num')) + + if ((/[?&:/]/.test(id))) id = id.replace(this.regExps.listDetailLink, '$1') + + this._requestObj_listDetail = xmRequest('/api/collect/getCollectStaticUrl', { listId: id }) + return this._requestObj_listDetail.promise.then(({ body }) => { + if (body.code !== this.successCode) return this.getListDetail(id, page, ++tryNum) + this._requestObj_listDetail = xmRequest(body.result.data.data.data.url) + return this._requestObj_listDetail.promise.then(({ body }) => { + if (!body.status) return this.getListDetail(id, page, ++tryNum) + // console.log(JSON.stringify(body)) + return { + list: this.filterListDetail(body.resultObj.songs), + page, + limit: this.limit_song, + total: body.resultObj.songCount, + source: 'xm', + info: { + name: body.resultObj.collectName, + img: body.resultObj.collectLogo, + desc: body.resultObj.description, + author: body.resultObj.userName, + play_count: this.formatPlayCount(body.resultObj.playCount), + }, + } + }) + }) + }, + filterListDetail(rawList) { + // console.log(rawList) + let ids = new Set() + const list = [] + rawList.forEach(songData => { + if (!songData) return + if (ids.has(songData.songId)) return + ids.add(songData.songId) + + const types = [] + const _types = {} + let size = null + for (const item of songData.purviewRoleVOs) { + if (!item.filesize) continue + size = sizeFormate(item.filesize) + switch (item.quality) { + case 's': + types.push({ type: 'wav', size }) + _types.wav = { + size, + } + break + case 'h': + types.push({ type: '320k', size }) + _types['320k'] = { + size, + } + break + case 'l': + types.push({ type: '128k', size }) + _types['128k'] = { + size, + } + break + } + } + types.reverse() + + list.push({ + singer: this.getSinger(songData.singerVOs), + name: songData.songName, + albumName: songData.albumName, + albumId: songData.albumId, + source: 'xm', + interval: formatPlayTime(parseInt(songData.length / 1000)), + songmid: songData.songId, + img: songData.albumLogo || songData.albumLogoS, + lrc: null, + lrcUrl: songData.lyricInfo && songData.lyricInfo.lyricFile, + types, + _types, + typeUrl: {}, + }) + }) + return list + }, + + // 获取列表数据 + getList(sortId, tagId, page, tryNum = 0) { + if (this._requestObj_list) this._requestObj_list.cancelHttp() + if (tryNum > 2) return Promise.reject(new Error('try max num')) + this._requestObj_list = xmRequest(this.songListUrl, this.getSongListData(sortId, tagId, page)) + return this._requestObj_list.promise.then(({ body }) => { + if (body.code !== this.successCode) return this.getList(sortId, tagId, page, ++tryNum) + return { + list: this.filterList(body.result.data.collects), + total: body.result.data.pagingVO.count, + page, + limit: body.result.data.pagingVO.pageSize, + source: 'xm', + } + }) + }, + filterList(rawData) { + return rawData.map(item => ({ + play_count: this.formatPlayCount(item.playCount), + id: item.listId, + author: item.userName, + name: item.collectName, + time: null, + img: item.collectLogo, + grade: null, + desc: null, + source: 'xm', + })) + }, + + // 获取标签 + getTag(tryNum = 0) { + if (this._requestObj_tags) this._requestObj_tags.cancelHttp() + if (tryNum > 2) return Promise.reject(new Error('try max num')) + this._requestObj_tags = xmRequest(this.tagsUrl, { recommend: 1 }) + return this._requestObj_tags.promise.then(({ body }) => { + if (body.code !== this.successCode) return this.getTag(++tryNum) + return this.filterTagInfo(body.result.data.recommendTags) + }) + }, + filterTagInfo(rawList) { + return { + hotTag: rawList[0].items.map(item => ({ + id: item.name, + name: item.name, + source: 'xm', + })), + tags: rawList.slice(1).map(item => ({ + name: item.title, + list: item.items.map(tag => ({ + parent_id: item.title, + parent_name: item.title, + id: tag.name, + name: tag.name, + source: 'xm', + })), + })), + source: 'xm', + } + }, + getTags() { + return this.getTag() + }, +} + +// getList +// getTags +// getListDetail diff --git a/src/renderer/utils/music/xm/util.js b/src/renderer/utils/music/xm/util.js new file mode 100644 index 00000000..d6fe066b --- /dev/null +++ b/src/renderer/utils/music/xm/util.js @@ -0,0 +1,119 @@ +import { httpGet, httpFetch } from '../../request' +import { toMD5 } from '../../index' +// import crateIsg from './isg' +import { rendererInvoke } from '../../../../common/ipc' + +if (!window.xm_token) { + let data = window.localStorage.getItem('xm_token') + window.xm_token = data ? JSON.parse(data) : { + cookies: {}, + cookie: null, + token: null, + isGetingToken: false, + } + window.xm_token.isGetingToken = false +} + +export const formatSinger = rawData => rawData.replace(/&/g, '、') + +const matchToken = headers => { + let cookies = {} + let token + for (const item of headers['set-cookie']) { + const [key, value] = item.substring(0, item.indexOf(';')).split('=') + cookies[key] = value + if (key == 'xm_sg_tk') token = value.substring(0, value.indexOf('_')) + } + // console.log(cookies) + return { token, cookies } +} + +const wait = time => new Promise(resolve => setTimeout(() => resolve(), time)) + +const createToken = (token, path, params) => toMD5(`${token}_xmMain_${path}_${params}`) + +const handleSaveToken = ({ token, cookies }) => { + Object.assign(window.xm_token.cookies, cookies) + // window.xm_token.cookies.isg = crateIsg() + window.xm_token.cookie = Object.keys(window.xm_token.cookies).map(k => `${k}=${window.xm_token.cookies[k]};`).join(' ') + if (token) window.xm_token.token = token + + window.localStorage.setItem('xm_token', JSON.stringify(window.xm_token)) +} + +export const getToken = (path, params) => new Promise((resolve, reject) => { + if (window.xm_token.isGetingToken) return wait(1000).then(() => getToken(path, params).then(data => resolve(data))) + if (window.xm_token.token) return resolve({ token: createToken(window.xm_token.token, path, params), cookie: window.xm_token.cookie }) + window.xm_token.isGetingToken = true + httpGet('https://www.xiami.com/', (err, resp) => { + window.xm_token.isGetingToken = false + if (err) return reject(err) + if (resp.statusCode != 200) return reject(new Error('获取失败')) + + handleSaveToken(matchToken(resp.headers)) + + resolve({ token: createToken(window.xm_token.token, path, params), cookie: window.xm_token.cookie }) + }) +}) + +const baseUrl = 'https://www.xiami.com' +export const xmRequest = (path, params = '') => { + let query = params + if (params != '') { + params = JSON.stringify(params) + query = '&_q=' + encodeURIComponent(params) + } + let requestObj = { + isInited: false, + isCancelled: false, + cancelHttp() { + if (!this.isInited) this.isCancelled = true + this.requestObj.cancelHttp() + }, + } + + requestObj.promise = getToken(path, params).then(data => { + // console.log(data) + if (requestObj.isCancelled) return Promise.reject('取消请求') + let url = path + if (!/^http/.test(path)) url = baseUrl + path + let s = `_s=${data.token}${query}` + url += (url.includes('?') ? '&' : '?') + s + requestObj.requestObj = httpFetch(url, { + headers: { + Referer: 'https://www.xiami.com/', + 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1', + cookie: data.cookie, + }, + }) + return requestObj.requestObj.promise.then(resp => { + // console.log(resp.body) + if (resp.statusCode != 200) { + // console.log(resp.headers) + window.xm_token.token = null + return Promise.reject(new Error('获取失败')) + } + if (resp.body.code !== 'SUCCESS' && resp.body.rgv587_flag == 'sm') { + window.globalObj.xm.isShowVerify = true + return wait(300).then(() => { + return rendererInvoke('xm_verify_open', 'https:' + resp.body.url).then(x5sec => { + handleSaveToken({ cookies: { x5sec } }) + // console.log(x5sec) + window.globalObj.xm.isShowVerify = false + return Promise.reject(new Error('获取失败')) + }) + }) + } + if (resp.headers['set-cookie']) handleSaveToken(matchToken(resp.headers)) + + return Promise.resolve(resp) + }) + }) + return requestObj +} + +export const closeVerifyModal = async() => { + if (!window.globalObj.xm.isShowVerify) return + await rendererInvoke('xm_verify_close') + window.globalObj.xm.isShowVerify = false +} diff --git a/src/renderer/views/Leaderboard.vue b/src/renderer/views/Leaderboard.vue index addb1f7f..bc6cf52e 100644 --- a/src/renderer/views/Leaderboard.vue +++ b/src/renderer/views/Leaderboard.vue @@ -129,6 +129,8 @@ export default { case 'tx': case 'wy': type = '128k' + case 'xm': + if (type == 'flac') type = 'wav' } this.createDownloadMultiple({ list: [...this.selectdData], type }) this.isShowDownloadMultiple = false diff --git a/src/renderer/views/Search.vue b/src/renderer/views/Search.vue index 5dcf7dda..2826b6fc 100644 --- a/src/renderer/views/Search.vue +++ b/src/renderer/views/Search.vue @@ -24,7 +24,7 @@ material-checkbox(:id="index.toString()" v-model="selectdData" :value="item") td.break(style="width: 25%;") span.select {{item.name}} - span.badge.badge-theme-success(:class="$style.labelQuality" v-if="item._types.ape || item._types.flac") {{$t('material.song_list.lossless')}} + span.badge.badge-theme-success(:class="$style.labelQuality" v-if="item._types.ape || item._types.flac || item._types.wav") {{$t('material.song_list.lossless')}} span.badge.badge-theme-info(:class="$style.labelQuality" v-else-if="item._types['320k']") {{$t('material.song_list.high_quality')}} span(:class="$style.labelSource" v-if="searchSourceId == 'all'") {{item.source}} td.break(style="width: 20%;") diff --git a/src/renderer/views/SongList.vue b/src/renderer/views/SongList.vue index e2c92547..927b891c 100644 --- a/src/renderer/views/SongList.vue +++ b/src/renderer/views/SongList.vue @@ -100,6 +100,7 @@ export default { case 'tx': case 'mg': case 'kg': + case 'xm': list.push({ name: this.$t('view.song_list.open_list', { name: this.sourceInfo.sources.find(s => s.id == this.source).name }), id: 'importSongList', @@ -254,6 +255,8 @@ export default { case 'tx': case 'wy': type = '128k' + case 'xm': + if (type == 'flac') type = 'wav' } this.createDownloadMultiple({ list: this.filterList(this.selectdData), type }) this.resetSelect()