新增虾米音源

pull/225/head
lyswhut 2020-04-23 12:50:46 +08:00
parent 7bd4002f09
commit ccf362db41
40 changed files with 931 additions and 14 deletions

View File

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

View File

@ -5,6 +5,7 @@
- 新增托盘设置,默认关闭,可到设置开启,感谢@LasyIsLazy提交的PR
- 新增打开酷狗源用户歌单
- 新增使用协议
- 新增虾米音源
### 优化

View File

@ -36,4 +36,7 @@ module.exports = {
fontSize: '18px',
},
],
navigationUrlWhiteList: [
/^https:\/\/www\.xiami\.com/,
],
}

View File

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

View File

@ -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

View File

@ -12,3 +12,6 @@ require('./getCacheSize')
require('./setIgnoreMouseEvent')
require('./getEnvParams')
require('./tray')
require('./updateSetting')
require('./xm_verify')

View File

@ -0,0 +1,7 @@
const { mainOn } = require('../../common/ipc')
mainOn('updateAppSetting', (event, setting) => {
if (!setting) return
global.appSetting = setting
})

View File

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

View File

@ -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")
</template>
<script>
@ -42,6 +44,9 @@ export default {
apiSource: 'test',
proxy: {},
isShowPact: false,
xm: {
isShowVerify: false,
},
},
updateTimeout: null,
envParams: {
@ -63,6 +68,7 @@ export default {
created() {
this.saveSetting = throttle(n => {
window.electronStore_config.set('setting', n)
rendererSend('updateAppSetting', n)
})
this.saveDefaultList = throttle(n => {
window.electronStore_list.set('defaultList', n)
@ -286,6 +292,9 @@ export default {
})
}
},
handleXMVerifyModalClose() {
music.xm.closeVerifyModal()
},
},
beforeDestroy() {
this.clearUpdateTimeout()

View File

@ -434,14 +434,14 @@ export default {
switch (songInfo.source) {
case 'wy':
case 'tx':
// case 'kg':
return '128k'
// case 'kg':
}
let type = '128k'
if (highQuality && songInfo._types['320k']) type = '320k'
return type
},
setUrl(targetSong, isRefresh) {
setUrl(targetSong, isRefresh, isRetryed = false) {
let type = this.getPlayType(this.setting.player.highQuality, targetSong)
this.musicInfo.url = targetSong.typeUrl[type]
this.status = this.$t('core.player.geting_url')
@ -450,6 +450,7 @@ export default {
this.audio.src = this.musicInfo.url = targetSong.typeUrl[type]
}).catch(err => {
if (err.message == requestMsg.cancelRequest) return
if (!isRetryed) return this.setUrl(targetSong, isRefresh, true)
this.status = err.message
this.addDelayNextTimeout()
return Promise.reject(err)

View File

@ -41,6 +41,7 @@ export default {
switch (type) {
case 'flac':
case 'ape':
case 'wav':
return this.$t('material.download_modal.lossless')
case '320k':
return this.$t('material.download_modal.high_quality')
@ -54,6 +55,8 @@ export default {
case 'wy':
case 'tx':
return type == '128k'
case 'xm':
return type == '128k' || type == '320k'
default:
return true

View File

@ -8,7 +8,7 @@ material-modal(:show="show" :bg-close="bgClose" @close="handleClose")
material-btn(:class="$style.btn" @click="handleClick('128k')") {{$t('material.download_multiple_modal.normal')}} - 128K
material-btn(:class="$style.btn" @click="handleClick('320k')") {{$t('material.download_multiple_modal.high_quality')}} - 320K
//- material-btn(:class="$style.btn" @click="handleClick('ape')") - APE
material-btn(:class="$style.btn" @click="handleClick('flac')") {{$t('material.download_multiple_modal.lossless')}} - FLAC
material-btn(:class="$style.btn" @click="handleClick('flac')") {{$t('material.download_multiple_modal.lossless')}} - FLAC/WAV
</template>
<script>

View File

@ -134,7 +134,7 @@ export default {
border-radius: 5px;
box-shadow: 0 0 3px rgba(0, 0, 0, .3);
overflow: hidden;
max-height: 70%;
max-height: 80%;
max-width: 70%;
position: relative;
display: flex;

View File

@ -22,7 +22,7 @@ div(:class="$style.songList")
material-checkbox(:id="index.toString()" v-model="selectdList" @change="handleChangeSelect" :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')}}
td.break(style="width: 20%;")
span.select {{item.singer}}

View File

@ -112,6 +112,7 @@ export default {
cursor: pointer;
padding: 0 10px;
font-size: 12px;
min-width: 56px;
// color: @color-btn;
outline: none;
transition: background-color @transition-theme;

View File

@ -0,0 +1,50 @@
<template lang="pug">
material-modal(:show="show" :bg-close="bgClose" @close="handleClose")
main.ignore-to-rem(:class="$style.main")
h2 {{$t('material.xm_verify_modal.title')}}
</template>
<script>
export default {
props: {
show: {
type: Boolean,
default: false,
},
bgClose: {
type: Boolean,
default: true,
},
},
methods: {
handleClose() {
this.$emit('close')
},
},
}
</script>
<style lang="less" module>
@import '../../assets/styles/layout.less';
.main {
background: #fff !important;
&:global(.ignore-to-rem) {
padding: 15px;
width: 360px;
height: 330px;
h2 {
font-size: 16px;
}
}
h2 {
font-size: 13px;
color: @color-theme_2-font;
line-height: 1.3;
text-align: center;
}
}
</style>

View File

@ -1,5 +1,5 @@
{
"btn_tip": "腾讯、网易音源仅支持下载128k音质",
"btn_tip": "腾讯、网易音源仅支持下载128k音质\n虾米音源不支持下载无损音质",
"lossless": "无损音质",
"high_quality": "高品音质",
"normal": "普通音质"

View File

@ -0,0 +1,3 @@
{
"title": "虾米音乐校验"
}

View File

@ -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": "聚合大会"

View File

@ -1,5 +1,5 @@
{
"btn_tip": "騰訊、網易音源僅支持下載128k音質",
"btn_tip": "騰訊、網易音源僅支持下載128k音質\n蝦米音源不支持下載無損音質",
"lossless": "無損音質",
"high_quality": "高品音質",
"normal": "普通音質"

View File

@ -0,0 +1,3 @@
{
"title": "蝦米音樂校驗"
}

View File

@ -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": "聚合大會"
}

View File

@ -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"

View File

@ -0,0 +1,3 @@
{
"title": "Xiami music verify"
}

View File

@ -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"

View File

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

View File

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

View File

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

View File

@ -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) {

View File

@ -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

View File

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

View File

@ -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

View File

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

View File

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

View File

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

View File

@ -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

View File

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

View File

@ -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

View File

@ -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%;")

View File

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