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()