新增Scheme URL对音乐搜索的调用支持;新增Scheme URL以url传参的方式调用
parent
13ffcfbbcd
commit
2f56a7090a
20
FAQ.md
20
FAQ.md
|
@ -305,16 +305,34 @@ Windows 7 未开启 Aero 效果时桌面歌词会有问题,详情看上面的
|
||||||
|
|
||||||
- URL统一以`lxmusic://`开头
|
- URL统一以`lxmusic://`开头
|
||||||
- 此技术目前只支持 Windows、Mac系统
|
- 此技术目前只支持 Windows、Mac系统
|
||||||
- URL传参以经过URL编码的JSON数据传参,例:`lxmusic://music/play?data=xxxx`,其中`xxxx`为经过URL编码后的JSON数据
|
|
||||||
- 若无特别说明,源的可用值为:`kw/kg/tx/wy/mg`
|
- 若无特别说明,源的可用值为:`kw/kg/tx/wy/mg`
|
||||||
- 若无特别说明,音质的可用值为:`128k/320k/flac/flac32bit`
|
- 若无特别说明,音质的可用值为:`128k/320k/flac/flac32bit`
|
||||||
|
|
||||||
|
目前支持两种传参方式:
|
||||||
|
|
||||||
|
- 通过`data`传参,以经过URL编码的JSON数据传参,例:`lxmusic://music/play?data=xxxx`,其中`xxxx`为经过URL编码后的JSON数据,支持复杂的参数调用
|
||||||
|
- 通过`URL`传参,适用于简单传参的调用,不需要转成JSON格式,例:`lxmusic://music/search/xxxx`,但仍然需要对数据进行URL编码,只适应于简单参数调用(v1.18.0新增)
|
||||||
|
|
||||||
|
### `data`方式传参
|
||||||
|
|
||||||
|
以经过URL编码的JSON数据传参,例:`lxmusic://music/play?data=xxxx`,其中`xxxx`为经过URL编码后的JSON数据,JSON数据内容取决于下表的参数部分
|
||||||
|
|
||||||
| 描述 | URL | 参数
|
| 描述 | URL | 参数
|
||||||
| --- | --- | ---
|
| --- | --- | ---
|
||||||
| 打开歌单 | `songlist/open` | `source<String>`(源,必须)<br>`id<String/Number>`(歌单ID,可选)<br>`url<String>`(歌单URL,可选)其中ID与URL必需传一个
|
| 打开歌单 | `songlist/open` | `source<String>`(源,必须)<br>`id<String/Number>`(歌单ID,可选)<br>`url<String>`(歌单URL,可选)其中ID与URL必需传一个
|
||||||
| 播放歌单 | `songlist/play` | `source<String>`(源,必须)<br>`id<String/Number>`(歌单ID,可选)<br>`url<String>`(歌单URL,可选)其中`id`与`url`必需传一个<br>`index<Number>`(播放第几首歌,可选,从0开始)
|
| 播放歌单 | `songlist/play` | `source<String>`(源,必须)<br>`id<String/Number>`(歌单ID,可选)<br>`url<String>`(歌单URL,可选)其中`id`与`url`必需传一个<br>`index<Number>`(播放第几首歌,可选,从0开始)
|
||||||
|
| 搜索歌曲 | `music/search` | `keywords<String/Number>`(要搜索的内容,必须)<br>`source<String>`(源,可选)
|
||||||
| 播放歌曲 | `music/play` | `name<String>`(歌曲名,必传)<br>`singer<String>`(艺术家名,必传)<br>`source<String>`(源,必传)<br>`songmid<String/Number>`(歌曲ID,必传)<br>`img<String>`(歌曲图片链接,选传)<br>`albumId<String/Number>`(歌曲专辑ID,选传)<br>`interval<String>`(格式化后的歌曲时长,选传,例:`03:55`)<br>`albumName<String>`(歌曲专辑名称,选传)<br>`types<Object>`(歌曲可用音质数组,必传,<br>数组格式:`[{"type": "<音质>", size: "<格式化后的文件大小,选传>", hash: "<kg源必传>"}]`,<br>例:`[{"type": "128k", size: "3.56M"}, {"type": "320k", size: null}]`)<br><br>以下为平台特定参数:<br>`hash<String>`(歌曲hash,kg源必传)<br>`strMediaMid<String>`(歌曲strMediaMid,tx源必传)<br>`albumMid<String>`(歌曲albumMid,tx源专用,选传)<br>`copyrightId<String>`(歌曲copyrightId,mg源必传)<br>`lrcUrl<String>`(歌曲lrcUrl,mg源专用,选传)
|
| 播放歌曲 | `music/play` | `name<String>`(歌曲名,必传)<br>`singer<String>`(艺术家名,必传)<br>`source<String>`(源,必传)<br>`songmid<String/Number>`(歌曲ID,必传)<br>`img<String>`(歌曲图片链接,选传)<br>`albumId<String/Number>`(歌曲专辑ID,选传)<br>`interval<String>`(格式化后的歌曲时长,选传,例:`03:55`)<br>`albumName<String>`(歌曲专辑名称,选传)<br>`types<Object>`(歌曲可用音质数组,必传,<br>数组格式:`[{"type": "<音质>", size: "<格式化后的文件大小,选传>", hash: "<kg源必传>"}]`,<br>例:`[{"type": "128k", size: "3.56M"}, {"type": "320k", size: null}]`)<br><br>以下为平台特定参数:<br>`hash<String>`(歌曲hash,kg源必传)<br>`strMediaMid<String>`(歌曲strMediaMid,tx源必传)<br>`albumMid<String>`(歌曲albumMid,tx源专用,选传)<br>`copyrightId<String>`(歌曲copyrightId,mg源必传)<br>`lrcUrl<String>`(歌曲lrcUrl,mg源专用,选传)
|
||||||
|
|
||||||
|
### `URL`方式传参
|
||||||
|
|
||||||
|
由于URL传参只适用于简单传参场景,所以目前只支持以下功能的调用:
|
||||||
|
|
||||||
|
| 描述 | URL | 参数
|
||||||
|
| --- | --- | ---
|
||||||
|
| 搜索歌曲 | `music/search/{source}/{keywords}` | `source`(源,可选)<br>`keywords`(要搜索的内容,必须)<br>例:`music/search/kw/xxx`、`music/search/xxx`
|
||||||
|
| 打开歌单 | `songlist/open/{source}/{id/url}` | `source`(源,必须)<br>`id/url`(歌单ID或歌单URL,必须)<br>例:`songlist/open/kw/123456`
|
||||||
|
|
||||||
## 自定义源脚本编写说明
|
## 自定义源脚本编写说明
|
||||||
|
|
||||||
文件请使用UTF-8编码格式编写,脚本所用编程语言为JavaScript,可以使用ES6+语法,脚本与应用的交互是使用类似事件收发的方式进行,这是一个基本的脚本例子:
|
文件请使用UTF-8编码格式编写,脚本所用编程语言为JavaScript,可以使用ES6+语法,脚本与应用的交互是使用类似事件收发的方式进行,这是一个基本的脚本例子:
|
||||||
|
|
|
@ -3,6 +3,8 @@
|
||||||
- 新增“双击列表里的歌曲时自动切换到当前列表播放”设置,此功能仅对歌单、排行榜有效,默认关闭
|
- 新增“双击列表里的歌曲时自动切换到当前列表播放”设置,此功能仅对歌单、排行榜有效,默认关闭
|
||||||
- 新增打开收藏的在线列表的对应平台详情页功能,可以在我的列表-列表右键菜单中使用
|
- 新增打开收藏的在线列表的对应平台详情页功能,可以在我的列表-列表右键菜单中使用
|
||||||
- 新增定时暂停播放功能,由于此功能大多数人可能不常用,所以将其放在设置-基本设置中
|
- 新增定时暂停播放功能,由于此功能大多数人可能不常用,所以将其放在设置-基本设置中
|
||||||
|
- 新增Scheme URL对音乐搜索的调用支持,详情看常见问题-Scheme URL支持
|
||||||
|
- 新增Scheme URL以url传参的方式调用,详情看常见问题-Scheme URL支持
|
||||||
|
|
||||||
### 优化
|
### 优化
|
||||||
|
|
||||||
|
|
|
@ -1,281 +0,0 @@
|
||||||
import { useCommit, useAction, onBeforeUnmount, useRouter, useI18n, markRaw } from '@renderer/utils/vueTools'
|
|
||||||
import { base as eventBaseName } from '@renderer/event/names'
|
|
||||||
import { getEnvParams, clearEnvParamsDeeplink } from '@renderer/utils/tools'
|
|
||||||
import { decodeName } from '@renderer/utils'
|
|
||||||
// import { allList, defaultList, loveList, userLists } from '@renderer/core/share/list'
|
|
||||||
import { isShowPlayerDetail, setShowPlayerDetail, playMusicInfo } from '@renderer/core/share/player'
|
|
||||||
import usePlaySonglist from './compositions/usePlaySonglist'
|
|
||||||
import { dialog } from '@renderer/plugins/Dialog'
|
|
||||||
|
|
||||||
const useDialog = () => {
|
|
||||||
const { t } = useI18n()
|
|
||||||
const errorDialog = message => {
|
|
||||||
dialog({
|
|
||||||
message: `${t('deep_link__handle_error_tip', { message })}`,
|
|
||||||
confirmButtonText: t('ok'),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return errorDialog
|
|
||||||
}
|
|
||||||
|
|
||||||
const sources = ['kw', 'kg', 'tx', 'wy', 'mg']
|
|
||||||
const sourceVerify = source => {
|
|
||||||
if (!sources.includes(source)) throw new Error('Source no match')
|
|
||||||
}
|
|
||||||
|
|
||||||
const qualitys = ['128k', '320k', 'flac', 'flac32bit']
|
|
||||||
const qualityFilter = (source, types) => {
|
|
||||||
types = types.filter(({ type }) => qualitys.includes(type)).map(({ type, size, hash }) => {
|
|
||||||
if (size != null && typeof size != 'string') throw new Error(type + ' size type no match')
|
|
||||||
if (source == 'kg' && typeof hash != 'string') throw new Error(type + ' hash type no match')
|
|
||||||
return hash == null ? { type, size } : { type, size, hash }
|
|
||||||
})
|
|
||||||
if (!types.length) throw new Error('quality no match')
|
|
||||||
return types
|
|
||||||
}
|
|
||||||
|
|
||||||
const dataVerify = (rules, data) => {
|
|
||||||
const newData = {}
|
|
||||||
for (const rule of rules) {
|
|
||||||
const val = data[rule.key]
|
|
||||||
if (rule.required && val == null) throw new Error(rule.key + ' missing')
|
|
||||||
if (val != null) {
|
|
||||||
if (rule.types && !rule.types.includes(typeof val)) throw new Error(rule.key + ' type no match')
|
|
||||||
if (rule.max && String(val).length > rule.max) throw new Error(rule.key + ' max length no match')
|
|
||||||
if (rule.min && String(val).length > rule.min) throw new Error(rule.key + ' min length no match')
|
|
||||||
}
|
|
||||||
newData[rule.key] = val
|
|
||||||
}
|
|
||||||
return newData
|
|
||||||
}
|
|
||||||
|
|
||||||
export default () => {
|
|
||||||
// const setList = useCommit('list', 'setList')
|
|
||||||
// const listAdd = useCommit('list', 'listAdd')
|
|
||||||
// const listMove = useCommit('list', 'listMove')
|
|
||||||
// const listAddMultiple = useCommit('list', 'listAddMultiple')
|
|
||||||
// const listMoveMultiple = useCommit('list', 'listMoveMultiple')
|
|
||||||
// const listRemove = useCommit('list', 'listRemove')
|
|
||||||
// const listRemoveMultiple = useCommit('list', 'listRemoveMultiple')
|
|
||||||
// const listClear = useCommit('list', 'listClear')
|
|
||||||
// const updateMusicInfo = useCommit('list', 'updateMusicInfo')
|
|
||||||
// const createUserList = useCommit('list', 'createUserList')
|
|
||||||
// const removeUserList = useCommit('list', 'removeUserList')
|
|
||||||
// const setUserListName = useCommit('list', 'setUserListName')
|
|
||||||
// const setMusicPosition = useCommit('list', 'setMusicPosition')
|
|
||||||
// // const setSyncListData = useCommit('list', 'setSyncListData')
|
|
||||||
// const setUserListPosition = useCommit('list', 'setUserListPosition')
|
|
||||||
const router = useRouter()
|
|
||||||
const setTempPlayList = useCommit('player', 'setTempPlayList')
|
|
||||||
const playNext = useAction('player', 'playNext')
|
|
||||||
const playSongListDetail = usePlaySonglist()
|
|
||||||
let isInited = false
|
|
||||||
|
|
||||||
const showErrorDialog = useDialog()
|
|
||||||
|
|
||||||
const handleOpenSonglist = params => {
|
|
||||||
if (params.id) {
|
|
||||||
router.replace({
|
|
||||||
path: '/songList',
|
|
||||||
query: {
|
|
||||||
source: params.source,
|
|
||||||
id: params.id,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} else if (params.url) {
|
|
||||||
router.replace({
|
|
||||||
path: '/songList',
|
|
||||||
query: {
|
|
||||||
source: params.source,
|
|
||||||
url: params.url,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handlePlayMusic = _musicInfo => {
|
|
||||||
const musicInfo = {
|
|
||||||
..._musicInfo,
|
|
||||||
singer: decodeName(_musicInfo.singer),
|
|
||||||
name: decodeName(_musicInfo.name),
|
|
||||||
albumName: decodeName(_musicInfo.albumName),
|
|
||||||
otherSource: null,
|
|
||||||
_types: {},
|
|
||||||
typeUrl: {},
|
|
||||||
}
|
|
||||||
for (const type of musicInfo.types) {
|
|
||||||
musicInfo._types[type.type] = { size: type.size }
|
|
||||||
}
|
|
||||||
markRaw(musicInfo)
|
|
||||||
const isPlaying = !!playMusicInfo.musicInfo
|
|
||||||
setTempPlayList([{ listId: '__temp__', musicInfo, isTop: true }])
|
|
||||||
if (isPlaying) playNext()
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSonglist = (action, songlistInfo) => {
|
|
||||||
sourceVerify(songlistInfo.source)
|
|
||||||
switch (action) {
|
|
||||||
case 'open':
|
|
||||||
songlistInfo = dataVerify([
|
|
||||||
{ key: 'source', types: ['string'] },
|
|
||||||
{ key: 'id', types: ['string', 'number'], max: 64 },
|
|
||||||
{ key: 'url', types: ['string'], max: 500 },
|
|
||||||
], songlistInfo)
|
|
||||||
if (isShowPlayerDetail.value) setShowPlayerDetail(false)
|
|
||||||
handleOpenSonglist(songlistInfo)
|
|
||||||
break
|
|
||||||
case 'play':
|
|
||||||
songlistInfo = dataVerify([
|
|
||||||
{ key: 'source', types: ['string'] },
|
|
||||||
{ key: 'id', types: ['string', 'number'], max: 64 },
|
|
||||||
{ key: 'url', types: ['string'], max: 500 },
|
|
||||||
{ key: 'index', types: ['number'], max: 1000000 },
|
|
||||||
], songlistInfo)
|
|
||||||
playSongListDetail(songlistInfo.source, songlistInfo.id ?? songlistInfo.url, songlistInfo.index ?? 0)
|
|
||||||
break
|
|
||||||
default: throw new Error('Unknown action: ' + action)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleMusic = (action, musicInfo) => {
|
|
||||||
switch (musicInfo.source) {
|
|
||||||
case 'kw':
|
|
||||||
musicInfo = dataVerify([
|
|
||||||
{ key: 'name', types: ['string'], required: true, max: 200 },
|
|
||||||
{ key: 'singer', types: ['string'], required: true, max: 200 },
|
|
||||||
{ key: 'source', types: ['string'], required: true },
|
|
||||||
{ key: 'songmid', types: ['string', 'number'], max: 64, required: true },
|
|
||||||
{ key: 'img', types: ['string'], max: 1024 },
|
|
||||||
{ key: 'albumId', types: ['string', 'number'], max: 64 },
|
|
||||||
{ key: 'interval', types: ['string'], max: 64 },
|
|
||||||
{ key: 'albumName', types: ['string'], max: 64 },
|
|
||||||
{ key: 'types', types: ['object'], required: true },
|
|
||||||
], musicInfo)
|
|
||||||
break
|
|
||||||
case 'kg':
|
|
||||||
musicInfo = dataVerify([
|
|
||||||
{ key: 'name', types: ['string'], required: true, max: 200 },
|
|
||||||
{ key: 'singer', types: ['string'], required: true, max: 200 },
|
|
||||||
{ key: 'source', types: ['string'], required: true },
|
|
||||||
{ key: 'songmid', types: ['string', 'number'], max: 64, required: true },
|
|
||||||
{ key: 'img', types: ['string'], max: 1024 },
|
|
||||||
{ key: 'albumId', types: ['string', 'number'], max: 64 },
|
|
||||||
{ key: 'interval', types: ['string'], max: 64 },
|
|
||||||
{ key: '_interval', types: ['number'], max: 64 },
|
|
||||||
{ key: 'albumName', types: ['string'], max: 64 },
|
|
||||||
{ key: 'types', types: ['object'], required: true },
|
|
||||||
|
|
||||||
{ key: 'hash', types: ['string'], required: true, max: 64 },
|
|
||||||
], musicInfo)
|
|
||||||
break
|
|
||||||
case 'tx':
|
|
||||||
musicInfo = dataVerify([
|
|
||||||
{ key: 'name', types: ['string'], required: true, max: 200 },
|
|
||||||
{ key: 'singer', types: ['string'], required: true, max: 200 },
|
|
||||||
{ key: 'source', types: ['string'], required: true },
|
|
||||||
{ key: 'songmid', types: ['string', 'number'], max: 64, required: true },
|
|
||||||
{ key: 'img', types: ['string'], max: 1024 },
|
|
||||||
{ key: 'albumId', types: ['string', 'number'], max: 64 },
|
|
||||||
{ key: 'interval', types: ['string'], max: 64 },
|
|
||||||
{ key: 'albumName', types: ['string'], max: 64 },
|
|
||||||
{ key: 'types', types: ['object'], required: true },
|
|
||||||
|
|
||||||
{ key: 'strMediaMid', types: ['string'], required: true, max: 64 },
|
|
||||||
{ key: 'albumMid', types: ['string'], max: 64 },
|
|
||||||
], musicInfo)
|
|
||||||
break
|
|
||||||
case 'wy':
|
|
||||||
musicInfo = dataVerify([
|
|
||||||
{ key: 'name', types: ['string'], required: true, max: 200 },
|
|
||||||
{ key: 'singer', types: ['string'], required: true, max: 200 },
|
|
||||||
{ key: 'source', types: ['string'], required: true },
|
|
||||||
{ key: 'songmid', types: ['string', 'number'], max: 64, required: true },
|
|
||||||
{ key: 'img', types: ['string'], max: 1024 },
|
|
||||||
{ key: 'albumId', types: ['string', 'number'], max: 64 },
|
|
||||||
{ key: 'interval', types: ['string'], max: 64 },
|
|
||||||
{ key: 'albumName', types: ['string'], max: 64 },
|
|
||||||
{ key: 'types', types: ['object'], required: true },
|
|
||||||
], musicInfo)
|
|
||||||
break
|
|
||||||
case 'mg':
|
|
||||||
musicInfo = dataVerify([
|
|
||||||
{ key: 'name', types: ['string'], required: true, max: 200 },
|
|
||||||
{ key: 'singer', types: ['string'], required: true, max: 200 },
|
|
||||||
{ key: 'source', types: ['string'], required: true },
|
|
||||||
{ key: 'songmid', types: ['string', 'number'], max: 64, required: true },
|
|
||||||
{ key: 'img', types: ['string'], max: 1024 },
|
|
||||||
{ key: 'albumId', types: ['string', 'number'], max: 64 },
|
|
||||||
{ key: 'interval', types: ['string'], max: 64 },
|
|
||||||
{ key: 'albumName', types: ['string'], max: 64 },
|
|
||||||
{ key: 'types', types: ['object'], required: true },
|
|
||||||
|
|
||||||
{ key: 'copyrightId', types: ['string', 'number'], required: true, max: 64 },
|
|
||||||
{ key: 'lrcUrl', types: ['string'], max: 1024 },
|
|
||||||
], musicInfo)
|
|
||||||
break
|
|
||||||
default: throw new Error('Unknown action: ' + action)
|
|
||||||
}
|
|
||||||
musicInfo.types = qualityFilter(musicInfo.source, musicInfo.types)
|
|
||||||
switch (action) {
|
|
||||||
case 'play':
|
|
||||||
handlePlayMusic(musicInfo)
|
|
||||||
break
|
|
||||||
default: throw new Error('Unknown action: ' + action)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleLinkAction = link => {
|
|
||||||
// console.log(link)
|
|
||||||
const [url, search] = link.split('?')
|
|
||||||
const [type, action] = url.replace('lxmusic://', '').split('/')
|
|
||||||
const params = {}
|
|
||||||
for (const param of search.split('&')) {
|
|
||||||
const [key, value] = param.split('=')
|
|
||||||
params[key] = value
|
|
||||||
}
|
|
||||||
if (params.data) params.data = JSON.parse(decodeURIComponent(params.data))
|
|
||||||
console.log(params.data)
|
|
||||||
switch (type) {
|
|
||||||
case 'music':
|
|
||||||
handleMusic(action, params.data)
|
|
||||||
break
|
|
||||||
case 'songlist':
|
|
||||||
handleSonglist(action, params.data)
|
|
||||||
break
|
|
||||||
default: throw new Error('Unknown type: ' + type)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleFocus = () => {
|
|
||||||
if (!isInited) return
|
|
||||||
|
|
||||||
getEnvParams().then(envParams => {
|
|
||||||
if (!envParams.deeplink) return
|
|
||||||
clearEnvParamsDeeplink()
|
|
||||||
try {
|
|
||||||
handleLinkAction(envParams.deeplink)
|
|
||||||
} catch (err) {
|
|
||||||
showErrorDialog(err.message)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
window.eventHub.on(eventBaseName.focus, handleFocus)
|
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
window.eventHub.off(eventBaseName.focus, handleFocus)
|
|
||||||
})
|
|
||||||
|
|
||||||
return envParams => {
|
|
||||||
if (envParams.deeplink) {
|
|
||||||
clearEnvParamsDeeplink()
|
|
||||||
try {
|
|
||||||
handleLinkAction(envParams.deeplink)
|
|
||||||
} catch (err) {
|
|
||||||
showErrorDialog(err.message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
isInited = true
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,74 @@
|
||||||
|
import { onBeforeUnmount } from '@renderer/utils/vueTools'
|
||||||
|
import { base as eventBaseName } from '@renderer/event/names'
|
||||||
|
import { getEnvParams, clearEnvParamsDeeplink } from '@renderer/utils/tools'
|
||||||
|
|
||||||
|
import { useDialog } from './utils'
|
||||||
|
import useMusicAction from './useMusicAction'
|
||||||
|
import useSonglistAction from './useSonglistAction'
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
let isInited = false
|
||||||
|
|
||||||
|
const showErrorDialog = useDialog()
|
||||||
|
|
||||||
|
const handleMusicAction = useMusicAction()
|
||||||
|
const handleSonglistAction = useSonglistAction()
|
||||||
|
|
||||||
|
|
||||||
|
const handleLinkAction = link => {
|
||||||
|
// console.log(link)
|
||||||
|
const [url, search] = link.split('?')
|
||||||
|
const [type, action, ...paths] = url.replace('lxmusic://', '').split('/')
|
||||||
|
const params = {}
|
||||||
|
if (search) {
|
||||||
|
for (const param of search.split('&')) {
|
||||||
|
const [key, value] = param.split('=')
|
||||||
|
params[key] = value
|
||||||
|
}
|
||||||
|
if (params.data) params.data = JSON.parse(decodeURIComponent(params.data))
|
||||||
|
}
|
||||||
|
params.paths = paths.map(p => decodeURIComponent(p))
|
||||||
|
console.log(params)
|
||||||
|
switch (type) {
|
||||||
|
case 'music':
|
||||||
|
handleMusicAction(action, params)
|
||||||
|
break
|
||||||
|
case 'songlist':
|
||||||
|
handleSonglistAction(action, params)
|
||||||
|
break
|
||||||
|
default: throw new Error('Unknown type: ' + type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFocus = () => {
|
||||||
|
if (!isInited) return
|
||||||
|
|
||||||
|
getEnvParams().then(envParams => {
|
||||||
|
if (!envParams.deeplink) return
|
||||||
|
clearEnvParamsDeeplink()
|
||||||
|
try {
|
||||||
|
handleLinkAction(envParams.deeplink)
|
||||||
|
} catch (err) {
|
||||||
|
showErrorDialog(err.message)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
window.eventHub.on(eventBaseName.focus, handleFocus)
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
window.eventHub.off(eventBaseName.focus, handleFocus)
|
||||||
|
})
|
||||||
|
|
||||||
|
return envParams => {
|
||||||
|
if (envParams.deeplink) {
|
||||||
|
clearEnvParamsDeeplink()
|
||||||
|
try {
|
||||||
|
handleLinkAction(envParams.deeplink)
|
||||||
|
} catch (err) {
|
||||||
|
showErrorDialog(err.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
isInited = true
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,167 @@
|
||||||
|
import { useCommit, useAction, useRouter, markRaw } from '@renderer/utils/vueTools'
|
||||||
|
import { decodeName } from '@renderer/utils'
|
||||||
|
// import { allList, defaultList, loveList, userLists } from '@renderer/core/share/list'
|
||||||
|
import { playMusicInfo } from '@renderer/core/share/player'
|
||||||
|
|
||||||
|
import { dataVerify, qualityFilter, sources } from './utils'
|
||||||
|
|
||||||
|
const useSearchMusic = () => {
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
return ({ paths, data: params }) => {
|
||||||
|
let text
|
||||||
|
let source
|
||||||
|
if (params) {
|
||||||
|
text = dataVerify([
|
||||||
|
{ key: 'keywords', types: ['string', 'number'], max: 128, required: true },
|
||||||
|
], params).keywords
|
||||||
|
source = params.source
|
||||||
|
} else {
|
||||||
|
if (!paths.length) throw new Error('Keyword missing')
|
||||||
|
|
||||||
|
if (paths.length > 1) {
|
||||||
|
text = paths[1]
|
||||||
|
source = paths[0]
|
||||||
|
} else {
|
||||||
|
text = paths[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (text.length > 128) text = text.substring(0, 128)
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceList = [...sources, 'all']
|
||||||
|
source = sourceList.includes(source) ? source : null
|
||||||
|
router.replace({
|
||||||
|
path: '/search',
|
||||||
|
query: {
|
||||||
|
text,
|
||||||
|
source,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const usePlayMusic = () => {
|
||||||
|
const setTempPlayList = useCommit('player', 'setTempPlayList')
|
||||||
|
const playNext = useAction('player', 'playNext')
|
||||||
|
|
||||||
|
const filterInfoByPlayMusic = musicInfo => {
|
||||||
|
switch (musicInfo.source) {
|
||||||
|
case 'kw':
|
||||||
|
musicInfo = dataVerify([
|
||||||
|
{ key: 'name', types: ['string'], required: true, max: 200 },
|
||||||
|
{ key: 'singer', types: ['string'], required: true, max: 200 },
|
||||||
|
{ key: 'source', types: ['string'], required: true },
|
||||||
|
{ key: 'songmid', types: ['string', 'number'], max: 64, required: true },
|
||||||
|
{ key: 'img', types: ['string'], max: 1024 },
|
||||||
|
{ key: 'albumId', types: ['string', 'number'], max: 64 },
|
||||||
|
{ key: 'interval', types: ['string'], max: 64 },
|
||||||
|
{ key: 'albumName', types: ['string'], max: 64 },
|
||||||
|
{ key: 'types', types: ['object'], required: true },
|
||||||
|
], musicInfo)
|
||||||
|
break
|
||||||
|
case 'kg':
|
||||||
|
musicInfo = dataVerify([
|
||||||
|
{ key: 'name', types: ['string'], required: true, max: 200 },
|
||||||
|
{ key: 'singer', types: ['string'], required: true, max: 200 },
|
||||||
|
{ key: 'source', types: ['string'], required: true },
|
||||||
|
{ key: 'songmid', types: ['string', 'number'], max: 64, required: true },
|
||||||
|
{ key: 'img', types: ['string'], max: 1024 },
|
||||||
|
{ key: 'albumId', types: ['string', 'number'], max: 64 },
|
||||||
|
{ key: 'interval', types: ['string'], max: 64 },
|
||||||
|
{ key: '_interval', types: ['number'], max: 64 },
|
||||||
|
{ key: 'albumName', types: ['string'], max: 64 },
|
||||||
|
{ key: 'types', types: ['object'], required: true },
|
||||||
|
|
||||||
|
{ key: 'hash', types: ['string'], required: true, max: 64 },
|
||||||
|
], musicInfo)
|
||||||
|
break
|
||||||
|
case 'tx':
|
||||||
|
musicInfo = dataVerify([
|
||||||
|
{ key: 'name', types: ['string'], required: true, max: 200 },
|
||||||
|
{ key: 'singer', types: ['string'], required: true, max: 200 },
|
||||||
|
{ key: 'source', types: ['string'], required: true },
|
||||||
|
{ key: 'songmid', types: ['string', 'number'], max: 64, required: true },
|
||||||
|
{ key: 'img', types: ['string'], max: 1024 },
|
||||||
|
{ key: 'albumId', types: ['string', 'number'], max: 64 },
|
||||||
|
{ key: 'interval', types: ['string'], max: 64 },
|
||||||
|
{ key: 'albumName', types: ['string'], max: 64 },
|
||||||
|
{ key: 'types', types: ['object'], required: true },
|
||||||
|
|
||||||
|
{ key: 'strMediaMid', types: ['string'], required: true, max: 64 },
|
||||||
|
{ key: 'albumMid', types: ['string'], max: 64 },
|
||||||
|
], musicInfo)
|
||||||
|
break
|
||||||
|
case 'wy':
|
||||||
|
musicInfo = dataVerify([
|
||||||
|
{ key: 'name', types: ['string'], required: true, max: 200 },
|
||||||
|
{ key: 'singer', types: ['string'], required: true, max: 200 },
|
||||||
|
{ key: 'source', types: ['string'], required: true },
|
||||||
|
{ key: 'songmid', types: ['string', 'number'], max: 64, required: true },
|
||||||
|
{ key: 'img', types: ['string'], max: 1024 },
|
||||||
|
{ key: 'albumId', types: ['string', 'number'], max: 64 },
|
||||||
|
{ key: 'interval', types: ['string'], max: 64 },
|
||||||
|
{ key: 'albumName', types: ['string'], max: 64 },
|
||||||
|
{ key: 'types', types: ['object'], required: true },
|
||||||
|
], musicInfo)
|
||||||
|
break
|
||||||
|
case 'mg':
|
||||||
|
musicInfo = dataVerify([
|
||||||
|
{ key: 'name', types: ['string'], required: true, max: 200 },
|
||||||
|
{ key: 'singer', types: ['string'], required: true, max: 200 },
|
||||||
|
{ key: 'source', types: ['string'], required: true },
|
||||||
|
{ key: 'songmid', types: ['string', 'number'], max: 64, required: true },
|
||||||
|
{ key: 'img', types: ['string'], max: 1024 },
|
||||||
|
{ key: 'albumId', types: ['string', 'number'], max: 64 },
|
||||||
|
{ key: 'interval', types: ['string'], max: 64 },
|
||||||
|
{ key: 'albumName', types: ['string'], max: 64 },
|
||||||
|
{ key: 'types', types: ['object'], required: true },
|
||||||
|
|
||||||
|
{ key: 'copyrightId', types: ['string', 'number'], required: true, max: 64 },
|
||||||
|
{ key: 'lrcUrl', types: ['string'], max: 1024 },
|
||||||
|
], musicInfo)
|
||||||
|
break
|
||||||
|
default: throw new Error('Unknown source: ' + musicInfo.source)
|
||||||
|
}
|
||||||
|
musicInfo.types = qualityFilter(musicInfo.source, musicInfo.types)
|
||||||
|
return musicInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
return ({ data: _musicInfo }) => {
|
||||||
|
_musicInfo = filterInfoByPlayMusic(_musicInfo)
|
||||||
|
|
||||||
|
const musicInfo = {
|
||||||
|
..._musicInfo,
|
||||||
|
singer: decodeName(_musicInfo.singer),
|
||||||
|
name: decodeName(_musicInfo.name),
|
||||||
|
albumName: decodeName(_musicInfo.albumName),
|
||||||
|
otherSource: null,
|
||||||
|
_types: {},
|
||||||
|
typeUrl: {},
|
||||||
|
}
|
||||||
|
for (const type of musicInfo.types) {
|
||||||
|
musicInfo._types[type.type] = { size: type.size }
|
||||||
|
}
|
||||||
|
markRaw(musicInfo)
|
||||||
|
const isPlaying = !!playMusicInfo.musicInfo
|
||||||
|
setTempPlayList([{ listId: '__temp__', musicInfo, isTop: true }])
|
||||||
|
if (isPlaying) playNext()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
const handleSearchMusic = useSearchMusic()
|
||||||
|
const handlePlayMusic = usePlayMusic()
|
||||||
|
|
||||||
|
return (action, info) => {
|
||||||
|
switch (action) {
|
||||||
|
case 'search':
|
||||||
|
handleSearchMusic(info)
|
||||||
|
break
|
||||||
|
case 'play':
|
||||||
|
handlePlayMusic(info)
|
||||||
|
break
|
||||||
|
default: throw new Error('Unknown action: ' + action)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,102 @@
|
||||||
|
import { useRouter } from '@renderer/utils/vueTools'
|
||||||
|
import { isShowPlayerDetail, setShowPlayerDetail } from '@renderer/core/share/player'
|
||||||
|
import usePlaySonglist from '../compositions/usePlaySonglist'
|
||||||
|
|
||||||
|
import { dataVerify, sourceVerify } from './utils'
|
||||||
|
|
||||||
|
const useOpenSonglist = () => {
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const handleOpenSonglist = params => {
|
||||||
|
if (params.id) {
|
||||||
|
router.replace({
|
||||||
|
path: '/songList',
|
||||||
|
query: {
|
||||||
|
source: params.source,
|
||||||
|
id: params.id,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} else if (params.url) {
|
||||||
|
router.replace({
|
||||||
|
path: '/songList',
|
||||||
|
query: {
|
||||||
|
source: params.source,
|
||||||
|
url: params.url,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ({ paths, data }) => {
|
||||||
|
let songlistInfo = {
|
||||||
|
source: null,
|
||||||
|
id: null,
|
||||||
|
url: null,
|
||||||
|
}
|
||||||
|
if (!data) {
|
||||||
|
songlistInfo.source = paths[0]
|
||||||
|
songlistInfo.url = paths[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceVerify(songlistInfo.source)
|
||||||
|
|
||||||
|
songlistInfo = dataVerify([
|
||||||
|
{ key: 'source', types: ['string'] },
|
||||||
|
{ key: 'id', types: ['string', 'number'], max: 64 },
|
||||||
|
{ key: 'url', types: ['string'], max: 500 },
|
||||||
|
], songlistInfo)
|
||||||
|
|
||||||
|
if (!songlistInfo.id && !songlistInfo.url) throw new Error('id or url missing')
|
||||||
|
if (isShowPlayerDetail.value) setShowPlayerDetail(false)
|
||||||
|
handleOpenSonglist(songlistInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const usePlaySonglistDetail = () => {
|
||||||
|
const playSongListDetail = usePlaySonglist()
|
||||||
|
|
||||||
|
return ({ paths, data }) => {
|
||||||
|
let songlistInfo = {
|
||||||
|
source: null,
|
||||||
|
id: null,
|
||||||
|
url: null,
|
||||||
|
index: null,
|
||||||
|
}
|
||||||
|
if (!data) {
|
||||||
|
songlistInfo.source = paths[0]
|
||||||
|
songlistInfo.url = paths[1]
|
||||||
|
songlistInfo.index = paths[2]
|
||||||
|
if (songlistInfo.index != null) songlistInfo.index = parseInt(songlistInfo.index)
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceVerify(songlistInfo.source)
|
||||||
|
|
||||||
|
songlistInfo = dataVerify([
|
||||||
|
{ key: 'source', types: ['string'] },
|
||||||
|
{ key: 'id', types: ['string', 'number'], max: 64 },
|
||||||
|
{ key: 'url', types: ['string'], max: 500 },
|
||||||
|
{ key: 'index', types: ['number'], max: 1000000 },
|
||||||
|
], songlistInfo)
|
||||||
|
|
||||||
|
if (!songlistInfo.id && !songlistInfo.url) throw new Error('id or url missing')
|
||||||
|
|
||||||
|
playSongListDetail(songlistInfo.source, songlistInfo.id ?? songlistInfo.url, songlistInfo.index ?? 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
const handleOpenSonglist = useOpenSonglist()
|
||||||
|
const handlePlaySonglist = usePlaySonglistDetail()
|
||||||
|
|
||||||
|
|
||||||
|
return (action, info) => {
|
||||||
|
switch (action) {
|
||||||
|
case 'open':
|
||||||
|
handleOpenSonglist(info)
|
||||||
|
break
|
||||||
|
case 'play':
|
||||||
|
handlePlaySonglist(info)
|
||||||
|
break
|
||||||
|
default: throw new Error('Unknown action: ' + action)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
import { useI18n } from '@renderer/utils/vueTools'
|
||||||
|
import { dialog } from '@renderer/plugins/Dialog'
|
||||||
|
|
||||||
|
export const useDialog = () => {
|
||||||
|
const { t } = useI18n()
|
||||||
|
const errorDialog = message => {
|
||||||
|
dialog({
|
||||||
|
message: `${t('deep_link__handle_error_tip', { message })}`,
|
||||||
|
confirmButtonText: t('ok'),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return errorDialog
|
||||||
|
}
|
||||||
|
|
||||||
|
export const sources = ['kw', 'kg', 'tx', 'wy', 'mg']
|
||||||
|
export const sourceVerify = source => {
|
||||||
|
if (!sources.includes(source)) throw new Error('Source no match')
|
||||||
|
}
|
||||||
|
|
||||||
|
export const qualitys = ['128k', '320k', 'flac', 'flac32bit']
|
||||||
|
export const qualityFilter = (source, types) => {
|
||||||
|
types = types.filter(({ type }) => qualitys.includes(type)).map(({ type, size, hash }) => {
|
||||||
|
if (size != null && typeof size != 'string') throw new Error(type + ' size type no match')
|
||||||
|
if (source == 'kg' && typeof hash != 'string') throw new Error(type + ' hash type no match')
|
||||||
|
return hash == null ? { type, size } : { type, size, hash }
|
||||||
|
})
|
||||||
|
if (!types.length) throw new Error('quality no match')
|
||||||
|
return types
|
||||||
|
}
|
||||||
|
|
||||||
|
export const dataVerify = (rules, data) => {
|
||||||
|
const newData = {}
|
||||||
|
for (const rule of rules) {
|
||||||
|
const val = data[rule.key]
|
||||||
|
if (rule.required && val == null) throw new Error(rule.key + ' missing')
|
||||||
|
if (val != null) {
|
||||||
|
if (rule.types && !rule.types.includes(typeof val)) throw new Error(rule.key + ' type no match')
|
||||||
|
if (rule.max && String(val).length > rule.max) throw new Error(rule.key + ' max length no match')
|
||||||
|
if (rule.min && String(val).length > rule.min) throw new Error(rule.key + ' min length no match')
|
||||||
|
}
|
||||||
|
newData[rule.key] = val
|
||||||
|
}
|
||||||
|
return newData
|
||||||
|
}
|
|
@ -3,6 +3,7 @@ export default {
|
||||||
state.setting.themeId = val
|
state.setting.themeId = val
|
||||||
},
|
},
|
||||||
setSearchSource(state, { searchSource, tempSearchSource }) {
|
setSearchSource(state, { searchSource, tempSearchSource }) {
|
||||||
|
console.log(searchSource, tempSearchSource)
|
||||||
if (searchSource != null) state.setting.search.searchSource = searchSource
|
if (searchSource != null) state.setting.search.searchSource = searchSource
|
||||||
if (tempSearchSource != null) state.setting.search.tempSearchSource = tempSearchSource
|
if (tempSearchSource != null) state.setting.search.tempSearchSource = tempSearchSource
|
||||||
},
|
},
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
div(:class="$style.search")
|
div(:class="$style.search")
|
||||||
//- transition
|
//- transition
|
||||||
div(:class="$style.header")
|
div(:class="$style.header")
|
||||||
base-tab(:class="$style.tab" :list="sources" align="left" item-key="id" item-name="name" v-model="searchSourceId")
|
base-tab(:class="$style.tab" :list="sources" align="left" item-key="id" item-name="name" @change="handleSourceChange" v-model="searchSourceId")
|
||||||
div(:class="$style.main")
|
div(:class="$style.main")
|
||||||
div(:class="$style.list" v-show="isLoading || listInfo.list.length")
|
div(:class="$style.list" v-show="isLoading || listInfo.list.length")
|
||||||
div.thead(:class="$style.thead")
|
div.thead(:class="$style.thead")
|
||||||
|
@ -109,12 +109,28 @@ export default {
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
beforeRouteUpdate(to, from, next) {
|
beforeRouteUpdate(to, from) {
|
||||||
if (to.query.text === undefined) return
|
if (to.query.source && (this.sourceList[to.query.source] || to.query.source == 'all')) {
|
||||||
|
if (this.setting.search.searchSource != to.query.source) {
|
||||||
|
this.setSearchSource({
|
||||||
|
searchSource: to.query.source,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (this.searchSourceId != to.query.source) {
|
||||||
|
this.searchSourceId = to.query.source
|
||||||
|
}
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.handleGetHotSearch()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (to.query.text != null && this.text != to.query.text) {
|
||||||
this.text = to.query.text
|
this.text = to.query.text
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$nextTick(() => {
|
||||||
this.page = 1
|
this.page = 1
|
||||||
this.handleSearch(this.text, this.page)
|
this.handleSearch(this.text, this.page)
|
||||||
next()
|
})
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
this.listenEvent()
|
this.listenEvent()
|
||||||
|
@ -124,18 +140,20 @@ export default {
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
// console.log('mounted')
|
// console.log('mounted')
|
||||||
|
if (this.$route.query.source && (this.sourceList[this.$route.query.source] || this.$route.query.source == 'all')) {
|
||||||
// 处理搜索源不存在时页面报错的问题
|
this.setSearchSource({
|
||||||
if (!this.sourceList[this.setting.search.searchSource] && this.setting.search.searchSource != 'all') {
|
searchSource: this.$route.query.source,
|
||||||
|
})
|
||||||
|
} else if (!this.sourceList[this.setting.search.searchSource] && this.setting.search.searchSource != 'all') { // 处理搜索源不存在时页面报错的问题
|
||||||
this.setSearchSource({
|
this.setSearchSource({
|
||||||
searchSource: 'kw',
|
searchSource: 'kw',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
this.searchSourceId = this.setting.search.searchSource
|
this.searchSourceId = this.setting.search.searchSource
|
||||||
if (this.$route.query.text === undefined) {
|
if (this.$route.query.text == null) {
|
||||||
this.text = this.$store.getters['search/searchText']
|
this.text = this.$store.getters['search/searchText']
|
||||||
this.page = this.listInfo.page
|
this.page = this.listInfo.page
|
||||||
} else if (this.$route.query.text === '') {
|
} else if (this.$route.query.text == '') {
|
||||||
this.clearList()
|
this.clearList()
|
||||||
} else {
|
} else {
|
||||||
this.text = this.$route.query.text
|
this.text = this.$route.query.text
|
||||||
|
@ -156,18 +174,6 @@ export default {
|
||||||
'listInfo.list'() {
|
'listInfo.list'() {
|
||||||
this.removeAllSelect()
|
this.removeAllSelect()
|
||||||
},
|
},
|
||||||
searchSourceId(n) {
|
|
||||||
if (n === this.setting.search.searchSource) return
|
|
||||||
if (this.text !== '') this.isLoading = true
|
|
||||||
this.$nextTick(() => {
|
|
||||||
this.page = 1
|
|
||||||
this.handleSearch(this.text, this.page)
|
|
||||||
this.handleGetHotSearch()
|
|
||||||
})
|
|
||||||
this.setSearchSource({
|
|
||||||
searchSource: n,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters(['userInfo', 'setting']),
|
...mapGetters(['userInfo', 'setting']),
|
||||||
|
@ -417,7 +423,7 @@ export default {
|
||||||
this.getHotSearch(this.setting.search.searchSource)
|
this.getHotSearch(this.setting.search.searchSource)
|
||||||
},
|
},
|
||||||
handleNoitemSearch(text) {
|
handleNoitemSearch(text) {
|
||||||
this.$router.push({
|
this.$router.replace({
|
||||||
path: 'search',
|
path: 'search',
|
||||||
query: {
|
query: {
|
||||||
text,
|
text,
|
||||||
|
@ -512,6 +518,15 @@ export default {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
handleSourceChange(source) {
|
||||||
|
this.$router.replace({
|
||||||
|
path: 'search',
|
||||||
|
query: {
|
||||||
|
text: this.text,
|
||||||
|
source,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
Loading…
Reference in New Issue