Compare commits

...

82 Commits

Author SHA1 Message Date
lyswhut
ba2b1dac92 Merge branch 'dev' 2021-03-07 12:38:11 +08:00
lyswhut
56ffeb3008 发布v1.8.0 2021-03-07 12:36:57 +08:00
lyswhut
b3757fd6b5 修复在下载列表右击未下载完成的歌曲弹出的右击菜单中没有开始下载选项的问题 2021-03-07 12:22:53 +08:00
lyswhut
e0de81230e Merge branch 'api' into dev 2021-03-07 12:01:59 +08:00
lyswhut
2bc669c828 添加更新日志 2021-03-07 01:29:23 +08:00
lyswhut
493f7809a4 优化歌词特殊字符移除 2021-03-07 01:22:25 +08:00
lyswhut
ef52cd6991 Merge branch 'dev' into lrc 2021-03-07 01:18:54 +08:00
lyswhut
480f15b6f6 优化酷狗搜索结果 2021-03-07 01:18:44 +08:00
lyswhut
fcfee88ef4 移除日志打印 2021-03-07 00:56:00 +08:00
lyswhut
19f647e6ab Merge branch 'dev' into lrc 2021-03-04 15:27:45 +08:00
lyswhut
27e5ec02f4 优化单首歌曲的“添加到列表”弹窗歌曲列表状态的显示 2021-03-04 15:27:29 +08:00
lyswhut
0808d77a2d 修复鼠标显示状态 2021-03-04 14:33:23 +08:00
lyswhut
90d9e9b911 修复歌词缩放设置失效的问题 2021-03-04 13:20:19 +08:00
lyswhut
ebfbc11c20 Merge branch 'dev' into lrc 2021-03-03 17:21:13 +08:00
lyswhut
2764743062 更新说明 2021-03-03 16:52:19 +08:00
lyswhut
c66d7d6187 完善歌词 2021-03-03 16:51:02 +08:00
lyswhut
f2b6c0a443 支持桌面歌词拖动 2021-03-02 18:41:27 +08:00
lyswhut
aef0ffa506 修改桌面歌词为逐字播放 2021-03-02 18:10:12 +08:00
lyswhut
040fcfba4f 初步完善歌词 2021-03-02 08:32:26 +08:00
lyswhut
9f72831e4a 更新依赖 2021-02-23 19:51:01 +08:00
lyswhut
fa0d9ad4c3 新增启动参数-play 2021-02-22 12:59:39 +08:00
lyswhut
b6107b7281 优化列表存储逻辑 2021-02-21 16:19:08 +08:00
lyswhut
6ffe61562b 移除自动换源清理 2021-02-21 15:46:19 +08:00
lyswhut
b5f5e13f22 移除xm verify窗口 2021-02-21 12:34:50 +08:00
lyswhut
376b7545e1 修复我的列表的灰色歌曲会显示下载按钮的问题 2021-02-20 01:01:58 +08:00
lyswhut
ffaa3e591b 修复kg获取图片失败的错误处理 2021-02-20 00:50:00 +08:00
lyswhut
24d5722ef7 移除log 2021-02-19 22:10:37 +08:00
lyswhut
9e1fe0a6c1 移除多余代码 2021-02-19 21:59:58 +08:00
lyswhut
d128aeb6ef 修复kg歌曲详情页的歌曲无法播放的问题 2021-02-19 11:47:25 +08:00
lyswhut
ef371dedb4 修正描述 2021-02-15 19:53:46 +08:00
lyswhut
00d262f0e6 修复mg源歌单列表无法加载的问题 2021-02-15 12:40:12 +08:00
lyswhut
475b8924c3 修复歌单列表加载失败时无法进入歌单打开界面的问题 2021-02-15 10:52:29 +08:00
lyswhut
0afb1417a3 修复下载列表数据存储问题 2021-02-06 00:09:09 +08:00
lyswhut
b6b41660d6 移除未使用的变量 2021-02-05 23:07:36 +08:00
lyswhut
c78811fe36 移除虾米源,允许播放除了搜索列表以外的所有歌曲 2021-02-05 23:06:08 +08:00
lyswhut
a0254eb4f2 优化英文名歌曲匹配 2021-02-04 17:26:11 +08:00
lyswhut
e9c96ecde2 修复搜索列表双击不支持播放的源时会导致切歌的问题 2021-02-04 14:37:52 +08:00
lyswhut
f9cbf9254c 新增设置-其他-列表缓存信息清理功能 2021-02-02 15:08:07 +08:00
lyswhut
70d24ffdfd 修复音乐搜索列表的稍后播放功能无效的问题 2021-02-01 18:26:26 +08:00
lyswhut
84530ca0a2 添加网盘无法打开的提示 2021-02-01 16:17:57 +08:00
lyswhut
e848fda4e3 将下载列表与我的列表分离存储 2021-02-01 14:55:39 +08:00
lyswhut
51964e51c6 Merge branch 'dev' 2021-01-30 14:33:58 +08:00
lyswhut
0c63268df1 发布v1.7.1版本 2021-01-30 14:30:05 +08:00
lyswhut
316f62c3c8 修复MAC下xm音乐滑块验证问题 2021-01-30 14:29:20 +08:00
lyswhut
9e37800426 修复非透明模式下右侧滚动条无法拖动的问题 2021-01-30 14:18:12 +08:00
lyswhut
3e0ee374c9 移除对changelog的html对version.json的写入 2021-01-30 13:46:46 +08:00
lyswhut
bb411a365c Merge branch 'dev' 2021-01-30 13:17:47 +08:00
lyswhut
da0878a7a7 更新依赖到最新 2021-01-30 13:15:32 +08:00
lyswhut
d0328f17e1 发布v1.7.0版本 2021-01-30 13:14:33 +08:00
lyswhut
c6f49984a7 修改默认弹出层动画 2021-01-30 12:53:39 +08:00
lyswhut
f53fb071f0 更新electron到9.4.2 2021-01-29 22:24:37 +08:00
lyswhut
2cc8af71d0 修复自动换源遇到不支持的源时报错的问题 2021-01-29 10:18:48 +08:00
lyswhut
006da66028 导出歌单移除换源信息 2021-01-28 14:09:22 +08:00
lyswhut
2a74abdef4 修复xm源验证问题 2021-01-28 10:22:26 +08:00
lyswhut
269b14394d 修复了一个由版本更新日志显示导致的潜在远程代码执行攻击漏洞 2021-01-27 19:20:29 +08:00
lyswhut
d6d6fae2ca 禁止打开非http协议链接 2021-01-26 08:43:07 +08:00
lyswhut
331afc37df 新增“记住播放进度”功能的控制,优化设置项展示 2021-01-25 10:44:03 +08:00
lyswhut
041bfdaf3c 修复切换音乐来源可能无法切歌的问题 2021-01-23 19:36:29 +08:00
lyswhut
023cd15cd0 调整加载动画速度 2021-01-23 16:45:01 +08:00
lyswhut
f2fb37df4c 修复快速切换歌曲时, 会出现播放的歌曲和界面展示的歌曲不一致的问题 2021-01-23 16:33:12 +08:00
lyswhut
c4ae0dddb0 修改自动更新阶段的描述 2021-01-23 15:43:14 +08:00
lyswhut
f66077f012 修复手动切歌不清除稍后播放列表的问题 2021-01-23 14:56:57 +08:00
lyswhut
a31d6bc5ae 修改菜单大小 2021-01-23 14:42:01 +08:00
lyswhut
fad13f517b 更新electron到9.4.1 2021-01-21 16:26:46 +08:00
lyswhut
ef36517a26 修复恢复播放会导致重复播放的问题 2021-01-21 15:16:46 +08:00
lyswhut
085e45a50f 新增“稍后播放”功能 2021-01-21 14:39:37 +08:00
lyswhut
43b06a686b 修改构建命令 2021-01-20 14:55:34 +08:00
lyswhut
e8dba4d133 优化播放歌曲换源匹配规则 2021-01-18 16:28:11 +08:00
lyswhut
1af3cd1d77 优化播放歌曲换源匹配 2021-01-18 16:21:57 +08:00
lyswhut
4c44310fea 移除下面热搜词的加载 2021-01-16 01:11:55 +08:00
lyswhut
3e343a80fd 搜索界面新增搜索状态的提示 2021-01-16 01:11:17 +08:00
lyswhut
313fac9f18 Merge branch 'dev' 2021-01-13 21:35:00 +08:00
lyswhut
2c242c66e5 发布v1.6.1版本 2021-01-13 21:34:28 +08:00
lyswhut
4e2b525c99 移除initTOC调用 2021-01-13 14:06:59 +08:00
lyswhut
93c76d4ba0 添加参数修改说明 2021-01-13 14:05:47 +08:00
lyswhut
b69ecca252 修改目录 2021-01-13 11:16:16 +08:00
lyswhut
947011103c 降级electron到9.3.3修复部分系统没有声音的问题 2021-01-13 11:14:53 +08:00
lyswhut
2fd7e4503a 修复自动换源导致的搜索列表每页变成10条数据的问题 2021-01-12 11:42:56 +08:00
lyswhut
a5fce15661 改进自动换源时的歌曲匹配 2021-01-12 11:09:27 +08:00
lyswhut
f1561ddc21 修复某些情况下自动换源的时间过长时会终止换源自动切歌的问题 2021-01-11 16:21:08 +08:00
lyswhut
74ce094d79 更新无法试听解决方法 2021-01-11 14:12:58 +08:00
lyswhut
f832d70701 更新-dha参数的描述 2021-01-10 13:14:50 +08:00
120 changed files with 6993 additions and 5597 deletions

View File

@@ -19,6 +19,7 @@
"plugins": [
"@babel/plugin-syntax-dynamic-import",
"@babel/plugin-transform-modules-umd",
"@babel/plugin-transform-runtime"
"@babel/plugin-transform-runtime",
"@babel/plugin-proposal-class-properties"
]
}

View File

@@ -6,6 +6,85 @@ Project versioning adheres to [Semantic Versioning](http://semver.org/).
Commit convention is based on [Conventional Commits](http://conventionalcommits.org).
Change log format is based on [Keep a Changelog](http://keepachangelog.com/).
## [1.8.0](https://github.com/lyswhut/lx-music-desktop/compare/v1.7.1...v1.8.0) - 2021-03-07
### 新增
- 新增设置-其他-列表缓存信息清理功能,注:此功能一般情况下不要使用
- 新增启动参数`-play`可以在启动软件时播放指定歌单使用方法看Readme.md的"启动参数"部分
- 新增逐字歌词播放,默认开启,可到设置界面关闭,注:本功能目前仅对酷狗源的歌曲有效
- 新增自定义源功能,源编写规则可以去常见问题查看
### 优化
- 允许播放除了搜索列表以外的所有歌曲,即原来没有播放按钮或者灰色的歌曲都可以去尝试点击播放。注:该功能的原理是尝试自动切换到其他源播放,所以不一定会播放成功,特别是对于那些独家的资源
- 优化单首歌曲的“添加到列表”弹窗歌曲列表状态的显示;现在在收藏单首歌曲时,若列表存在本歌曲则列表名字将变成灰色不可点击状态。总的来说,在添加单首歌曲时若列表名是灰色,则证明当前歌曲已在那个列表中
- 将歌词翻译放到原文的下方,同时新增当前播放翻译的高亮功能
### 移除
- 移除虾米源。注:虽然已移除该源,但仍可尝试去播放之前添加的歌曲,虽然不一定会成功
### 修复
- 修复音乐搜索列表的稍后播放功能无效的问题
- 修复搜索列表双击不支持播放的源时会导致切歌的问题
- 修复歌单列表加载失败时无法进入歌单打开界面的问题
- 修复mg源歌单列表无法加载的问题
- 修复kg跳转到官方歌曲详情页的歌曲无法播放的问题
- 修复我的列表的歌曲添加到其他列表时不排除当前列表的问题
- 修复在下载列表右击未下载完成的歌曲弹出的右击菜单中没有开始下载选项的问题
### 变更
- 歌词翻译显示功能修改为默认关闭,注:此变更仅影响首次安装软件的用户
### 其他
- 更新electron到v9.4.4
## [1.7.1](https://github.com/lyswhut/lx-music-desktop/compare/v1.7.0...v1.7.1) - 2021-01-30
### 修复
- 修复非透明模式下右侧滚动条无法拖动的问题
- 修复MAC下xm音乐滑块验证问题
## [1.7.0](https://github.com/lyswhut/lx-music-desktop/compare/v1.6.1...v1.7.0) - 2021-01-30
### 新增
- 搜索界面新增搜索状态的提示
- 新增“稍后播放”功能,可在歌曲列表右键菜单使用
- 新增“记住播放进度”功能的控制,该功能默认不再开启,可到播放设置-记住播放进度开启
### 优化
- 优化播放歌曲换源匹配
- 优化设置界面设置项的展示
### 修复
- 修复快速切换歌曲时, 会出现播放的歌曲和界面展示的歌曲不一致的问题
- 修复了一个由版本更新日志显示导致的潜在远程代码执行攻击漏洞该漏洞影响v1.6.1及之前的所有版本,请务必更新到最新版本
- 修复xm搜索源验证问题
### 其他
- 更新electron到9.4.2
## [1.6.1](https://github.com/lyswhut/lx-music-desktop/compare/v1.6.0...v1.6.1) - 2021-01-13
### 优化
- 改进自动换源时的歌曲匹配
### 修复
- 修复某些情况下自动换源的时间过长时会终止换源自动切歌的问题
- 修复自动换源导致的搜索列表每页变成10条数据的问题
- 降级electron到9.3.3修复部分系统没有声音的问题
## [1.6.0](https://github.com/lyswhut/lx-music-desktop/compare/v1.5.0...v1.6.0) - 2021-01-10
### 新增

170
FAQ.md
View File

@@ -13,7 +13,7 @@
1. 尝试更新到最新版本
2. 尝试切换其他歌曲(或直接搜索该歌曲),若全部歌曲都无法试听与下载则进行下一步
3. 尝试到 设置-音乐来源 切换到其他接口
4. 尝试切换网络,比如用手机开热点(目前存在某些网络无法访问接口服务器的情况
4. 尝试切换网络,比如用手机开热点(所有歌曲都提示请求异常时可通过此方法解决,或等一两天后再试
5. 若还不行请到这个链接查看详情:<https://github.com/lyswhut/lx-music-desktop/issues/5>
6. 若没有在第5条链接中的第一条评论中看到接口无法使用的说明则应该是你网络无法访问接口服务器的问题如果接口有问题我会在那里说明。
@@ -62,6 +62,8 @@
根据Electron里issue的[解决方案](https://github.com/electron/electron/issues/2170#issuecomment-736223269)<br>
若你遇到透明问题可尝试添加启动参数 `-dha` 来禁用硬件加速,例如:`.\lx-music-desktop.exe -dha`
v1.6.0及之后的版本才支持`-dha`参数
## 软件启动后,界面无法显示
对于软件启动后,可以在任务栏看到软件,但软件界面在桌面上无任何显示,或者整个界面偶尔闪烁的情况。<br>
@@ -109,13 +111,13 @@ Windows 7 未开启 Aero 效果时桌面歌词会有问题,详情看下面的
`v1.2.1`以前的版本在 Ubuntu 18.10 下第一次开启桌面歌词时歌词窗口会变白,需要关闭后再开启,
`v1.2.1`及之后的版本已修复该问题。
其他 Linux 系统未测试,如有异常也是意料之中,目前不打算去处理 Linux 平台的桌面歌词问题。
其他 Linux 系统未测试,如有异常也是意料之中,目前不打算去处理 Linux 平台的桌面歌词问题,但你可以尝试按照`Linux 下界面异常`的解决方案去解决
## 歌曲下载失败
### 提示 `ENOENT: no such file or directory, mkdir`
更换下载歌曲目录即可解决(一般是设置的歌曲下载目录没有读写权限导致的)。
更换下载歌曲目录即可解决(一般是设置的歌曲下载目录没有读写权限导致的)。
### 提示 `请求异常` 或 `Fail`
@@ -169,3 +171,165 @@ Windows 7 未开启 Aero 效果时桌面歌词会有问题,详情看下面的
`v0.17.0`起,由于加入了音频输出设备切换功能,该功能调用了 [MediaDevices.enumerateDevices()](https://developer.mozilla.org/zh-CN/docs/Web/API/MediaDevices/enumerateDevices),可能导致安全软件提示洛雪要访问摄像头(目前发现卡巴斯基会提示),但实际上没有用到摄像头,并且摄像头的提示灯也不会亮,你可以选择阻止访问。
最后,若出现杀毒软件报毒、存在恶意行为,请自行判断选择是否继续使用本软件!
## 自定义源脚本编写说明
文件请使用UTF-8编码格式编写脚本所用编程语言为JavaScript可以使用ES6+语法,脚本与应用的交互是使用类似事件收发的方式进行,这是一个基本的脚本例子:
```js
/**
* @name 测试音乐源
* @description 我只是一个测试音乐源哦
* @version 1.0.0
* @author xxx
* @homepage http://xxx
*/
const { EVENT_NAMES, request, on, send } = window.lx
const qualitys = {
kw: {
'128k': '128',
'320k': '320',
flac: 'flac',
},
}
const httpRequest = (url, options) => new Promise((resolve, reject) => {
request(url, options, (err, resp) => {
if (err) return reject(err)
resolve(resp.body)
})
})
const apis = {
kw: {
musicUrl({ songmid }, quality) {
return httpRequest('http://xxx').then(data => {
return data.url
})
},
},
}
// 注册应用API请求事件
// source 音乐源可能的值取决于初始化时传入的sources对象的源key值
// info 请求附加信息内容根据action变化
// action 请求操作类型目前只有musicUrl即获取音乐URL链接
// 当action为musicUrl时info的结构{type, musicInfo}
// info.type音乐质量可能的值有128k / 320k / flac取决于初始化时对应源传入的qualitys值中的一个
// info.musicInfo音乐信息对象里面有音乐ID、名字等信息
on(EVENT_NAMES.request, ({ source, action, info }) => {
// 回调必须返回 Promise 对象
switch (action) {
// action 为 musicUrl 时需要在 Promise 返回歌曲 url
case 'musicUrl':
return apis[source].musicUrl(info.musicInfo, qualitys[source][info.type]).catch(err => {
console.log(err)
return Promise.reject(err)
})
}
})
// 脚本初始化完成后需要发送inited事件告知应用
send(EVENT_NAMES.inited, {
status: true, // 初始化成功 or 失败
openDevTools: false, // 是否打开开发者工具,方便用于调试脚本
sources: { // 当前脚本支持的源
kw: { // 支持的源对象可用key值kw/kg/tx/wy/mg
name: '酷我音乐',
type: 'music', // 目前固定为 music
actions: ['musicUrl'], // 目前固定为 ['musicUrl']
qualitys: ['128k', '320k', 'flac'], // 当前脚本的该源所支持获取的Url音质有效的值有['128k', '320k', 'flac']
},
},
})
```
### 自定义源信息
文件的开头必须包含以下注释:
```js
/**
* @name 测试脚本
* @description 我只是一个测试脚本
* @version 1.0.0
* @author xxx
* @homepage http://xxx
*/
```
- `@name `源的名字建议不要过长10个字符以内
- `@description `源的描述建议不要过长20个字符以内可不填不填时必须保留 @description
- `@version`:源的版本号,可不填,不填时可以删除 @version
- `@author `:脚本作者名字,可不填,不填时可以删除 @author
- `@homepage `:脚本主页,可不填,不填时可以删除 @homepage
### `window.lx`
应用为脚本暴露的API对象。
#### `window.lx.EVENT_NAMES`
常量事件名称对象,发送、注册事件时传入事件名时使用,可用值:
| 事件名 | 描述
| --- | ---
| `inited` | 脚本初始化完成后发送给应用的事件名,发送该事件时需要传入以下信息:`{status, sources, openDevTools}`<br>`status`:初始化结果(`true`成功,`false`失败)<br>`openDevTools`是否打开DevTools此选项可用于开发脚本时的调试<br>`sources`:支持的源信息对象,<br>`sources[kw/kg/tx/wy/mg].name`:源的名字(目前非必须)<br>`sources[kw/kg/tx/wy/mg].type`:源类型,目前固定值需为`music`<br>`sources[kw/kg/tx/wy/mg].actions`支持的actions由于目前只支持`musicUrl`,所以固定传`['musicUrl']`即可<br>`sources[kw/kg/tx/wy/mg].qualitys`:该源支持的音质列表,支持的值`['128k', '320k', 'flac']`,该字段用于控制应用可用的音质类型
| `request` | 应用API请求事件名回调入参`handler({ source, action, info})`,回调必须返回`Promise`对象<br>`source`:音乐源,可能的值取决于初始化时传入的`sources`对象的源key值<br>`info`:请求附加信息,内容根据`action`变化<br>`action`:请求操作类型,目前只有`musicUrl`即获取音乐URL链接需要在 Promise 返回歌曲 url`info`的结构:`{type, musicInfo}``info.type`:音乐质量,可能的值有`128k` / `320k` / `flac`(取决于初始化时对应源传入的`qualitys`值中的一个),`info.musicInfo`音乐信息对象里面有音乐ID、名字等信息
#### `window.lx.on`
事件注册方法,应用主动与脚本通信时使用:
```js
/**
* @param event_name 事件名
* @param handler 事件处理回调 -- 注意:注册的回调必须返回 Promise 对象
*/
window.lx.on(event_name, handler)
```
**注意:** 注册的回调必须返回 `Promise` 对象。
#### `window.lx.send`
事件发送方法,脚本主动与应用通信时使用:
```js
/**
* @param event_name 事件名
* @param datas 要传给应用的数据
*/
window.lx.send(event_name, datas)
```
#### `window.lx.request`
HTTP请求方法用于发送HTTP请求此HTTP请求方法不受跨域规则限制
```js
/**
* @param url 请求的URL
* @param options 请求选项,可用选项有 method / headers / body / form / formData / timeout
* @param callback 请求结果的回调 入参err, resp, body
* @return 返回一个方法调用此方法可以终止HTTP请求
*/
const cancelHttp = window.lx.request(url, options, callback)
```
#### `window.lx.utils`
应用提供给脚本的工具方法:
- `window.lx.utils.buffer.from`对应Node.js的 `Buffer.from`
- `window.lx.utils.crypto.aesEncrypt`AES加密 `aesEncrypt(buffer, mode, key, iv)`
- `window.lx.utils.crypto.md5`MD5加密 `md5(str)`
- `window.lx.utils.crypto.randomBytes`:生成随机字符串 `randomBytes(size)`
- `window.lx.utils.crypto.rsaEncrypt`RSA加密 `rsaEncrypt(buffer, key)`
目前仅提供以上工具方法如果需要其他方法可以开issue讨论。

View File

@@ -47,7 +47,7 @@
软件变化请查看:[更新日志](https://github.com/lyswhut/lx-music-desktop/blob/master/CHANGELOG.md)<br>
软件下载请转到:[发布页面](https://github.com/lyswhut/lx-music-desktop/releases)<br>
或者到网盘下载网盘内有MAC、windows版`https://www.lanzoux.com/b0bf2cfa/` 密码:`glqw`<br>
或者到网盘下载网盘内有MAC、windows版`https://www.lanzoux.com/b0bf2cfa/` 密码:`glqw`(若链接无法打开请百度:蓝奏云链接打不开)<br>
使用常见问题请转至:[常见问题](https://github.com/lyswhut/lx-music-desktop/blob/master/FAQ.md)
### 源码使用方法
@@ -62,7 +62,7 @@ npm run dev
npm run pack:dir
# 构建安装包Windows版
npm run pack
npm run pack:win
# 构建安装包Mac版
npm run pack:mac
@@ -81,8 +81,14 @@ npm run pack:linux
目前软件已支持的启动参数如下:
- `-search` 启动软件时自动在搜索框搜索指定的内容,例如:`-search="突然的自我 - 伍佰"`
- `-dha` 禁用硬件加速启动Disable Hardware Acceleration窗口显示有问题时可以尝试添加此参数启动
- `-dt` 以非透明模式启动Disable Transparent对于未开启AERO效果的win7系统可加此参数启动以确保界面正常显示原来的`-nt`参数已重命名为`-dt`
- `-dha` 禁用硬件加速启动Disable Hardware Acceleration窗口显示有问题时可以尝试添加此参数启动v1.6.0起新增)
- `-dt` 以非透明模式启动Disable Transparent对于未开启AERO效果的win7系统可加此参数启动以确保界面正常显示(注:该参数对桌面歌词无效),原来的`-nt`参数已重命名为`-dt`v1.6.0起重命名)
- `-play` 启动时播放指定列表的音乐,参数说明:
- `type`:播放类型,目前固定为`songList`
- `source`:播放源,可用值为`kw/kg/tx/wy/mg/myList`,其中`kw/kg/tx/wy/mg`对应各源的在线列表,`myList`为本地列表
- `link`要播放的在线列表歌单链接、或IDsource为`kw/kg/tx/wy/mg`之一(在线列表)时必传,举例:`./lx-music-desktop -play="type=songList&source=kw&link=歌单URL or ID"注意如果传入URL时必须对URL进行编码后再传入
- `name`要播放的本地列表歌单名字source为`myList`时必传,举例:`./lx-music-desktop -play="type=songList&source=myList&name=默认列表"
- `index`:从列表的哪个位置开始播放,选传,若不传默认播放第一首歌曲,举例:`./lx-music-desktop -play="type=songList&source=myList&name=默认列表&index=2"
### 常见问题

View File

@@ -17,8 +17,8 @@ module.exports = merge(baseConfig, {
NODE_ENV: '"development"',
},
__static: `"${path.join(__dirname, '../../src/static').replace(/\\/g, '\\\\')}"`,
__userApi: `"${path.join(__dirname, '../../src/main/modules/userApi').replace(/\\/g, '\\\\')}"`,
}),
new webpack.NoEmitOnErrorsPlugin(),
new FriendlyErrorsPlugin({
onErrors(severity, errors) { // Silent warning from electron-debug
if (severity != 'warning') return

View File

@@ -1,6 +1,7 @@
const path = require('path')
const { merge } = require('webpack-merge')
const webpack = require('webpack')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const baseConfig = require('./webpack.config.base')
@@ -20,6 +21,18 @@ module.exports = merge(baseConfig, {
__filename: false,
},
plugins: [
new CopyWebpackPlugin({
patterns: [
{
from: path.join(__dirname, '../../src/main/modules/userApi/renderer'),
to: path.join(__dirname, '../../dist/electron/userApi/renderer'),
},
{
from: path.join(__dirname, '../../src/main/modules/userApi/rendererEvent/name.js'),
to: path.join(__dirname, '../../dist/electron/userApi/rendererEvent/name.js'),
},
],
}),
new webpack.DefinePlugin({
'process.env': {
NODE_ENV: '"production"',

View File

@@ -11,7 +11,6 @@ module.exports = merge(baseConfig, {
devtool: 'eval-source-map',
plugins: [
new webpack.HotModuleReplacementPlugin(),
new webpack.NoEmitOnErrorsPlugin(),
new FriendlyErrorsPlugin(),
new webpack.DefinePlugin({
'process.env': {

View File

@@ -11,7 +11,6 @@ module.exports = merge(baseConfig, {
devtool: 'eval-source-map',
plugins: [
new webpack.HotModuleReplacementPlugin(),
new webpack.NoEmitOnErrorsPlugin(),
new FriendlyErrorsPlugin(),
new webpack.DefinePlugin({
'process.env': {

View File

@@ -179,6 +179,9 @@ function startElectron() {
function electronLog(data, color) {
let log = data.toString()
if (/[0-9A-z]+/.test(log)) {
// 抑制 user api 窗口使用 data url 加载页面时 vue扩展 的报错日志刷屏的问题
if (color == 'red' && typeof log === 'string' && log.includes('"Extension server error: Operation failed: Permission denied", source: devtools://devtools/bundled/extensions/extensions.js')) return
console.log(chalk[color](log))
}
}

6290
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +1,12 @@
{
"name": "lx-music-desktop",
"version": "1.6.0",
"description": "一个免费的音乐下载助手",
"version": "1.8.0",
"description": "一个免费的音乐查找助手",
"main": "./dist/electron/main.js",
"productName": "lx-music-desktop",
"scripts": {
"pack": "node build-config/pack.js && npm run pack:win",
"pack:win": "npm run pack:win:setup:x86_64 && npm run pack:win:7z",
"pack": "node build-config/pack.js && npm run pack:win:setup:x64",
"pack:win": "node build-config/pack.js && npm run pack:win:setup:x86_64 && npm run pack:win:7z",
"pack:win:setup:x86_64": "cross-env TARGET=win_安装版 ARCH=x86_64 electron-builder -w=nsis --x64 --ia32",
"pack:win:setup:x64": "cross-env TARGET=win_安装版 ARCH=x64 electron-builder -w=nsis --x64",
"pack:win:setup:x86": "cross-env TARGET=win_安装版 ARCH=x86 electron-builder -w=nsis --ia32",
@@ -34,18 +34,20 @@
"publish:gh:linux": "node build-config/pack.js && npm run publish:linux",
"publish:linux": "npm run publish:linux:deb && npm run publish:linux:appImage && npm run publish:linux:rpm && npm run publish:linux:pacman",
"publish:linux:appImage": "cross-env ARCH=x64 electron-builder -l=AppImage -p onTagOrDraft",
"publish:linux:deb": "npm run publish:linux:deb:x64 && npm run publish:linux:deb:x86 && npm run publish:linux:deb:arm64",
"publish:linux:deb": "npm run publish:linux:deb:x64 && npm run publish:linux:deb:x86 && npm run publish:linux:deb:arm64 && npm run publish:linux:deb:armv7l",
"publish:linux:deb:x64": "cross-env ARCH=x64 electron-builder -l=deb --x64 -p onTagOrDraft",
"publish:linux:deb:x86": "cross-env ARCH=x86 electron-builder -l=deb --ia32 -p onTagOrDraft",
"publish:linux:deb:arm64": "cross-env ARCH=arm64 electron-builder -l=deb --arm64 -p onTagOrDraft",
"publish:linux:deb:armv7l": "cross-env ARCH=armv7l electron-builder -l=deb --armv7l -p onTagOrDraft",
"publish:linux:rpm": "cross-env ARCH=x64 electron-builder -l=rpm --x64 -p onTagOrDraft",
"publish:linux:pacman": "cross-env ARCH=x64 electron-builder -l=pacman --x64 -p onTagOrDraft",
"pack:linux": "node build-config/pack.js && npm run pack:linux:deb && npm run pack:linux:appImage && npm run pack:linux:rpm && npm run pack:linux:pacman",
"pack:linux:appImage": "cross-env ARCH=x64 electron-builder -l=AppImage",
"pack:linux:deb": "npm run pack:linux:deb:x64 && npm run pack:linux:deb:x86 && npm run pack:linux:deb:arm64",
"pack:linux:deb": "npm run pack:linux:deb:x64 && npm run pack:linux:deb:x86 && npm run pack:linux:deb:arm64 && npm run pack:linux:deb:armv7l",
"pack:linux:deb:x64": "cross-env ARCH=x64 electron-builder -l=deb --x64",
"pack:linux:deb:x86": "cross-env ARCH=x86 electron-builder -l=deb --ia32",
"pack:linux:deb:arm64": "cross-env ARCH=arm64 electron-builder -l=deb --arm64",
"pack:linux:deb:armv7l": "cross-env ARCH=armv7l electron-builder -l=deb --armv7l",
"pack:linux:rpm": "cross-env ARCH=x64 electron-builder -l=rpm --x64",
"pack:linux:pacman": "cross-env ARCH=x64 electron-builder -l=pacman --x64",
"pack:mac": "node build-config/pack.js && electron-builder -m=dmg",
@@ -58,7 +60,8 @@
"build:renderer-lyric": "cross-env NODE_ENV=production webpack --config build-config/renderer-lyric/webpack.config.prod.js --progress --hide-modules",
"build": "npm run clean:electron && npm run build:main && npm run build:renderer && npm run build:renderer-lyric",
"lint": "eslint --ext .js,.vue -f ./node_modules/eslint-formatter-friendly src",
"lint:fix": "eslint --ext .js,.vue -f ./node_modules/eslint-formatter-friendly --fix src"
"lint:fix": "eslint --ext .js,.vue -f ./node_modules/eslint-formatter-friendly --fix src",
"up": "cross-env ELECTRON_GET_USE_PROXY=true GLOBAL_AGENT_HTTPS_PROXY=http://localhost:1081 npm update"
},
"browserslist": [
"Electron 9.3.4"
@@ -160,12 +163,13 @@
},
"homepage": "https://github.com/lyswhut/lx-music-desktop#readme",
"devDependencies": {
"@babel/core": "^7.12.10",
"@babel/core": "^7.13.8",
"@babel/plugin-proposal-class-properties": "^7.13.0",
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/plugin-transform-modules-umd": "^7.12.1",
"@babel/plugin-transform-runtime": "^7.12.10",
"@babel/plugin-transform-modules-umd": "^7.13.0",
"@babel/plugin-transform-runtime": "^7.13.9",
"@babel/polyfill": "^7.12.1",
"@babel/preset-env": "^7.12.11",
"@babel/preset-env": "^7.13.9",
"babel-eslint": "^10.1.0",
"babel-loader": "^8.2.2",
"babel-minify-webpack-plugin": "^0.3.1",
@@ -173,69 +177,69 @@
"cfonts": "^2.9.1",
"chalk": "^4.1.0",
"changelog-parser": "^2.8.0",
"copy-webpack-plugin": "^6.4.0",
"core-js": "^3.8.2",
"copy-webpack-plugin": "^6.4.1",
"core-js": "^3.9.1",
"cross-env": "^7.0.3",
"css-loader": "^4.3.0",
"del": "^6.0.0",
"electron": "^9.4.0",
"electron-builder": "^22.9.1",
"electron": "^9.4.4",
"electron-builder": "^22.10.5",
"electron-debug": "^3.2.0",
"electron-devtools-installer": "^3.1.1",
"eslint": "^7.17.0",
"eslint": "^7.21.0",
"eslint-config-standard": "^14.1.1",
"eslint-formatter-friendly": "^7.0.0",
"eslint-loader": "^4.0.2",
"eslint-plugin-html": "^6.1.1",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^4.2.1",
"eslint-plugin-promise": "^4.3.1",
"eslint-plugin-standard": "^4.1.0",
"file-loader": "^6.2.0",
"friendly-errors-webpack-plugin": "^1.7.0",
"html-webpack-plugin": "^4.5.1",
"less": "^3.12.2",
"less-loader": "^7.2.1",
"html-webpack-plugin": "^4.5.2",
"less": "^3.13.1",
"less-loader": "^7.3.0",
"markdown-it": "^12.0.4",
"mini-css-extract-plugin": "^0.12.0",
"optimize-css-assets-webpack-plugin": "^5.0.4",
"postcss-loader": "^4.1.0",
"postcss-loader": "^4.2.0",
"postcss-pxtorem": "^5.1.1",
"pug": "^3.0.0",
"pug": "^3.0.2",
"pug-loader": "^2.4.0",
"pug-plain-loader": "^1.1.0",
"raw-loader": "^4.0.2",
"rimraf": "^3.0.2",
"spinnies": "^0.5.1",
"stylus": "^0.54.8",
"stylus-loader": "^4.3.1",
"stylus-loader": "^4.3.3",
"terser-webpack-plugin": "^4.2.3",
"url-loader": "^4.1.1",
"vue-loader": "^15.9.6",
"vue-template-compiler": "^2.6.12",
"webpack": "^4.44.2",
"webpack": "^4.46.0",
"webpack-cli": "^3.3.12",
"webpack-dev-server": "^3.11.1",
"webpack-dev-server": "^3.11.2",
"webpack-hot-middleware": "^2.25.0",
"webpack-merge": "^5.7.3"
},
"dependencies": {
"crypto-js": "^4.0.0",
"dnscache": "^1.0.2",
"electron-log": "^4.3.1",
"electron-log": "^4.3.2",
"electron-store": "^6.0.1",
"electron-updater": "^4.3.5",
"electron-updater": "^4.3.8",
"iconv-lite": "^0.6.2",
"image-size": "^0.9.3",
"image-size": "^0.9.4",
"js-htmlencode": "^0.3.0",
"lrc-file-parser": "^1.0.5",
"lrc-file-parser": "^1.0.7",
"needle": "^2.6.0",
"node-id3": "^0.2.2",
"request": "^2.88.2",
"vue": "^2.6.12",
"vue-i18n": "^8.22.3",
"vue-router": "^3.4.9",
"vuex": "^3.6.0",
"vue-i18n": "^8.23.0",
"vue-router": "^3.5.1",
"vuex": "^3.6.2",
"vuex-router-sync": "^5.0.0"
}
}

View File

@@ -1,20 +1,34 @@
### 新增
- 我的列表右键菜单新增列表排序功能,可调整单曲、多选后的歌曲的顺序。注意:多选排序还将会按照选中歌曲时的顺序排序
- 添加鼠标提示的自动关闭功能鼠标长时间目前是10秒不动时鼠标提示将会自动关闭
- 添加鼠标指向歌曲封面的提示(对于进度条左边的歌曲封面,你可能不知道的操作->右击在“我的列表”定位当前播放的歌曲
- 隐藏播放详情页按钮添加快速隐藏详情页提示(你可能不知道的操作->在播放详情页内的任意非窗口可拖动区域右键双击可以快速隐藏详情页)
- 添加桌面歌词字体、透明度调整按钮微调提示(你可能不知道的操作->对于字体、透明度可右击微调)
- 我的列表右键菜单添加搜索当前歌曲功能
- 新增`-dha`参数添加此启动参数将禁用硬件加速启动Disable Hardware Acceleration窗口显示有问题时可以尝试添加此参数启动Linux系统的界面显示有问题时可尝试添加此参数启动若不行可尝试添加`-dt`参数启动
- 新增播放自动换源功能~
- 新增设置-其他-列表缓存信息清理功能,注:此功能一般情况下不要使用
- 新增启动参数`-play`可以在启动软件时播放指定歌单使用方法看Readme.md的"启动参数"部分
- 新增逐字歌词播放,默认开启,可到设置界面关闭,注:本功能目前仅对酷狗源的歌曲有效
- 新增自定义源功能,源编写规则可以去常见问题查看
### 变更
### 优化
- `-nt`参数更名为`-dt`Disable Transparent目前原来的`-nt`参数仍然可用,但将在后续的版本中移除
- 允许播放除了搜索列表以外的所有歌曲,即原来没有播放按钮或者灰色的歌曲都可以去尝试点击播放。注:该功能的原理是尝试自动切换到其他源播放,所以不一定会播放成功,特别是对于那些独家的资源
- 优化单首歌曲的“添加到列表”弹窗歌曲列表状态的显示;现在在收藏单首歌曲时,若列表存在本歌曲则列表名字将变成灰色不可点击状态。总的来说,在添加单首歌曲时若列表名是灰色,则证明当前歌曲已在那个列表中
- 将歌词翻译放到原文的下方,同时新增当前播放翻译的高亮功能
### 移除
- 移除虾米源。注:虽然已移除该源,但仍可尝试去播放之前添加的歌曲,虽然不一定会成功
### 修复
- 修复恢复上次播放的歌曲时在随机播放模式下不把恢复播放的歌曲放入已播放队列的问题(该问题会导致随机模式下会导致未播放完整个列表前就会再次随机到该歌曲,以及无法通过上一曲切回该歌曲)
- 修复音乐嵌入的封面在 Mac 系统无法显示的问题
- 修复`-dt`(原来的`-nt`)启动参数不真正生效的问题
- 修复音乐搜索列表的稍后播放功能无效的问题
- 修复搜索列表双击不支持播放的源时会导致切歌的问题
- 修复歌单列表加载失败时无法进入歌单打开界面的问题
- 修复mg源歌单列表无法加载的问题
- 修复kg跳转到官方歌曲详情页的歌曲无法播放的问题
- 修复我的列表的歌曲添加到其他列表时不排除当前列表的问题
- 修复在下载列表右击未下载完成的歌曲弹出的右击菜单中没有开始下载选项的问题
### 变更
- 歌词翻译显示功能修改为默认关闭,注:此变更仅影响首次安装软件的用户
### 其他
- 更新electron到v9.4.4

View File

@@ -44,7 +44,7 @@ module.exports = async newVerNum => {
desc: version.desc,
})
version.version = newVerNum
version.desc = newChangeLog
version.desc = newMDChangeLog.replace(/(?:^|(\n))#{1,6} (.+)\n/g, '$1$2').trim()
pkg.version = newVerNum
console.log(chalk.blue('new version: ') + chalk.green(newVerNum))

File diff suppressed because one or more lines are too long

View File

@@ -51,6 +51,6 @@ module.exports = {
},
],
navigationUrlWhiteList: [
/^https:\/\/www\.xiami\.com/,
],
}

View File

@@ -2,7 +2,7 @@ const path = require('path')
const os = require('os')
const defaultSetting = {
version: '1.0.38',
version: '1.0.40',
player: {
togglePlayMethod: 'listLoop',
highQuality: false,
@@ -11,7 +11,9 @@ const defaultSetting = {
isMute: false,
mediaDeviceId: 'default',
isMediaDeviceRemovedStopPlay: false,
isShowLyricTransition: true,
isShowLyricTransition: false,
isPlayLxlrc: true,
isSavePlayTime: false,
},
desktopLyric: {
enable: false,

View File

@@ -2,11 +2,17 @@ const { ipcMain, ipcRenderer } = require('electron')
const names = require('./ipcNames')
exports.mainOn = (event, callback) => {
ipcMain.on(event, callback)
exports.mainOn = (name, callback) => {
ipcMain.on(name, callback)
}
exports.mainOnce = (event, callback) => {
ipcMain.once(event, callback)
exports.mainOnce = (name, callback) => {
ipcMain.once(name, callback)
}
exports.mainOff = (name, callback) => {
ipcMain.removeListener(name, callback)
}
exports.mainOffAll = name => {
ipcMain.removeAllListeners(name)
}
exports.mainHandle = (name, callback) => {
@@ -15,6 +21,9 @@ exports.mainHandle = (name, callback) => {
exports.mainHandleOnce = (name, callback) => {
ipcMain.handleOnce(name, callback)
}
exports.mainHandleRemove = name => {
ipcMain.removeListener(name)
}
exports.mainSend = (window, name, params) => {
window.webContents.send(name, params)
@@ -33,5 +42,11 @@ exports.rendererOn = (name, callback) => {
exports.rendererOnce = (name, callback) => {
ipcRenderer.once(name, callback)
}
exports.rendererOff = (name, callback) => {
ipcRenderer.removeListener(name, callback)
}
exports.rendererOffAll = name => {
ipcRenderer.removeAllListeners(name)
}
exports.NAMES = names

View File

@@ -50,6 +50,15 @@ const names = {
get_data: 'get_data',
save_data: 'save_data',
get_hot_key: 'get_hot_key',
import_user_api: 'import_user_api',
remove_user_api: 'remove_user_api',
set_user_api: 'set_user_api',
get_user_api_list: 'get_user_api_list',
request_user_api: 'request_user_api',
request_user_api_cancel: 'request_user_api_cancel',
get_user_api_status: 'get_user_api_status',
user_api_status: 'user_api_status',
},
winLyric: {
close: 'close',

View File

@@ -1,7 +1,7 @@
const log = require('electron-log')
const Store = require('electron-store')
const { defaultSetting, overwriteSetting } = require('./defaultSetting')
const apiSource = require('../renderer/utils/music/api-source-info')
// const apiSource = require('../renderer/utils/music/api-source-info')
const defaultHotKey = require('./defaultHotKey')
const { dialog, app } = require('electron')
const path = require('path')
@@ -115,10 +115,10 @@ exports.mergeSetting = (setting, version) => {
setting = defaultSettingCopy
}
if (!apiSource.some(api => api.id === setting.apiSource && !api.disabled)) {
let api = apiSource.find(api => !api.disabled)
if (api) setting.apiSource = api.id
}
// if (!apiSource.some(api => api.id === setting.apiSource && !api.disabled)) {
// let api = apiSource.find(api => !api.disabled)
// if (api) setting.apiSource = api.id
// }
return { setting, version: defaultVersion }
}
@@ -150,6 +150,9 @@ exports.initSetting = () => {
const electronStore_config = new Store({
name: 'config',
})
const electronStore_downloadList = new Store({
name: 'downloadList',
})
let setting = electronStore_config.get('setting')
if (setting) {
let version = electronStore_config.get('version')
@@ -165,7 +168,7 @@ exports.initSetting = () => {
}
const downloadList = electronStore_config.get('download')
if (downloadList) {
if (downloadList.list) electronStore_list.set('downloadList', downloadList.list)
if (downloadList.list) electronStore_downloadList.set('list', downloadList.list)
electronStore_config.delete('download')
}
}
@@ -181,6 +184,13 @@ exports.initSetting = () => {
}
}
// 从我的列表分离下载列表 v1.7.0 后
let downloadList = electronStore_list.get('downloadList')
if (downloadList) {
electronStore_downloadList.set('list', downloadList)
electronStore_list.delete('downloadList')
}
const { version: settingVersion, setting: newSetting } = exports.mergeSetting(setting, electronStore_config.get('version'))
// 重置 ^0.18.2 排行榜ID

View File

@@ -1,4 +1,4 @@
global.lx_event = {}
if (!global.lx_event) global.lx_event = {}
const Common = require('./Common')
const MainWindow = require('./MainWindow')
@@ -6,8 +6,12 @@ const Tray = require('./Tray')
const WinLyric = require('./WinLyric')
const HotKey = require('./HotKey')
const { Event: UserApi } = require('../modules/userApi')
if (!global.lx_event.common) global.lx_event.common = new Common()
if (!global.lx_event.mainWindow) global.lx_event.mainWindow = new MainWindow()
if (!global.lx_event.tray) global.lx_event.tray = new Tray()
if (!global.lx_event.winLyric) global.lx_event.winLyric = new WinLyric()
if (!global.lx_event.hotKey) global.lx_event.hotKey = new HotKey()
if (!global.lx_event.userApi) global.lx_event.userApi = new UserApi()

View File

@@ -54,6 +54,7 @@ app.on('web-contents-created', (event, contents) => {
event.preventDefault()
if (/^devtools/.test(navigationUrl)) return
console.log(navigationUrl)
if (!/^https?:\/\//.test(navigationUrl)) return
await shell.openExternal(navigationUrl)
})
contents.on('will-attach-webview', (event, webPreferences, params) => {

View File

@@ -2,3 +2,4 @@ require('./appMenu')
require('./winLyric')
require('./tray')
require('./hotKey')
require('./userApi')

View File

@@ -0,0 +1,2 @@
exports.userApis = []

View File

@@ -0,0 +1,11 @@
const { EventEmitter } = require('events')
const USER_API_EVENT_NAME = require('./name')
class UserApi extends EventEmitter {
status(info) {
this.emit(USER_API_EVENT_NAME.status, info)
}
}
module.exports = UserApi

View File

@@ -0,0 +1,3 @@
module.exports = {
status: 'status',
}

View File

@@ -0,0 +1,44 @@
const Event = require('./event/event')
const eventNames = require('./event/name')
const { closeWindow } = require('./main')
const { getUserApis, importApi, removeApi } = require('./utils')
const { request, cancelRequest, getStatus, loadApi } = require('./rendererEvent/rendererEvent')
// const { getApiList, importApi, removeApi, setApi, getStatus, request, eventNames }
let userApiId
exports.Event = Event
exports.eventNames = eventNames
exports.getApiList = getUserApis
exports.importApi = script => {
return {
apiInfo: importApi(script),
apiList: getUserApis(),
}
}
exports.request = request
exports.cancelRequest = cancelRequest
exports.getStatus = getStatus
exports.removeApi = async ids => {
if (userApiId && ids.includes(userApiId)) {
userApiId = null
await closeWindow()
}
removeApi(ids)
return getUserApis()
}
exports.setApi = async id => {
if (userApiId) {
userApiId = null
await closeWindow()
}
const apiList = getUserApis()
if (!apiList.some(a => a.id === id)) return
userApiId = id
await loadApi(id)
}

View File

@@ -0,0 +1,88 @@
const { BrowserWindow } = require('electron')
const fs = require('fs')
const path = require('path')
// eslint-disable-next-line no-undef
const dir = global.isDev ? __userApi : path.join(__dirname, 'userApi')
const wait = time => new Promise(resolve => setTimeout(() => resolve(), time))
let html = ''
fs.readFile(path.join(dir, 'renderer/user-api.html'), 'utf8', (err, data) => {
if (err) throw new Error('api html read failed, info: ' + err.message)
html = data
})
const denyEvents = [
'new-window',
'will-navigate',
'will-redirect',
'will-attach-webview',
'will-prevent-unload',
'media-started-playing',
]
const winEvent = win => {
win.on('closed', () => {
win = global.modules.userApiWindow = null
})
}
exports.createWindow = async userApi => {
if (global.modules.userApiWindow) return
while (true) {
if (html) break
await wait(100)
}
/**
* Initial window options
*/
global.modules.userApiWindow = new BrowserWindow({
enableRemoteModule: false,
resizable: false,
minimizable: false,
maximizable: false,
fullscreenable: false,
show: false,
webPreferences: {
contextIsolation: true,
worldSafeExecuteJavaScript: true,
nodeIntegration: false,
preload: path.join(dir, 'renderer/preload.js'),
},
})
for (const eventName of denyEvents) {
global.modules.userApiWindow.webContents.on(eventName, event => {
event.preventDefault()
})
}
global.modules.userApiWindow.webContents.session.setPermissionRequestHandler((webContents, permission, callback) => {
callback(false)
})
winEvent(global.modules.userApiWindow)
// console.log(html.replace('</body>', `<script>${userApi.script}</script></body>`))
const randomNum = Math.random().toString().substring(2, 10)
global.modules.userApiWindow.loadURL(
'data:text/html;charset=UTF-8,' + encodeURIComponent(html
.replace('<meta http-equiv="Content-Security-Policy" content="default-src \'none\'">',
`<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'nonce-${randomNum}';">`)
.replace('</body>', `<script nonce="${randomNum}">${userApi.script}</script></body>`)))
// global.modules.userApiWindow.loadFile(path.join(dir, 'renderer/user-api.html'))
// global.modules.userApiWindow.webContents.openDevTools()
}
exports.closeWindow = async() => {
if (!global.modules.userApiWindow) return
await Promise.all([
global.modules.userApiWindow.webContents.session.clearAuthCache(),
global.modules.userApiWindow.webContents.session.clearStorageData(),
global.modules.userApiWindow.webContents.session.clearCache(),
])
global.modules.userApiWindow.destroy()
global.modules.userApiWindow = null
}

View File

@@ -0,0 +1,227 @@
const { contextBridge, ipcRenderer } = require('electron')
const needle = require('needle')
const { createCipheriv, publicEncrypt, constants, randomBytes, createHash } = require('crypto')
const USER_API_RENDERER_EVENT_NAME = require('../rendererEvent/name')
const sendMessage = (action, status, data, message) => {
ipcRenderer.send(action, { status, data, message })
}
let isInitedApi = false
const EVENT_NAMES = {
request: 'request',
inited: 'inited',
}
const eventNames = Object.values(EVENT_NAMES)
const events = {
request: null,
}
const allSources = ['kw', 'kg', 'tx', 'wy', 'mg']
const supportQualitys = {
kw: ['128k', '320k', 'flac'],
kg: ['128k', '320k', 'flac'],
tx: ['128k', '320k', 'flac'],
wy: ['128k', '320k', 'flac'],
mg: ['128k', '320k', 'flac'],
}
const supportActions = {
kw: ['musicUrl'],
kg: ['musicUrl'],
tx: ['musicUrl'],
wy: ['musicUrl'],
mg: ['musicUrl'],
xm: ['musicUrl'],
}
const handleRequest = (context, { requestKey, data }) => {
// console.log(data)
if (!events.request) return sendMessage(USER_API_RENDERER_EVENT_NAME.response, false, { requestKey }, 'Request event is not defined')
try {
events.request.call(context, { source: data.source, action: data.action, info: data.info }).then(response => {
let sendData = {
requestKey,
}
switch (data.action) {
case 'musicUrl':
sendData.result = {
source: data.source,
action: data.action,
data: {
type: data.info.type,
url: response,
},
}
break
}
sendMessage(USER_API_RENDERER_EVENT_NAME.response, true, sendData)
}).catch(err => {
sendMessage(USER_API_RENDERER_EVENT_NAME.response, false, { requestKey }, err.message)
})
} catch (err) {
sendMessage(USER_API_RENDERER_EVENT_NAME.response, false, { requestKey }, err.message)
}
}
/**
*
* @param {*} context
* @param {*} info {
* status: true,
* message: 'xxx',
* sources: {
* kw: ['128k', '320k', 'flac'],
* kg: ['128k', '320k', 'flac'],
* tx: ['128k', '320k', 'flac'],
* wy: ['128k', '320k', 'flac'],
* mg: ['128k', '320k', 'flac'],
* }
* }
*/
const handleInit = (context, info) => {
if (!info) {
sendMessage(USER_API_RENDERER_EVENT_NAME.init, false, null, 'Init failed')
// sendMessage(USER_API_RENDERER_EVENT_NAME.init, false, null, typeof info.message === 'string' ? info.message.substring(0, 100) : '')
return
}
if (info.openDevTools === true) {
sendMessage(USER_API_RENDERER_EVENT_NAME.openDevTools)
}
if (!info.status) {
sendMessage(USER_API_RENDERER_EVENT_NAME.init, false, null, 'Init failed')
// sendMessage(USER_API_RENDERER_EVENT_NAME.init, false, null, typeof info.message === 'string' ? info.message.substring(0, 100) : '')
return
}
const sourceInfo = {
sources: {},
}
try {
for (const source of allSources) {
const userSource = info.sources[source]
if (!userSource || userSource.type !== 'music') continue
const qualitys = supportQualitys[source]
const actions = supportActions[source]
sourceInfo.sources[source] = {
type: 'music',
actions: actions.filter(a => userSource.actions.includes(a)),
qualitys: qualitys.filter(q => userSource.qualitys.includes(q)),
}
}
} catch (error) {
console.log(error)
sendMessage(USER_API_RENDERER_EVENT_NAME.init, false, null, error.message)
return
}
sendMessage(USER_API_RENDERER_EVENT_NAME.init, true, sourceInfo)
ipcRenderer.on(USER_API_RENDERER_EVENT_NAME.request, (event, data) => {
handleRequest(context, data)
})
}
contextBridge.exposeInMainWorld('lx', {
EVENT_NAMES,
request(url, { method = 'get', timeout, headers, body, form, formData }, callback) {
let options = { headers }
let data
if (body) {
data = body
} else if (form) {
data = form
// data.content_type = 'application/x-www-form-urlencoded'
options.json = false
} else if (formData) {
data = formData
// data.content_type = 'multipart/form-data'
options.json = false
}
options.response_timeout = timeout
let request = needle.request(method, url, data, options, (err, resp, body) => {
if (!err) {
body = resp.body = resp.raw.toString()
try {
resp.body = JSON.parse(resp.body)
} catch (_) {}
body = resp.body
}
callback(err, {
statusCode: resp.statusCode,
statusMessage: resp.statusMessage,
headers: resp.headers,
bytes: resp.bytes,
raw: resp.raw,
body: body,
}, body)
}).request
return () => {
if (!request.aborted) request.abort()
request = null
}
},
send(eventName, data) {
return new Promise((resolve, reject) => {
if (!eventNames.includes(eventName)) return reject(new Error('The event is not supported: ' + eventName))
switch (eventName) {
case EVENT_NAMES.inited:
if (isInitedApi) return
isInitedApi = true
handleInit(this, data)
break
default:
resolve(new Error('Unknown event name: ' + eventName))
}
})
},
on(eventName, handler) {
if (!eventNames.includes(eventName)) return Promise.reject(new Error('The event is not supported: ' + eventName))
switch (eventName) {
case EVENT_NAMES.request:
events.request = handler
break
}
},
utils: {
crypto: {
aesEncrypt(buffer, mode, key, iv) {
const cipher = createCipheriv('aes-128-' + mode, key, iv)
return Buffer.concat([cipher.update(buffer), cipher.final()])
},
rsaEncrypt(buffer, key) {
buffer = Buffer.concat([Buffer.alloc(128 - buffer.length), buffer])
return publicEncrypt({ key: key, padding: constants.RSA_NO_PADDING }, buffer)
},
randomBytes(size) {
return randomBytes(size)
},
md5(str) {
return createHash('md5').update(str).digest('hex')
},
},
buffer: {
from(...args) {
return Buffer.from(...args)
},
},
},
// removeEvent(eventName, handler) {
// if (!eventNames.includes(eventName)) return Promise.reject(new Error('The event is not supported: ' + eventName))
// let handlers
// switch (eventName) {
// case EVENT_NAMES.request:
// handlers = events.request
// break
// }
// for (let index = 0; index < handlers.length; index++) {
// if (handlers[index] === handler) {
// handlers.splice(index, 1)
// break
// }
// }
// },
// removeAllEvents() {
// for (const handlers of Object.values(events)) {
// handlers.splice(0, handlers.length)
// }
// },
})

View File

@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
</body>
</html>

View File

@@ -0,0 +1,12 @@
const names = {
init: '',
request: '',
response: '',
openDevTools: '',
}
for (const key of Object.keys(names)) {
names[key] = `userApi_${key}`
}
module.exports = names

View File

@@ -0,0 +1,82 @@
const { mainOn, mainSend } = require('@common/ipc')
const USER_API_RENDERER_EVENT_NAME = require('../rendererEvent/name')
const { createWindow } = require('../main')
const { getUserApis } = require('../utils')
let userApi
let status = { status: true }
const requestQueue = new Map()
const timeouts = {}
const handleInit = (event, { status, message, data: apiInfo }) => {
// console.log('inited')
if (!status) {
console.log('init failed:', message)
global.lx_event.userApi.status(status = { status: false, apiInfo: userApi, message })
return
}
global.lx_event.userApi.status(status = { status: true, apiInfo: { ...userApi, sources: apiInfo.sources } })
}
const handleResponse = (event, { status, data: { requestKey, result }, message }) => {
const request = requestQueue.get(requestKey)
if (!request) return
requestQueue.delete(requestKey)
clearTimeout(timeouts[requestKey])
delete timeouts[requestKey]
if (status) {
request[0](result)
} else {
request[1](new Error(message))
}
}
const handleOpenDevTools = () => {
if (global.modules.userApiWindow) {
global.modules.userApiWindow.webContents.openDevTools()
}
}
mainOn(USER_API_RENDERER_EVENT_NAME.init, handleInit)
mainOn(USER_API_RENDERER_EVENT_NAME.response, handleResponse)
mainOn(USER_API_RENDERER_EVENT_NAME.openDevTools, handleOpenDevTools)
exports.loadApi = async apiId => {
if (!apiId) return global.lx_event.userApi.status(status = { status: false, message: 'api id is null' })
userApi = getUserApis().find(api => api.id == apiId)
console.log('load api', userApi.name)
await createWindow(userApi)
// if (!userApi) return global.lx_event.userApi.status(status = { status: false, message: 'api script is not found' })
// if (!global.modules.userApiWindow) {
// global.lx_event.userApi.status(status = { status: false, message: 'user api runtime is not defined' })
// throw new Error('user api window is not defined')
// }
// // const path = require('path')
// // // eslint-disable-next-line no-undef
// // userApi.script = require('fs').readFileSync(path.join(global.isDev ? __userApi : __dirname, 'renderer/test-api.js')).toString()
// console.log('load api', userApi.name)
// mainSend(global.modules.userApiWindow, USER_API_RENDERER_EVENT_NAME.init, { userApi })
}
exports.cancelRequest = requestKey => {
if (!requestQueue.has(requestKey)) return
const request = requestQueue.get(requestKey)
request[1](new Error('Cancel request'))
requestQueue.delete(requestKey)
clearTimeout(timeouts[requestKey])
delete timeouts[requestKey]
}
exports.request = ({ requestKey, data }) => new Promise((resolve, reject) => {
if (!userApi) return reject(new Error('user api is not load'))
// const requestKey = `request__${Math.random().toString().substring(2)}`
timeouts[requestKey] = setTimeout(() => {
exports.cancelRequest(requestKey)
}, 20000)
requestQueue.set(requestKey, [resolve, reject, data])
mainSend(global.modules.userApiWindow, USER_API_RENDERER_EVENT_NAME.request, { requestKey, data })
})
exports.getStatus = () => status

View File

@@ -0,0 +1,46 @@
const { userApis: defaultUserApis } = require('../config')
const Store = require('electron-store')
let userApis
const electronStore_userApi = new Store({
name: 'userApi',
})
exports.getUserApis = () => {
if (userApis) return userApis
userApis = electronStore_userApi.get('userApis')
if (!userApis) {
userApis = defaultUserApis
electronStore_userApi.set('userApis', userApis)
}
return userApis
}
exports.importApi = script => {
let scriptInfo = script.split(/\r?\n/)
let name = scriptInfo[1] || ''
let description = scriptInfo[2] || ''
name = name.startsWith(' * @name ') ? name.replace(' * @name ', '').trim() : `user_api_${new Date().toLocaleString()}`
if (name.length > 10) name = name.substring(0, 10) + '...'
description = description.startsWith(' * @description ') ? description.replace(' * @description ', '').trim() : ''
if (description.length > 20) description = description.substring(0, 20) + '...'
const apiInfo = {
id: `user_api_${Math.random().toString().substring(2, 5)}_${Date.now()}`,
name,
description,
script,
}
userApis.push(apiInfo)
electronStore_userApi.set('userApis', userApis)
return apiInfo
}
exports.removeApi = ids => {
for (let index = userApis.length - 1; index > -1; index--) {
if (ids.includes(userApis[index].id)) {
userApis.splice(index, 1)
ids.splice(index, 1)
}
}
electronStore_userApi.set('userApis', userApis)
}

View File

@@ -14,6 +14,7 @@ const setLrcConfig = () => {
config: desktopLyric,
languageId: global.appSetting.langId,
isShowLyricTransition: global.appSetting.player.isShowLyricTransition,
isPlayLxlrc: global.appSetting.player.isPlayLxlrc,
})
if (isLock != desktopLyric.isLock) {
isLock = desktopLyric.isLock

View File

@@ -24,7 +24,12 @@ mainOn(ipcWinLyricNames.set_lyric_config, (event, config) => {
})
mainHandle(ipcWinLyricNames.get_lyric_config, async() => {
return { config: global.appSetting.desktopLyric, languageId: global.appSetting.langId, isShowLyricTransition: global.appSetting.player.isShowLyricTransition }
return {
config: global.appSetting.desktopLyric,
languageId: global.appSetting.langId,
isShowLyricTransition: global.appSetting.player.isShowLyricTransition,
isPlayLxlrc: global.appSetting.player.isPlayLxlrc,
}
})
mainOn(ipcWinLyricNames.set_win_bounds, (event, options) => {

View File

@@ -18,6 +18,6 @@ require('./showDialog')
require('./playList')
require('./data')
require('./xm_verify')
require('./kw_decodeLyric')
require('./userApi')

View File

@@ -3,6 +3,7 @@ const { mainOn, NAMES: { mainWindow: ipcMainWindowNames }, mainHandle } = requir
let electronStore_list
let electronStore_downloadList
mainHandle(ipcMainWindowNames.get_playlist, async(event, isIgnoredError = false) => {
if (!electronStore_list) {
@@ -11,13 +12,35 @@ mainHandle(ipcMainWindowNames.get_playlist, async(event, isIgnoredError = false)
clearInvalidConfig: !isIgnoredError,
})
}
if (!electronStore_downloadList) {
electronStore_downloadList = new Store({
name: 'downloadList',
})
}
return {
defaultList: electronStore_list.get('defaultList'),
loveList: electronStore_list.get('loveList'),
userList: electronStore_list.get('userList'),
downloadList: electronStore_list.get('downloadList'),
downloadList: electronStore_downloadList.get('list'),
}
})
mainOn(ipcMainWindowNames.save_playlist, (event, { type, data }) => electronStore_list && electronStore_list.set(type, data))
const handleSaveList = ({ defaultList, loveList, userList }) => {
if (!electronStore_list) return
let data = {}
if (defaultList != null) data.defaultList = defaultList
if (loveList != null) data.loveList = loveList
if (userList != null) data.userList = userList
electronStore_list.set(data)
}
mainOn(ipcMainWindowNames.save_playlist, (event, { type, data }) => {
switch (type) {
case 'myList':
handleSaveList(data)
break
case 'downloadList':
electronStore_downloadList && electronStore_downloadList.set('list', data)
break
}
})

View File

@@ -0,0 +1,35 @@
const { mainSend, mainHandle, NAMES: { mainWindow: ipcMainWindowNames } } = require('@common/ipc')
const { getApiList, importApi, removeApi, setApi, getStatus, request, cancelRequest, eventNames } = require('../modules/userApi')
const handleStatusChange = status => {
mainSend(global.modules.mainWindow, ipcMainWindowNames.user_api_status, status)
}
global.lx_event.userApi.on(eventNames.status, handleStatusChange)
mainHandle(ipcMainWindowNames.import_user_api, async(event, script) => {
return importApi(script)
})
mainHandle(ipcMainWindowNames.remove_user_api, (event, apiIds) => {
return removeApi(apiIds)
})
mainHandle(ipcMainWindowNames.set_user_api, (event, apiId) => {
return setApi(apiId)
})
mainHandle(ipcMainWindowNames.get_user_api_list, event => {
return getApiList()
})
mainHandle(ipcMainWindowNames.get_user_api_status, event => {
return getStatus()
})
mainHandle(ipcMainWindowNames.request_user_api, (event, musicInfo) => {
return request(musicInfo)
})
mainHandle(ipcMainWindowNames.request_user_api_cancel, (event, requestKey) => {
return cancelRequest(requestKey)
})

View File

@@ -1,4 +0,0 @@
const { isMac } = require('../../../common/utils')
// mac下的 BrowserView 无法拖动验证栏,改用 BrowserWindow
require(isMac ? './xm_verify_win' : './xm_verify_view')

View File

@@ -1,4 +0,0 @@
const { isMac } = require('../../../common/utils')
// mac下的 BrowserView 无法拖动验证栏,改用 BrowserWindow
require(isMac ? './xm_verify_win' : './xm_verify_view')

View File

@@ -1,66 +0,0 @@
const { BrowserView } = require('electron')
const { mainHandle, NAMES: { mainWindow: ipcMainWindowNames } } = require('../../../common/ipc')
const { getWindowSizeInfo } = require('../../utils')
let view
let isActioned = false
let rejectFn
const closeView = async() => {
if (!view) return
// await view.webContents.session.clearCache()
if (global.modules.mainWindow) global.modules.mainWindow.removeBrowserView(view)
await view.webContents.session.clearStorageData()
view.destroy()
view = null
}
mainHandle(ipcMainWindowNames.handle_xm_verify_open, (event, url) => new Promise((resolve, reject) => {
if (!global.modules.mainWindow) return reject(new Error('mainWindow is undefined'))
if (view) {
global.modules.mainWindow.removeBrowserView(view)
view.destroy()
}
rejectFn = reject
isActioned = false
view = new BrowserView({
webPreferences: {
enableRemoteModule: false,
disableHtmlFullscreenWindowResize: true,
},
})
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]) => {
isActioned = true
await closeView()
if (!x5sec) return reject(new Error('get x5sec failed'))
resolve(x5sec.value)
}).catch(async err => {
isActioned = true
await closeView()
reject(err)
})
})
global.modules.mainWindow.setBrowserView(view)
const windowSizeInfo = getWindowSizeInfo(global.appSetting)
view.setBounds({ x: (windowSizeInfo.width - 380) / 2, y: ((windowSizeInfo.height - 320 + 52) / 2), width: 380, height: 320 })
view.webContents.loadURL(url, {
httpReferrer: 'https://www.xiami.com/',
})
// view.webContents.openDevTools()
}))
mainHandle(ipcMainWindowNames.handle_xm_verify_close, async() => {
await closeView()
if (!rejectFn) return
if (!isActioned) rejectFn(new Error('canceled verify'))
rejectFn = null
})

View File

@@ -1,77 +0,0 @@
const { BrowserWindow } = require('electron')
const { mainHandle, NAMES: { mainWindow: ipcMainWindowNames } } = require('../../../common/ipc')
const { getWindowSizeInfo } = require('../../utils')
let win
const closeWin = async() => {
if (!win) return
// await win.webContents.session.clearCache()
// if (global.modules.mainWindow) global.modules.mainWindow.removeBrowserView(win)
if (win.isDestroyed()) {
win = null
return
}
await win.webContents.session.clearStorageData()
win.destroy()
win = null
}
mainHandle(ipcMainWindowNames.handle_xm_verify_open, (event, url) => new Promise((resolve, reject) => {
if (!global.modules.mainWindow) return reject(new Error('mainWindow is undefined'))
if (win) win.destroy()
let isActioned = false
const mainWindowSizeInfo = global.modules.mainWindow.getBounds()
const windowSizeInfo = getWindowSizeInfo(global.appSetting)
win = new BrowserWindow({
parent: global.modules.mainWindow,
width: 460,
height: 370,
resizable: false,
// transparent: true,
x: mainWindowSizeInfo.x + (windowSizeInfo.width - 460) / 2,
y: mainWindowSizeInfo.y + (windowSizeInfo.height - 320 + 52) / 2,
minimizable: false,
maximizable: false,
// movable: false,
// frame: false,
// modal: true,
webPreferences: {
enableRemoteModule: false,
disableHtmlFullscreenWindowResize: true,
},
})
win.webContents.on('did-finish-load', () => {
if (/punish\?/.test(win.webContents.getURL())) return
let ses = win.webContents.session
ses.cookies.get({ name: 'x5sec' })
.then(async([x5sec]) => {
isActioned = true
await closeWin()
if (!x5sec) return reject(new Error('get x5sec failed'))
resolve(x5sec.value)
}).catch(async err => {
isActioned = true
await closeWin()
reject(err)
})
})
win.webContents.loadURL(url, {
httpReferrer: 'https://www.xiami.com/',
})
win.on('closed', async() => {
await closeWin()
if (isActioned) return
reject(new Error('canceled verify'))
})
// win.webContents.openDevTools()
}))
mainHandle(ipcMainWindowNames.handle_xm_verify_close, async() => {
await closeWin()
})

View File

@@ -4,7 +4,7 @@
transition(enter-active-class="animated-fast fadeIn" leave-active-class="animated-fast fadeOut")
.control-bar(v-show="!lrcConfig.isLock")
core-control-bar(:lrcConfig="lrcConfig" :themes="themeList")
core-lyric(:lrcConfig="lrcConfig" :isShowLyricTransition="isShowLyricTransition")
core-lyric(:lrcConfig="lrcConfig" :isPlayLxlrc="isPlayLxlrc" :isShowLyricTransition="isShowLyricTransition")
div.resize-left(@mousedown.self="handleMouseDown('left', $event)")
div.resize-top(@mousedown.self="handleMouseDown('top', $event)")
div.resize-right(@mousedown.self="handleMouseDown('right', $event)")
@@ -45,6 +45,7 @@ export default {
},
},
isShowLyricTransition: true,
isPlayLxlrc: true,
themeList: [
{
id: 0,
@@ -118,9 +119,10 @@ export default {
document.removeEventListener('mouseup', this.handleMouseUp)
},
methods: {
handleUpdateConfig({ config, languageId, isShowLyricTransition }) {
handleUpdateConfig({ config, languageId, isShowLyricTransition, isPlayLxlrc }) {
this.lrcConfig = config
this.isShowLyricTransition = isShowLyricTransition
this.isPlayLxlrc = isPlayLxlrc
if (this.$i18n.locale !== languageId && languageId != null) this.$i18n.locale = languageId
},
handleMouseDown(origin, event) {

View File

@@ -1,7 +1,8 @@
<template lang="pug">
div(:class="[$style.lyric, lyricEvent.isMsDown ? $style.draging : null]" :style="lrcStyles" @wheel="handleWheel" @mousedown="handleLyricMouseDown" ref="dom_lyric")
div(:class="[$style.lyric, { [$style.draging]: lyricEvent.isMsDown }, { [$style.lrcActiveZoom]: lrcConfig.style.isZoomActiveLrc } ]" :style="lrcStyles" @wheel="handleWheel" @mousedown="handleLyricMouseDown" ref="dom_lyric")
div(:class="$style.lyricSpace")
div(v-for="(info, index) in lyricLines" :key="index" :class="[$style.lineContent, lyric.line == index ? (lrcConfig.style.isZoomActiveLrc ? $style.lrcActiveZoom : $style.lrcActive) : null]")
div(:class="[$style.lyricText]" ref="dom_lyric_text")
//- div(v-for="(info, index) in lyricLines" :key="index" :class="[$style.lineContent, lyric.line == index ? (lrcConfig.style.isZoomActiveLrc ? $style.lrcActiveZoom : $style.lrcActive) : null]")
p(:class="$style.lrcLine") {{info.text}}
div(:class="$style.lyricSpace")
</template>
@@ -9,7 +10,7 @@ div(:class="[$style.lyric, lyricEvent.isMsDown ? $style.draging : null]" :style=
<script>
import { rendererOn, rendererSend, NAMES } from '../../../common/ipc'
import { scrollTo } from '../../../renderer/utils'
import Lyric from 'lrc-file-parser'
import Lyric from '@renderer/utils/lyric-font-player'
let cancelScrollFn = null
@@ -27,6 +28,10 @@ export default {
}
},
},
isPlayLxlrc: {
type: Boolean,
default: true,
},
isShowLyricTransition: {
type: Boolean,
default: true,
@@ -65,6 +70,7 @@ export default {
lyrics: {
lyric: '',
tlyric: '',
lxlyric: '',
},
}
},
@@ -85,7 +91,7 @@ export default {
if (n.length) {
this.lyricLines = n
this.$nextTick(() => {
this.dom_lines = this.$refs.dom_lyric.querySelectorAll('p')
this.dom_lines = this.$refs.dom_lyric.querySelectorAll('.lrc-content')
this.handleScrollLrc()
})
} else {
@@ -97,7 +103,7 @@ export default {
if (this.lyricLines === this._lyricLines && this._lyricLines.length) return
this.lyricLines = this._lyricLines
this.$nextTick(() => {
this.dom_lines = this.$refs.dom_lyric.querySelectorAll('p')
this.dom_lines = this.$refs.dom_lyric.querySelectorAll('.lrc-content')
this.handleScrollLrc()
})
}, 50)
@@ -105,7 +111,7 @@ export default {
} else {
this.lyricLines = n
this.$nextTick(() => {
this.dom_lines = this.$refs.dom_lyric.querySelectorAll('p')
this.dom_lines = this.$refs.dom_lyric.querySelectorAll('.lrc-content')
this.handleScrollLrc()
})
}
@@ -120,8 +126,11 @@ export default {
},
immediate: true,
},
isShowLyricTransition(n) {
console.log(n)
isShowLyricTransition() {
this.setLyric()
rendererSend(NAMES.winLyric.get_lyric_info, 'status')
},
isPlayLxlrc() {
this.setLyric()
rendererSend(NAMES.winLyric.get_lyric_info, 'status')
},
@@ -129,6 +138,11 @@ export default {
created() {
rendererOn(NAMES.winLyric.set_lyric_info, (event, data) => this.handleSetInfo(data))
window.lrc = new Lyric({
lineClassName: 'lrc-content',
fontClassName: 'font',
shadowClassName: 'shadow',
shadowContent: true,
activeLineClassName: 'active',
onPlay: (line, text) => {
this.lyric.text = text
this.lyric.line = line
@@ -136,6 +150,12 @@ export default {
},
onSetLyric: lines => { // listening lyrics seting event
// console.log(lines) // lines is array of all lyric text
this.$refs.dom_lyric_text.textContent = ''
const dom_lines = document.createDocumentFragment()
for (const line of lines) {
dom_lines.appendChild(line.dom_line)
}
this.$refs.dom_lyric_text.appendChild(dom_lines)
this.lyric.lines = lines
this.lyric.line = 0
},
@@ -159,6 +179,7 @@ export default {
case 'lyric':
this.lyrics.lyric = data.lrc
this.lyrics.tlyric = data.tlrc
this.lyrics.lxlyric = data.lxlrc
this.setLyric()
break
case 'play':
@@ -171,8 +192,9 @@ export default {
break
case 'info':
// console.log('info', data)
this.lyrics.lyric = data.lyric
this.lyrics.tlyric = data.tlyric
this.lyrics.lyric = data.lrc
this.lyrics.tlyric = data.tlrc
this.lyrics.lxlyric = data.lxlrc
this.setLyric()
this.$nextTick(() => {
this.lyric.line = data.line
@@ -211,7 +233,10 @@ export default {
cancelScrollFn = scrollTo(this.$refs.dom_lyric, dom_p ? (dom_p.offsetTop - this.$refs.dom_lyric.clientHeight * 0.5 + dom_p.clientHeight / 2) : 0)
},
handleLyricMouseDown(e) {
if (e.target.classList.contains(this.$style.lrcLine)) {
if (e.target.classList.contains('font') ||
e.target.parentNode.classList.contains('font') ||
e.target.classList.contains('translation') ||
e.target.parentNode.classList.contains('translation')) {
this.lyricEvent.isMsDown = true
this.lyricEvent.msDownY = e.clientY
this.lyricEvent.msDownScrollY = this.$refs.dom_lyric.scrollTop
@@ -279,7 +304,11 @@ export default {
rendererSend(NAMES.winLyric.close)
},
setLyric() {
window.lrc.setLyric((this.isShowLyricTransition && this.lyrics.tlyric ? (this.lyrics.tlyric + '\n') : '') + (this.lyrics.lyric || ''))
window.lrc.setLyric(
this.isPlayLxlrc && this.lyrics.lxlyric ? this.lyrics.lxlyric : this.lyrics.lyric,
this.isShowLyricTransition && this.lyrics.tlyric ? this.lyrics.tlyric : '',
// (this.isShowLyricTransition && this.lyrics.tlyric ? (this.lyrics.tlyric + '\n') : '') + (this.lyrics.lyric || ''),
)
},
},
}
@@ -292,34 +321,93 @@ export default {
text-align: center;
height: 100%;
overflow: hidden;
font-size: 68px;
padding: 0 5px;
opacity: .6;
transition: opacity @transition-theme;
&.draging {
.lrc-line {
cursor: grabbing;
font-size: 16px;
color: @color-theme-lyric;
cursor: move;
:global {
.lrc-content {
line-height: 1.2;
margin: 16px 0;
overflow-wrap: break-word;
.font {
display: inline-block;
}
.font, .translation {
cursor: grab;
}
.translation {
transition: @transition-theme !important;
transition-property: font-size, color;
font-size: 0.8em;
margin-top: 5px;
}
.line {
transition-property: font-size, color !important;
background: none !important;
-webkit-text-fill-color: unset;
// -webkit-text-fill-color: none !important;
}
&.active {
.line {
color: @color-theme;
}
.translation {
color: @color-theme;
}
// span {
// color: @color-theme;
// }
}
span {
transition: @transition-theme !important;
transition-property: font-size !important;
font-size: 1em;
background-repeat: no-repeat;
background-color: @color-theme-lyric;
background-image: -webkit-linear-gradient(top, @color-theme, @color-theme);
-webkit-text-fill-color: transparent;
-webkit-background-clip: text;
background-size: 0 100%;
}
}
.shadow {
text-shadow: 1px 1px 2px rgb(32, 32, 32);
}
}
// p {
// padding: 8px 0;
// line-height: 1.2;
// overflow-wrap: break-word;
// transition: @transition-theme !important;
// transition-property: color, font-size;
// }
}
.lrc-line {
display: inline-block;
padding: 8px 0;
line-height: 1.2;
overflow-wrap: break-word;
transition: @transition-theme;
transition-property: color, font-size, text-shadow;
cursor: grab;
// font-weight: bold;
// background-clip: text;
color: @color-theme-lyric;
text-shadow: 1px 1px 2px #000;
// background: linear-gradient(@color-theme-lyric, @color-theme-lyric);
// background-clip: text;
// -webkit-background-clip: text;
// -webkit-text-fill-color: #fff;
// -webkit-text-stroke: thin #124628;
}
// .lrc-line {
// display: inline-block;
// padding: 8px 0;
// line-height: 1.2;
// overflow-wrap: break-word;
// transition: @transition-theme;
// transition-property: color, font-size, text-shadow;
// cursor: grab;
// // font-weight: bold;
// // background-clip: text;
// color: @color-theme-lyric;
// text-shadow: 1px 1px 2px #000;
// // background: linear-gradient(@color-theme-lyric, @color-theme-lyric);
// // background-clip: text;
// // -webkit-background-clip: text;
// // -webkit-text-fill-color: #fff;
// // -webkit-text-stroke: thin #124628;
// }
.lyric-space {
height: 70%;
}
@@ -334,9 +422,28 @@ export default {
// -webkit-text-stroke: thin #124628;
}
}
.draging {
:global {
.lrc-content {
.font, .translation {
cursor: grabbing;
}
}
}
}
.lrc-active-zoom {
.lrc-active;
font-size: 1.2em;
:global {
.lrc-content {
&.active {
.translation {
font-size: 1em;
}
span {
font-size: 1.2em;
}
}
}
}
}
.footer {
flex: 0 0 100px;
@@ -348,14 +455,30 @@ export default {
each(@themes, {
:global(#container.@{value}) {
.lrc-line {
color: ~'@{color-@{value}-theme-lyric}';
}
// .lrc-line {
// color: ~'@{color-@{value}-theme-lyric}';
// }
.lrc-active, .lrc-active-zoom {
.lrc-line {
color: ~'@{color-@{value}-theme-lyric_2}';
}
}
.lyric {
color: ~'@{color-@{value}-theme-lyric}';
:global {
.lrc-content {
&.active {
.line {
color: ~'@{color-@{value}-theme}';
}
}
span {
// background-color: ~'@{color-@{value}-theme_2-font}';
background-image: -webkit-linear-gradient(top, ~'@{color-@{value}-theme}', ~'@{color-@{value}-theme}');
}
}
}
}
}
})

View File

@@ -6,7 +6,6 @@
core-view#view
core-player#player
core-icons
material-xm-verify-modal(v-show="globalObj.xm.isShowVerify" :show="globalObj.xm.isShowVerify" :bg-close="false" @close="handleXMVerifyModalClose")
material-version-modal(v-show="version.showModal")
material-pact-modal(v-show="!setting.isAgreePact || globalObj.isShowPact")
#container(v-else :class="theme")
@@ -16,7 +15,6 @@
core-view#view
core-player#player
core-icons
material-xm-verify-modal(v-show="globalObj.xm.isShowVerify" :show="globalObj.xm.isShowVerify" :bg-close="false" @close="handleXMVerifyModalClose")
material-version-modal(v-show="version.showModal")
material-pact-modal(v-show="!setting.isAgreePact || globalObj.isShowPact")
</template>
@@ -27,8 +25,9 @@ import { mapMutations, mapGetters, mapActions } from 'vuex'
import { rendererOn, rendererSend, rendererInvoke, NAMES } from '../common/ipc'
import { isLinux } from '../common/utils'
import music from './utils/music'
import { throttle, openUrl, compareVer, getPlayList } from './utils'
import { throttle, openUrl, compareVer, getPlayList, parseUrlParams } from './utils'
import { base as eventBaseName } from './event/names'
import apiSourceInfo from './utils/music/api-source-info'
window.ELECTRON_DISABLE_SECURITY_WARNINGS = process.env.ELECTRON_DISABLE_SECURITY_WARNINGS
dnscache({
@@ -37,6 +36,20 @@ dnscache({
cachesize: 1000,
})
const listThrottle = (fn, delay = 100) => {
let timer = null
let _data = {}
return function(data) {
Object.assign(_data, data)
if (timer) return
timer = setTimeout(() => {
timer = null
fn.call(this, _data)
_data = {}
}, delay)
}
}
export default {
data() {
return {
@@ -44,12 +57,15 @@ export default {
isDT: false,
isLinux,
globalObj: {
apiSource: 'test',
apiSource: null,
proxy: {},
isShowPact: false,
qualityList: {},
xm: {
isShowVerify: false,
userApi: {
list: [],
status: false,
message: 'initing',
apis: {},
},
},
updateTimeout: null,
@@ -70,24 +86,12 @@ export default {
}),
},
created() {
this.saveDefaultList = throttle(n => {
this.saveMyList = listThrottle(data => {
rendererSend(NAMES.mainWindow.save_playlist, {
type: 'defaultList',
data: n,
type: 'myList',
data,
})
}, 500)
this.saveLoveList = throttle(n => {
rendererSend(NAMES.mainWindow.save_playlist, {
type: 'loveList',
data: n,
})
}, 500)
this.saveUserList = throttle(n => {
rendererSend(NAMES.mainWindow.save_playlist, {
type: 'userList',
data: n,
})
}, 500)
}, 300)
this.saveDownloadList = throttle(n => {
rendererSend(NAMES.mainWindow.save_playlist, {
type: 'downloadList',
@@ -115,19 +119,19 @@ export default {
},
defaultList: {
handler(n) {
this.saveDefaultList(n)
this.saveMyList({ defaultList: n })
},
deep: true,
},
loveList: {
handler(n) {
this.saveLoveList(n)
this.saveMyList({ loveList: n })
},
deep: true,
},
userList: {
handler(n) {
this.saveUserList(n)
this.saveMyList({ userList: n })
},
deep: true,
},
@@ -140,8 +144,20 @@ export default {
searchHistoryList(n) {
this.saveSearchHistoryList(n)
},
'globalObj.apiSource'(n) {
this.globalObj.qualityList = music.supportQuality[n]
'globalObj.apiSource'(n, o) {
if (/^user_api/.test(n)) {
this.globalObj.qualityList = {}
this.globalObj.userApi.status = false
this.globalObj.userApi.message = 'initing'
} else {
this.globalObj.qualityList = music.supportQuality[n]
}
if (o === null) return
rendererInvoke(NAMES.mainWindow.set_user_api, n).catch(err => {
console.log(err)
let api = apiSourceInfo.find(api => !api.disabled)
if (api) this.globalObj.apiSource = api.id
})
if (n != this.setting.apiSource) {
this.setSetting(Object.assign({}, this.setting, {
apiSource: n,
@@ -177,10 +193,12 @@ export default {
...mapMutations('player', {
setPlayList: 'setList',
}),
...mapActions('songList', ['getListDetailAll']),
init() {
document.documentElement.style.fontSize = this.windowSizeActive.fontSize
rendererInvoke(NAMES.mainWindow.get_env_params).then(this.handleEnvParamsInit)
const asyncTask = []
asyncTask.push(rendererInvoke(NAMES.mainWindow.get_env_params).then(this.handleEnvParamsInit))
document.body.addEventListener('click', this.handleBodyClick, true)
rendererOn(NAMES.mainWindow.update_available, (e, info) => {
@@ -244,14 +262,23 @@ export default {
}, 60 * 30 * 1000)
this.listenEvent()
this.initData()
asyncTask.push(this.initData())
asyncTask.push(this.initUserApi())
this.globalObj.apiSource = this.setting.apiSource
this.globalObj.qualityList = music.supportQuality[this.setting.apiSource]
if (/^user_api/.test(this.setting.apiSource)) {
rendererInvoke(NAMES.mainWindow.set_user_api, this.setting.apiSource)
} else {
this.globalObj.qualityList = music.supportQuality[this.setting.apiSource]
}
this.globalObj.proxy = Object.assign({}, this.setting.network.proxy)
window.globalObj = this.globalObj
// 初始化音乐sdk
music.init()
asyncTask.push(music.init())
Promise.all(asyncTask).then(() => {
this.handleInitEnvParamSearch()
this.handleInitEnvParamPlay()
})
},
enableIgnoreMouseEvents() {
if (this.isDT) return
@@ -265,12 +292,14 @@ export default {
},
initData() { // 初始化数据
this.initLocalList() // 初始化播放列表
return Promise.all([
this.initMyList(), // 初始化播放列表
this.initSearchHistoryList(), // 初始化搜索历史列表
])
// this.initDownloadList() // 初始化下载列表
this.initSearchHistoryList() // 初始化搜索历史列表
},
initLocalList() {
getPlayList().then(({ defaultList, loveList, userList, downloadList }) => {
initMyList() {
return getPlayList().then(({ defaultList, loveList, userList, downloadList }) => {
if (!defaultList) defaultList = this.defaultList
if (!loveList) loveList = this.loveList
if (userList) {
@@ -340,6 +369,72 @@ export default {
})
})
},
initUserApi() {
return Promise.all([
rendererOn(NAMES.mainWindow.user_api_status, (event, { status, message, apiInfo }) => {
// console.log(apiInfo)
this.globalObj.userApi.status = status
this.globalObj.userApi.message = message
if (status) {
if (apiInfo.id === this.setting.apiSource) {
let apis = {}
let qualitys = {}
for (const [source, { actions, type, qualitys: sourceQualitys }] of Object.entries(apiInfo.sources)) {
if (type != 'music') continue
apis[source] = {}
for (const action of actions) {
switch (action) {
case 'musicUrl':
apis[source].getMusicUrl = (songInfo, type) => {
const requestKey = `request__${Math.random().toString().substring(2)}`
return {
canceleFn() {
rendererInvoke(NAMES.mainWindow.request_user_api_cancel, requestKey)
},
promise: rendererInvoke(NAMES.mainWindow.request_user_api, {
requestKey,
data: {
source: source,
action: 'musicUrl',
info: {
type,
musicInfo: songInfo,
},
},
}).then(res => {
// console.log(res)
if (!/^https?:/.test(res.data.url)) return Promise.reject(new Error('Get url failed'))
return { type, url: res.data.url }
}).catch(err => {
console.log(err.message)
return Promise.reject(err)
}),
}
}
break
default:
break
}
}
qualitys[source] = sourceQualitys
}
this.globalObj.qualityList = qualitys
this.globalObj.userApi.apis = apis
}
}
}),
rendererInvoke(NAMES.mainWindow.get_user_api_list).then(res => {
// console.log(res)
if (![...apiSourceInfo.map(s => s.id), ...res.map(s => s.id)].includes(this.setting.apiSource)) {
console.warn('reset api')
let api = apiSourceInfo.find(api => !api.disabled)
if (api) this.globalObj.apiSource = api.id
}
this.globalObj.userApi.list = res
}),
])
},
showUpdateModal() {
(this.version.newVersion && this.version.newVersion.history
? Promise.resolve(this.version.newVersion)
@@ -389,18 +484,91 @@ export default {
document.body.addEventListener('mouseenter', this.dieableIgnoreMouseEvents)
document.body.addEventListener('mouseleave', this.enableIgnoreMouseEvents)
}
this.handleInitEnvParamSearch()
this.handleInitEnvParamPlay()
},
// 处理启动参数 search
handleInitEnvParamSearch() {
if (this.envParams.search == null) return
this.$router.push({
path: 'search',
query: {
text: this.envParams.search,
},
})
},
// 处理启动参数 play
handleInitEnvParamPlay() {
if (this.envParams.play == null || typeof this.envParams.play != 'string') return
// -play="source=kw&link=链接、ID"
// -play="source=myList&name=名字"
// -play="source=myList&name=名字&index=位置"
const params = parseUrlParams(this.envParams.play)
if (params.type != 'songList') return
this.handlePlaySongList(params)
},
handlePlaySongList(params) {
switch (params.source) {
case 'myList':
if (params.name != null) {
let targetList
const lists = Object.values(window.allList)
for (const list of lists) {
if (list.name === params.name) {
targetList = list
break
}
}
if (!targetList) return
if (this.envParams.search != null) {
this.$router.push({
path: 'search',
query: {
text: this.envParams.search,
},
})
this.setPlayList({
list: {
list: targetList.list,
id: targetList.id,
},
index: this.getListPlayIndex(targetList.list, params.index),
})
}
break
case 'kw':
case 'kg':
case 'tx':
case 'mg':
case 'wy':
this.playSongListDetail(params.source, params.link, params.index)
break
}
},
handleXMVerifyModalClose() {
music.xm.closeVerifyModal()
async playSongListDetail(source, link, playIndex) {
if (link == null) return
let list
try {
list = await this.getListDetailAll({ source, id: decodeURIComponent(link) })
} catch (err) {
console.log(err)
}
this.setPlayList({
list: {
list,
id: null,
},
index: this.getListPlayIndex(list, playIndex),
})
},
getListPlayIndex(list, index) {
if (index == null) {
index = 1
} else {
index = parseInt(index)
if (Number.isNaN(index)) {
index = 1
} else {
if (index < 1) index = 1
else if (index > list.length) index = list.length
}
}
return index - 1
},
listenEvent() {
window.eventHub.$on('key_escape_down', this.handle_key_esc_down)
@@ -462,6 +630,10 @@ body {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
#view { // 偏移5px距离解决非透明模式下右侧滚动条无法拖动的问题
margin-right: 5Px;
}
}
#container {

View File

@@ -871,6 +871,41 @@
transform: scale(1);
}
}
@keyframes hinge {
0% {
-webkit-transform-origin: top left;
transform-origin: top left;
-webkit-animation-timing-function: ease-in-out;
animation-timing-function: ease-in-out;
}
20%,
60% {
-webkit-transform: rotate3d(0, 0, 1, 80deg);
transform: rotate3d(0, 0, 1, 80deg);
-webkit-transform-origin: top left;
transform-origin: top left;
-webkit-animation-timing-function: ease-in-out;
animation-timing-function: ease-in-out;
}
40%,
80% {
-webkit-transform: rotate3d(0, 0, 1, 60deg);
transform: rotate3d(0, 0, 1, 60deg);
-webkit-transform-origin: top left;
transform-origin: top left;
-webkit-animation-timing-function: ease-in-out;
animation-timing-function: ease-in-out;
opacity: 1;
}
to {
-webkit-transform: translate3d(0, 700px, 0);
transform: translate3d(0, 700px, 0);
opacity: 0;
}
}
.flipInX {
backface-visibility: visible !important;

View File

@@ -34,7 +34,7 @@ div(:class="$style.player")
div(:class="$style.titleBtn" @click='addMusicTo' :tips="$t('core.player.add_music_to')")
svg(version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' height='80%' viewBox='0 0 512 512' space='preserve')
use(xlink:href='#icon-add-2')
//- div(:class="$style.playBtn" @click='handleNext' tips="音量")
//- div(:class="$style.playBtn" @click='playNext' tips="音量")
svg(version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' height='100%' viewBox='0 0 291.063 291.064' space='preserve')
use(xlink:href='#icon-sound')
@@ -49,7 +49,7 @@ div(:class="$style.player")
span(style="margin: 0 5px;") /
span {{maxPlayTimeStr}}
div(:class="$style.right")
div(:class="$style.playBtn" @click='handlePrev' :tips="$t('core.player.prev')" style="transform: rotate(180deg);")
div(:class="$style.playBtn" @click='playPrev' :tips="$t('core.player.prev')" style="transform: rotate(180deg);")
svg(version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' height='100%' viewBox='0 0 220.847 220.847' space='preserve')
use(xlink:href='#icon-nextMusic')
div(:class="$style.playBtn" :tips="isPlay ? $t('core.player.pause') : $t('core.player.play')" @click='togglePlay')
@@ -57,7 +57,7 @@ div(:class="$style.player")
use(xlink:href='#icon-pause')
svg(v-else version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' height='100%' viewBox='0 0 170 170' space='preserve')
use(xlink:href='#icon-play')
div(:class="$style.playBtn" @click='handleNext' :tips="$t('core.player.next')")
div(:class="$style.playBtn" @click='playNext' :tips="$t('core.player.next')")
svg(version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' height='100%' viewBox='0 0 220.847 220.847' space='preserve')
use(xlink:href='#icon-nextMusic')
//- transition(enter-active-class="animated lightSpeedIn"
@@ -85,14 +85,12 @@ div(:class="$style.player")
</template>
<script>
import Lyric from 'lrc-file-parser'
import Lyric from '@renderer/utils/lyric-font-player'
import { rendererSend, rendererOn, NAMES } from '../../../common/ipc'
import { formatPlayTime2, getRandom, checkPath, setTitle, clipboardWriteText, debounce, throttle, assertApiSupport } from '../../utils'
import { mapGetters, mapActions, mapMutations } from 'vuex'
import { requestMsg } from '../../utils/message'
import { isMac } from '../../../common/utils'
import { player as eventPlayerNames } from '../../../common/hotKey'
import musicSdk from '@renderer/utils/music'
import path from 'path'
let audio
@@ -107,7 +105,6 @@ const playNextModes = [
export default {
data() {
return {
show: true,
volume: 0,
nowPlayTime: 0,
maxPlayTime: 0,
@@ -123,7 +120,6 @@ export default {
singer: '',
album: '',
},
targetSong: null,
pregessWidth: 0,
lyric: {
lines: [],
@@ -133,7 +129,6 @@ export default {
delayNextTimeout: null,
restorePlayTime: 0,
retryNum: 0,
isMac,
volumeEvent: {
isMsDown: false,
msDownX: 0,
@@ -149,10 +144,19 @@ export default {
},
computed: {
...mapGetters(['setting']),
...mapGetters('player', ['list', 'playIndex', 'changePlay', 'listId', 'isShowPlayerDetail', 'playedList']),
...mapGetters('player', ['list', 'changePlay', 'playMusicInfo', 'isShowPlayerDetail', 'playInfo', 'playedList']),
// pic() {
// return this.musicInfo.img ? this.musicInfo.img : ''
// },
listId() { // 当前播放歌曲的列表ID
return this.playInfo.listId
},
playIndex() { // 当前播放歌曲所在列表的 播放列表的播放位置
return this.playInfo.playIndex
},
targetSong() {
return this.playInfo.musicInfo
},
title() {
return this.musicInfo.name
? this.setting.download.fileName.replace('歌名', this.musicInfo.name).replace('歌手', this.musicInfo.singer)
@@ -211,8 +215,9 @@ export default {
singer: this.musicInfo.singer,
name: this.musicInfo.name,
album: this.musicInfo.album,
lyric: this.musicInfo.lrc,
tlyric: this.musicInfo.tlrc,
lrc: this.musicInfo.lrc,
tlrc: this.musicInfo.tlrc,
lxlrc: this.musicInfo.lxlrc,
isPlay: this.isPlay,
line: this.lyric.line,
played_time: audio.currentTime * 1000,
@@ -247,38 +252,19 @@ export default {
if (!n) return
this.resetChangePlay()
if (window.restorePlayInfo) {
let musicInfo = this.targetSong = this.list[window.restorePlayInfo.index]
this.musicInfo.songmid = musicInfo.songmid
this.musicInfo.singer = musicInfo.singer
this.musicInfo.name = musicInfo.name
this.musicInfo.album = musicInfo.albumName
this.setImg(musicInfo)
this.setLrc(musicInfo)
this.nowPlayTime = this.restorePlayTime = window.restorePlayInfo.time
this.maxPlayTime = window.restorePlayInfo.maxTime || 0
this.handleUpdateWinLyricInfo('music_info', {
songmid: this.musicInfo.songmid,
singer: this.musicInfo.singer,
name: this.musicInfo.name,
album: this.musicInfo.album,
})
this.$nextTick(() => {
this.sendProgressEvent(this.progress, 'paused')
})
if (this.setting.player.togglePlayMethod == 'random') this.setPlayedList(musicInfo)
this.handleRestorePlay(window.restorePlayInfo)
window.restorePlayInfo = null
return
}
// console.log('changePlay')
this.handleRemoveMusic()
if (this.playIndex < 0) return
this.stopPlay()
if (!this.playInfo.musicInfo) return
this.play()
},
'setting.player.togglePlayMethod'(n) {
audio.loop = n === 'singleLoop'
if (this.playedList.length) this.clearPlayedList()
if (n == 'random' && this.playIndex > -1) this.setPlayedList(this.list[this.playIndex])
if (n == 'random') this.setPlayedList(this.playMusicInfo)
},
'setting.player.isMute'(n) {
audio.muted = n
@@ -289,6 +275,9 @@ export default {
'setting.player.isShowLyricTransition'() {
this.setLyric()
},
'setting.player.isPlayLxlrc'() {
this.setLyric()
},
async list(n, o) {
if (n === o && this.musicInfo.songmid) {
let index = this.listId == 'download'
@@ -297,13 +286,13 @@ export default {
if (index < 0) {
// console.log(this.playIndex)
if (n.length) {
this.fixPlayIndex(this.playIndex - 1)
this.handleNext()
this.setPlayIndex(this.playInfo.listPlayIndex - 1)
this.playNext()
} else {
this.setPlayIndex(-1)
this.setPlayMusicInfo(null)
}
} else {
this.fixPlayIndex(index)
this.setPlayIndex(index)
}
// console.log(this.playIndex)
}
@@ -317,26 +306,38 @@ export default {
},
nowPlayTime(n, o) {
if (Math.abs(n - o) > 2) this.isActiveTransition = true
this.savePlayInfo({
time: n,
maxTime: this.maxPlayTime,
listId: this.listId,
list: this.listId == null ? this.list : null,
index: this.playIndex,
})
if (this.setting.player.isSavePlayTime && !this.playInfo.isTempPlay) {
this.savePlayInfo({
time: n,
maxTime: this.maxPlayTime,
listId: this.listId,
list: this.listId == null ? this.list : null,
index: this.playIndex,
})
}
},
maxPlayTime(maxPlayTime) {
if (!this.playInfo.isTempPlay) {
this.savePlayInfo({
time: this.nowPlayTime,
maxTime: maxPlayTime,
listId: this.listId,
list: this.listId == null ? this.list : null,
index: this.playIndex,
})
}
},
},
methods: {
...mapActions('player', ['getUrl', 'getPic', 'getLrc']),
...mapActions('player', ['getUrl', 'getPic', 'getLrc', 'playPrev', 'playNext']),
...mapActions('list', ['getOtherSource']),
...mapMutations('player', [
'setPlayMusicInfo',
'setPlayIndex',
'fixPlayIndex',
'resetChangePlay',
'visiblePlayerDetail',
'clearPlayedList',
'setPlayedList',
'removePlayedList',
'setList',
]),
...mapMutations(['setVolume', 'setPlayNextMode', 'setVisibleDesktopLyric', 'setLockDesktopLyric']),
...mapMutations('list', ['updateMusicInfo']),
@@ -345,8 +346,8 @@ export default {
let eventHub = window.eventHub
let name = action == 'on' ? '$on' : '$off'
eventHub[name](eventPlayerNames.toggle_play.action, this.togglePlay)
eventHub[name](eventPlayerNames.next.action, this.handleNext)
eventHub[name](eventPlayerNames.prev.action, this.handlePrev)
eventHub[name](eventPlayerNames.next.action, this.playNext)
eventHub[name](eventPlayerNames.prev.action, this.playPrev)
eventHub[name](eventPlayerNames.volume_up.action, this.handleSetVolumeUp)
eventHub[name](eventPlayerNames.volume_down.action, this.handleSetVolumeDown)
eventHub[name](eventPlayerNames.volume_mute.action, this.handleSetVolumeMute)
@@ -377,18 +378,19 @@ export default {
console.log('播放完毕')
this.stopPlay()
this.status = this.statusText = this.$t('core.player.end')
this.handleNext()
this.playNext()
})
audio.addEventListener('error', () => {
// console.log('code', audio.error)
if (!this.musicInfo.songmid) return
console.log('出错')
this.stopPlay()
this.clearLoadingTimeout()
if (this.listId != 'download' && audio.error.code !== 1 && this.retryNum < 2) { // 若音频URL无效则尝试刷新2次URL
// console.log(this.retryNum)
if (!this.restorePlayTime) this.restorePlayTime = audio.currentTime // 记录出错的播放时间
this.retryNum++
this.setUrl(this.list[this.playIndex], true)
this.setUrl(this.targetSong, true)
this.status = this.statusText = this.$t('core.player.refresh_url')
return
}
@@ -406,7 +408,9 @@ export default {
audio.currentTime = this.restorePlayTime
this.restorePlayTime = 0
}
if (!this.targetSong.interval && this.listId != 'download') this.updateMusicInfo({ id: this.listId, index: this.playIndex, data: { interval: formatPlayTime2(this.maxPlayTime) } })
if (!this.targetSong.interval && this.listId != 'download') {
this.updateMusicInfo({ id: this.listId, index: this.playIndex, data: { interval: formatPlayTime2(this.maxPlayTime) }, musicInfo: this.targetSong })
}
})
audio.addEventListener('loadstart', () => {
console.log('loadstart')
@@ -452,6 +456,10 @@ export default {
})
window.lrc = new Lyric({
lineClassName: 'lrc-content',
fontClassName: 'font',
shadowContent: false,
activeLineClassName: 'active',
onPlay: (line, text) => {
this.lyric.text = text
this.lyric.line = line
@@ -463,16 +471,17 @@ export default {
this.lyric.lines = lines
this.lyric.line = 0
},
offset: 80,
// offset: 80,
})
this.handleRegisterEvent('on')
},
async play() {
console.log('play', this.playIndex)
this.clearDelayNextTimeout()
let targetSong = this.targetSong = this.list[this.playIndex]
if (this.setting.player.togglePlayMethod == 'random') this.setPlayedList(targetSong)
const targetSong = this.targetSong
if (this.setting.player.togglePlayMethod == 'random') this.setPlayedList(this.playMusicInfo)
this.retryNum = 0
this.restorePlayTime = 0
@@ -480,7 +489,7 @@ export default {
const filePath = path.join(this.setting.download.savePath, targetSong.fileName)
// console.log(filePath)
if (!await checkPath(filePath) || !targetSong.isComplate || /\.ape$/.test(filePath)) {
return this.list.length == 1 ? null : this.handleNext()
return this.list.length == 1 ? null : this.playNext()
}
this.musicInfo.songmid = targetSong.musicInfo.songmid
this.musicInfo.singer = targetSong.musicInfo.singer
@@ -491,7 +500,7 @@ export default {
this.setImg(targetSong.musicInfo)
this.setLrc(targetSong.musicInfo)
} else {
if (!this.assertApiSupport(targetSong.source)) return this.handleNext()
// if (!this.assertApiSupport(targetSong.source)) return this.playNext()
this.musicInfo.songmid = targetSong.songmid
this.musicInfo.singer = targetSong.singer
this.musicInfo.name = targetSong.name
@@ -506,6 +515,15 @@ export default {
name: this.musicInfo.name,
album: this.musicInfo.album,
})
if (!this.playInfo.isTempPlay) {
this.savePlayInfo({
time: this.nowPlayTime,
maxTime: this.maxPlayTime,
listId: this.listId,
list: this.listId == null ? this.list : null,
index: this.playIndex,
})
}
},
clearDelayNextTimeout() {
// console.log(this.delayNextTimeout)
@@ -518,120 +536,10 @@ export default {
this.clearDelayNextTimeout()
this.delayNextTimeout = setTimeout(() => {
this.delayNextTimeout = null
this.handleNext()
this.playNext()
}, 5000)
},
async filterList() {
// if (this.list.listName === null) return
let list
let playedList = [...this.playedList]
if (this.listId == 'download') {
list = []
for (const item of this.list) {
const filePath = path.join(this.setting.download.savePath, item.fileName)
if (!await checkPath(filePath) || !item.isComplate || /\.ape$/.test(filePath)) continue
let index = playedList.indexOf(item)
if (index > -1) {
playedList.splice(index, 1)
continue
}
list.push(item)
}
} else {
list = this.list.filter(s => {
let index = playedList.indexOf(s)
if (index > -1) {
playedList.splice(index, 1)
return false
}
return this.assertApiSupport(s.source)
})
}
if (!list.length && this.playedList.length) {
this.clearPlayedList()
return this.filterList()
}
return list
},
async handlePrev() {
// console.log(playIndex)
if (this.setting.player.togglePlayMethod == 'random' && this.playedList.length) {
let index = this.playedList.indexOf(this.targetSong)
index -= 1
while (true) {
if (index > -1) {
let listIndex = this.list.indexOf(this.playedList[index])
if (listIndex < 0) {
this.removePlayedList(index)
continue
}
this.setPlayIndex(listIndex)
return
}
break
}
}
let list = await this.filterList()
if (!list.length) return this.setPlayIndex(-1)
let playIndex = list.indexOf(this.list[this.playIndex])
let index
switch (this.setting.player.togglePlayMethod) {
case 'random':
index = this.hanldeListRandom(list, playIndex)
break
case 'listLoop':
case 'list':
index = playIndex === 0 ? list.length - 1 : playIndex - 1
break
default:
return
}
if (index < 0) return
index = this.list.indexOf(list[index])
this.setPlayIndex(index)
},
async handleNext() {
// if (this.list.listName === null) return
// eslint-disable-next-line no-debugger
if (this.setting.player.togglePlayMethod == 'random' && this.playedList.length) {
let index = this.playedList.indexOf(this.targetSong)
index += 1
while (true) {
if (index < this.playedList.length) {
let listIndex = this.list.indexOf(this.playedList[index])
if (listIndex < 0) {
this.removePlayedList(index)
continue
}
this.setPlayIndex(listIndex)
return
}
break
}
}
let list = await this.filterList()
if (!list.length) return this.setPlayIndex(-1)
let playIndex = list.indexOf(this.list[this.playIndex])
// console.log(playIndex)
let index
switch (this.setting.player.togglePlayMethod) {
case 'listLoop':
index = playIndex === list.length - 1 ? 0 : playIndex + 1
break
case 'random':
index = this.hanldeListRandom(list, playIndex)
break
case 'list':
index = playIndex === list.length - 1 ? -1 : playIndex + 1
break
default:
return
}
if (index < 0) return
index = this.list.indexOf(list[index])
this.setPlayIndex(index)
},
hanldeListRandom(list, index) {
return getRandom(0, list.length)
},
@@ -673,7 +581,7 @@ export default {
togglePlay() {
if (!audio.src) {
if (this.restorePlayTime != null) {
if (!this.assertApiSupport(this.targetSong.source)) return this.handleNext()
// if (!this.assertApiSupport(this.targetSong.source)) return this.playNext()
this.setUrl(this.targetSong)
}
return
@@ -702,8 +610,9 @@ export default {
this.musicInfo.url = targetSong.typeUrl[type]
this.status = this.statusText = this.$t('core.player.geting_url')
return this.getUrl({ musicInfo: targetSong, originMusic, type, isRefresh }).then(() => {
audio.src = this.musicInfo.url = targetSong.typeUrl[type]
return this.getUrl({ musicInfo: targetSong, originMusic, type, isRefresh }).then(url => {
if ((targetSong !== this.targetSong && originMusic !== this.targetSong) || this.isPlay) return
audio.src = this.musicInfo.url = url
}).catch(err => {
// console.log('err', err.message)
if (err.message == requestMsg.cancelRequest) return
@@ -712,14 +621,11 @@ export default {
this.status = this.statusText = 'Try toggle source...'
return (originMusic.otherSource && originMusic.otherSource.length ? Promise.resolve(originMusic.otherSource) : musicSdk.findMusic(originMusic)).then(res => {
this.updateMusicInfo({ id: this.listId, index: this.playIndex, data: { otherSource: res } })
return res
}).then(otherSource => {
return this.getOtherSource(originMusic).then(otherSource => {
console.log('find otherSource', otherSource)
if (otherSource.length) {
for (const item of otherSource) {
if (retryedSource.includes(item.source)) continue
if (retryedSource.includes(item.source) || !this.assertApiSupport(item.source)) continue
console.log('try toggle to: ', item.source, item.name, item.singer, item.interval)
return this.setUrl(item, isRefresh, false, retryedSource, originMusic)
}
@@ -743,10 +649,11 @@ export default {
this.getLrc(targetSong).then(() => {
this.musicInfo.lrc = targetSong.lrc
this.musicInfo.tlrc = targetSong.tlrc
this.musicInfo.lxlrc = targetSong.lxlrc
}).catch(() => {
this.status = this.statusText = this.$t('core.player.lyric_error')
}).finally(() => {
this.handleUpdateWinLyricInfo('lyric', { lrc: this.musicInfo.lrc, tlrc: this.musicInfo.tlrc })
this.handleUpdateWinLyricInfo('lyric', { lrc: this.musicInfo.lrc, tlrc: this.musicInfo.tlrc, lxlrc: this.musicInfo.lxlrc })
this.setLyric()
})
},
@@ -760,6 +667,7 @@ export default {
this.musicInfo.songmid = null
this.musicInfo.lrc = null
this.musicInfo.tlrc = null
this.musicInfo.lxlrc = null
this.musicInfo.url = null
this.nowPlayTime = 0
this.maxPlayTime = 0
@@ -824,13 +732,13 @@ export default {
this.setProgressWidth()
},
handleToMusicLocation() {
if (!this.listId || this.listId == 'download') return
if (!this.listId || this.listId == '__temp__' || this.listId == 'download') return
if (this.playIndex == -1) return
this.$router.push({
path: 'list',
query: {
id: this.listId,
scrollIndex: this.playIndex,
scrollIndex: this.playInfo.playIndex,
},
})
},
@@ -845,7 +753,7 @@ export default {
startLoadingTimeout() {
// console.log('start load timeout')
this.loadingTimeout = setTimeout(() => {
this.handleNext()
this.playNext()
}, 20000)
},
clearLoadingTimeout() {
@@ -864,7 +772,7 @@ export default {
if (skipTime > this.maxPlayTime) skipTime = (this.maxPlayTime - audio.currentTime) / 2
if (skipTime - this.mediaBuffer.playTime < 1 || this.maxPlayTime - skipTime < 1) {
this.mediaBuffer.playTime = 0
this.handleNext()
this.playNext()
return
}
this.startBuffering()
@@ -924,13 +832,13 @@ export default {
handlePlayDetailAction({ type, data }) {
switch (type) {
case 'prev':
this.handlePrev()
this.playPrev()
break
case 'togglePlay':
this.togglePlay()
break
case 'next':
this.handleNext()
this.playNext()
break
case 'progress':
this.setProgress(data)
@@ -950,7 +858,15 @@ export default {
})
},
setLyric() {
window.lrc.setLyric((this.setting.player.isShowLyricTransition && this.musicInfo.tlrc ? (this.musicInfo.tlrc + '\n') : '') + (this.musicInfo.lrc || ''))
window.lrc.setLyric(
this.setting.player.isPlayLxlrc && this.musicInfo.lxlrc ? this.musicInfo.lxlrc : this.musicInfo.lrc,
this.setting.player.isShowLyricTransition && this.musicInfo.tlrc ? this.musicInfo.tlrc : '',
// (
// this.setting.player.isShowLyricTransition && this.musicInfo.tlrc
// ? (this.musicInfo.tlrc + '\n')
// : ''
// ) + (this.musicInfo.lrc || ''),
)
if (this.isPlay && (this.musicInfo.url || this.listId == 'download')) {
window.lrc.play(audio.currentTime * 1000)
this.handleUpdateWinLyricInfo('play', audio.currentTime * 1000)
@@ -971,6 +887,28 @@ export default {
if (!this.musicInfo.songmid) return
this.isShowAddMusicTo = true
},
handleRestorePlay(restorePlayInfo) {
let musicInfo = this.list[restorePlayInfo.index]
this.musicInfo.songmid = musicInfo.songmid
this.musicInfo.singer = musicInfo.singer
this.musicInfo.name = musicInfo.name
this.musicInfo.album = musicInfo.albumName
this.setImg(musicInfo)
this.setLrc(musicInfo)
this.nowPlayTime = this.restorePlayTime = restorePlayInfo.time
this.maxPlayTime = restorePlayInfo.maxTime || 0
this.handleUpdateWinLyricInfo('music_info', {
songmid: this.musicInfo.songmid,
singer: this.musicInfo.singer,
name: this.musicInfo.name,
album: this.musicInfo.album,
})
this.$nextTick(() => {
this.sendProgressEvent(this.progress, 'paused')
})
if (this.setting.player.togglePlayMethod == 'random') this.setPlayedList(this.playMusicInfo)
},
},
}
</script>

View File

@@ -29,7 +29,8 @@
div(:class="$style.right")
div(:class="[$style.lyric, lyricEvent.isMsDown ? $style.draging : null]" @wheel="handleWheel" @mousedown="handleLyricMouseDown" ref="dom_lyric")
div(:class="$style.lyricSpace")
p(v-for="(info, index) in lyricLines" :key="index" :class="lyric.line == index ? $style.lrcActive : null") {{info.text}}
div(:class="[$style.lyricText]" ref="dom_lyric_text")
//- p(v-for="(info, index) in lyricLines" :key="index" :class="lyric.line == index ? $style.lrcActive : null") {{info.text}}
div(:class="$style.lyricSpace")
material-music-comment(:class="$style.comment" :titleFormat="this.setting.download.fileName" :musicInfo="musicInfo" v-model="isShowComment")
@@ -158,11 +159,14 @@ export default {
handler(n, o) {
this.isSetedLines = true
if (o) {
this.$refs.dom_lyric_text.textContent = ''
this.setLyric(n)
this._lyricLines = n
if (n.length) {
this.lyricLines = n
this.$nextTick(() => {
this.dom_lines = this.$refs.dom_lyric.querySelectorAll('p')
this.dom_lines = this.$refs.dom_lyric.querySelectorAll('.lrc-content')
this.handleScrollLrc()
})
} else {
@@ -174,7 +178,7 @@ export default {
if (this.lyricLines === this._lyricLines && this._lyricLines.length) return
this.lyricLines = this._lyricLines
this.$nextTick(() => {
this.dom_lines = this.$refs.dom_lyric.querySelectorAll('p')
this.dom_lines = this.$refs.dom_lyric.querySelectorAll('.lrc-content')
this.handleScrollLrc()
})
}, 50)
@@ -182,7 +186,7 @@ export default {
} else {
this.lyricLines = n
this.$nextTick(() => {
this.dom_lines = this.$refs.dom_lyric.querySelectorAll('p')
this.dom_lines = this.$refs.dom_lyric.querySelectorAll('.lrc-content')
this.handleScrollLrc()
})
}
@@ -235,6 +239,8 @@ export default {
document.addEventListener('mousemove', this.handleMouseMsMove)
document.addEventListener('mouseup', this.handleMouseMsUp)
window.addEventListener('resize', this.handleResize)
// console.log('object', this.$refs.dom_lyric_text)
this.setLyric(this.lyricLines)
},
beforeDestroy() {
this.clearLyricScrollTimeout()
@@ -250,6 +256,13 @@ export default {
...mapMutations('player', [
'visiblePlayerDetail',
]),
setLyric(lines) {
const dom_lines = document.createDocumentFragment()
for (const line of lines) {
dom_lines.appendChild(line.dom_line)
}
this.$refs.dom_lyric_text.appendChild(dom_lines)
},
handleResize() {
this.setProgressWidth()
},
@@ -540,6 +553,7 @@ export default {
&:before {
position: absolute;
z-index: 1;
top: 0;
left: 0;
content: ' ';
@@ -565,16 +579,62 @@ export default {
overflow: hidden;
font-size: 16px;
cursor: grab;
color: @color-theme_2-font;
&.draging {
cursor: grabbing;
}
p {
padding: 8px 0;
line-height: 1.2;
overflow-wrap: break-word;
transition: @transition-theme !important;
transition-property: color, font-size;
:global {
.lrc-content {
line-height: 1.2;
margin: 16px 0;
overflow-wrap: break-word;
.translation {
transition: @transition-theme !important;
transition-property: font-size, color;
font-size: 1em;
margin-top: 5px;
}
.line {
transition-property: font-size, color !important;
background: none !important;
-webkit-text-fill-color: unset;
// -webkit-text-fill-color: none !important;
}
&.active {
.line {
color: @color-theme;
}
.translation {
font-size: 1.2em;
color: @color-theme;
}
span {
// color: @color-theme;
font-size: 1.2em;
}
}
span {
transition: @transition-theme !important;
transition-property: font-size !important;
font-size: 1em;
background-repeat: no-repeat;
background-color: rgba(77, 77, 77, 0.9);
background-image: -webkit-linear-gradient(top, @color-theme, @color-theme);
-webkit-text-fill-color: transparent;
-webkit-background-clip: text;
background-size: 0 100%;
}
}
}
// p {
// padding: 8px 0;
// line-height: 1.2;
// overflow-wrap: break-word;
// transition: @transition-theme !important;
// transition-property: color, font-size;
// }
}
.lyric-space {
height: 70%;
@@ -736,6 +796,7 @@ each(@themes, {
.container {
border-left-color: ~'@{color-@{value}-theme}';
background-color: ~'@{color-@{value}-theme_2-background_1}';
// color: ~'@{color-@{value}-theme_2-font}';
}
.right {
&:before {
@@ -769,9 +830,24 @@ each(@themes, {
box-shadow: 0 0 4px ~'@{color-@{value}-theme-hover}';
// border-color: ~'@{color-@{value}-theme-hover}';
}
.lrc-active {
color: ~'@{color-@{value}-theme}';
.lyric {
:global {
.lrc-content {
&.active {
.line {
color: ~'@{color-@{value}-theme}';
}
}
span {
// background-color: ~'@{color-@{value}-theme_2-font}';
background-image: -webkit-linear-gradient(top, ~'@{color-@{value}-theme}', ~'@{color-@{value}-theme}');
}
}
}
}
// .lrc-active {
// color: ~'@{color-@{value}-theme}';
// }
.footerLeftControlBtns {
color: ~'@{color-@{value}-theme_2-font}';
}

View File

@@ -1,319 +0,0 @@
<template lang="pug">
div(:class="$style.lists")
h2(:class="$style.listsTitle") {{$t('core.aside.my_list')}}
ul.scroll(:class="$style.listsContent")
li(:class="$style.listsItem" v-for="item in lists" :key="item.id") {{item.name}}
transition(enter-active-class="animated-fast fadeIn" leave-active-class="animated-fast fadeOut")
div(v-show="!list.length" :class="$style.noitem")
p(v-html="noItem")
material-flow-btn(:show="isShowEditBtn && assertApiSupport(source)" :remove-btn="false" @btn-click="handleFlowBtnClick")
</template>
<script>
import { mapGetters } from 'vuex'
import { scrollTo, clipboardWriteText, assertApiSupport } from '../../utils'
export default {
name: 'MaterialSongList',
model: {
prop: 'selectdData',
event: 'input',
},
props: {
list: {
type: Array,
default() {
return []
},
},
page: {
type: Number,
required: true,
},
limit: {
type: Number,
required: true,
},
total: {
type: Number,
required: true,
},
selectdData: {
type: Array,
required: true,
},
source: {
type: String,
},
noItem: {
type: String,
default: '列表加载中...',
},
},
computed: {
...mapGetters(['setting']),
},
watch: {
selectdList(n) {
const len = n.length
if (len) {
this.isShowEditBtn = true
} else {
this.isShowEditBtn = false
}
},
selectdData(n) {
const len = n.length
if (len) {
this.isShowEditBtn = true
this.selectdList = [...n]
} else {
this.isShowEditBtn = false
this.removeAllSelect()
}
},
list(n) {
this.removeAllSelect()
if (!this.list.length) return
this.$nextTick(() => scrollTo(this.$refs.dom_scrollContent, 0))
},
},
data() {
return {
clickTime: 0,
clickIndex: -1,
isShowEditBtn: false,
selectdList: [],
keyEvent: {
isShiftDown: false,
isModDown: false,
isADown: false,
aDownTimeout: null,
},
lastSelectIndex: 0,
}
},
created() {
this.listenEvent()
},
beforeDestroy() {
this.unlistenEvent()
},
methods: {
listenEvent() {
window.eventHub.$on('key_shift_down', this.handle_key_shift_down)
window.eventHub.$on('key_shift_up', this.handle_key_shift_up)
window.eventHub.$on('key_mod_down', this.handle_key_mod_down)
window.eventHub.$on('key_mod_up', this.handle_key_mod_up)
window.eventHub.$on('key_mod+a_down', this.handle_key_mod_a_down)
window.eventHub.$on('key_mod+a_up', this.handle_key_mod_a_up)
},
unlistenEvent() {
window.eventHub.$off('key_shift_down', this.handle_key_shift_down)
window.eventHub.$off('key_shift_up', this.handle_key_shift_up)
window.eventHub.$off('key_mod_down', this.handle_key_mod_down)
window.eventHub.$off('key_mod_up', this.handle_key_mod_up)
window.eventHub.$off('key_mod+a_down', this.handle_key_mod_a_down)
window.eventHub.$off('key_mod+a_up', this.handle_key_mod_a_up)
},
handle_key_shift_down() {
if (!this.keyEvent.isShiftDown) this.keyEvent.isShiftDown = true
},
handle_key_shift_up() {
if (this.keyEvent.isShiftDown) this.keyEvent.isShiftDown = false
},
handle_key_mod_down() {
if (!this.keyEvent.isModDown) this.keyEvent.isModDown = true
},
handle_key_mod_up() {
if (this.keyEvent.isModDown) this.keyEvent.isModDown = false
},
handle_key_mod_a_down() {
if (!this.keyEvent.isADown) {
this.keyEvent.isModDown = false
this.keyEvent.isADown = true
this.handleSelectAllData()
if (this.keyEvent.aDownTimeout) clearTimeout(this.keyEvent.aDownTimeout)
this.keyEvent.aDownTimeout = setTimeout(() => {
this.keyEvent.aDownTimeout = null
this.keyEvent.isADown = false
}, 500)
}
},
handle_key_mod_a_up() {
if (this.keyEvent.isADown) {
if (this.keyEvent.aDownTimeout) {
clearTimeout(this.keyEvent.aDownTimeout)
this.keyEvent.aDownTimeout = null
}
this.keyEvent.isADown = false
}
},
handleDoubleClick(event, index) {
if (event.target.classList.contains('select')) return
this.handleSelectData(event, index)
if (
window.performance.now() - this.clickTime > 400 ||
this.clickIndex !== index
) {
this.clickTime = window.performance.now()
this.clickIndex = index
return
}
this.emitEvent(this.assertApiSupport(this.source) ? 'testPlay' : 'search', index)
this.clickTime = 0
this.clickIndex = -1
},
handleSelectData(event, clickIndex) {
if (this.keyEvent.isShiftDown) {
if (this.selectdList.length) {
let lastSelectIndex = this.lastSelectIndex
this.removeAllSelect()
if (lastSelectIndex != clickIndex) {
let isNeedReverse = false
if (clickIndex < lastSelectIndex) {
let temp = lastSelectIndex
lastSelectIndex = clickIndex
clickIndex = temp
isNeedReverse = true
}
this.selectdList = this.list.slice(lastSelectIndex, clickIndex + 1)
if (isNeedReverse) this.selectdList.reverse()
let nodes = this.$refs.dom_tbody.childNodes
do {
nodes[lastSelectIndex].classList.add('active')
lastSelectIndex++
} while (lastSelectIndex <= clickIndex)
}
} else {
event.currentTarget.classList.add('active')
this.selectdList.push(this.list[clickIndex])
this.lastSelectIndex = clickIndex
}
} else if (this.keyEvent.isModDown) {
this.lastSelectIndex = clickIndex
let item = this.list[clickIndex]
let index = this.selectdList.indexOf(item)
if (index < 0) {
this.selectdList.push(item)
event.currentTarget.classList.add('active')
} else {
this.selectdList.splice(index, 1)
event.currentTarget.classList.remove('active')
}
} else if (this.selectdList.length) {
this.removeAllSelect()
} else return
this.$emit('input', [...this.selectdList])
},
removeAllSelect() {
this.selectdList = []
let dom_tbody = this.$refs.dom_tbody
if (!dom_tbody) return
let nodes = dom_tbody.querySelectorAll('.active')
for (const node of nodes) {
if (node.parentNode == dom_tbody) node.classList.remove('active')
}
},
handleListBtnClick(info) {
this.emitEvent('listBtnClick', info)
},
handleSelectAllData() {
this.removeAllSelect()
this.selectdList = [...this.list]
let nodes = this.$refs.dom_tbody.childNodes
for (const node of nodes) {
node.classList.add('active')
}
this.$emit('input', [...this.selectdList])
},
handleTogglePage(page) {
this.emitEvent('togglePage', page)
},
handleFlowBtnClick(action) {
this.emitEvent('flowBtnClick', action)
},
emitEvent(action, data) {
this.$emit('action', { action, data })
},
handleChangeSelect() {
this.$emit('input', [...this.selectdList])
},
handleContextMenu(event) {
if (!event.target.classList.contains('select')) return
let classList = this.$refs.dom_scrollContent.classList
classList.add(this.$style.copying)
window.requestAnimationFrame(() => {
let str = window.getSelection().toString()
classList.remove(this.$style.copying)
str = str.trim()
if (!str.length) return
clipboardWriteText(str)
})
},
assertApiSupport(source) {
return assertApiSupport(source)
},
},
}
</script>
<style lang="less" module>
@import '../../assets/styles/layout.less';
.lists {
flex: none;
width: 15%;
display: flex;
flex-flow: column nowrap;
}
.title {
font-size: 12px;
line-height: 28px;
padding: 5px;
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
flex: none;
}
.list {
flex: auto;
min-width: 0;
}
.item {
}
.noitem {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
display: flex;
flex-flow: column nowrap;
justify-content: center;
align-items: center;
p {
font-size: 24px;
color: @color-theme_2-font-label;
}
}
each(@themes, {
:global(#container.@{value}) {
.tbody {
td {
&:first-child {
color: ~'@{color-@{value}-theme_2-font-label}';
}
}
}
.noitem {
p {
color: ~'@{color-@{value}-theme_2-font-label}';
}
}
}
})
</style>

View File

@@ -1,5 +1,5 @@
<template lang="pug">
button(:class="[$style.btn, min ? $style.min : null]" :disabled="disabled" @click="$emit('click', $event)")
button(:class="[$style.btn, min ? $style.min : null, outline ? $style.outline : null]" :disabled="disabled" @click="$emit('click', $event)")
slot
</template>
@@ -9,6 +9,10 @@ export default {
min: {
type: Boolean,
},
outline: {
type: Boolean,
default: false,
},
disabled: {
type: Boolean,
default: false,
@@ -36,6 +40,10 @@ export default {
opacity: .4;
}
&.outline {
background-color: transparent;
}
&:hover {
background-color: @color-btn-hover;
}
@@ -54,6 +62,9 @@ each(@themes, {
.btn {
color: ~'@{color-@{value}-btn}';
background-color: ~'@{color-@{value}-btn-background}';
&.outline {
background-color: transparent;
}
&:hover {
background-color: ~'@{color-@{value}-btn-hover}';
}

View File

@@ -6,7 +6,7 @@ material-modal(:show="show" :bg-close="bgClose" @close="handleClose")
span(:class="$style.name") {{this.musicInfo && `${musicInfo.name}`}}
| &nbsp;{{$t('material.list_add_modal.title_last')}}
div.scroll(:class="$style.btnContent")
material-btn(:class="$style.btn" :tips="$t('material.list_add_modal.btn_title', { name: item.name })" :key="item.id" @click="handleClick(index)" v-for="(item, index) in lists") {{item.name}}
material-btn(:class="$style.btn" :tips="$t('material.list_add_modal.btn_title', { name: item.name })" :key="item.id" :disabled="item.isExist" @click="handleClick(index)" v-for="(item, index) in lists") {{item.name}}
material-btn(:class="[$style.btn, $style.newList, isEditing ? $style.editing : null]" @click="handleEditing($event)" :tips="$t('view.list.lists_new_list_btn')")
svg(version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' viewBox='0 0 42 42' space='preserve')
use(xlink:href='#icon-addTo')
@@ -57,11 +57,12 @@ export default {
computed: {
...mapGetters('list', ['defaultList', 'loveList', 'userList']),
lists() {
if (!this.musicInfo) return []
return [
this.defaultList,
this.loveList,
...this.userList,
].filter(l => l.id != this.excludeListId.includes(l.id))
].filter(l => !this.excludeListId.includes(l.id)).map(l => ({ ...l, isExist: l.list.some(s => s.songmid == this.musicInfo.songmid) }))
},
spaceNum() {
return this.lists.length < 2 ? 0 : (3 - this.lists.length % 3 - 1)

View File

@@ -62,7 +62,7 @@ export default {
this.defaultList,
this.loveList,
...this.userList,
].filter(l => l.id != this.excludeListId.includes(l.id))
].filter(l => !this.excludeListId.includes(l.id))
},
spaceNum() {
return this.lists.length < 2 ? 0 : (3 - this.lists.length % 3 - 1)

View File

@@ -138,7 +138,7 @@ export default {
// will-change: transform;
li {
cursor: pointer;
min-width: 90px;
min-width: 96px;
line-height: 34px;
// color: @color-btn;
padding: 0 10px;

View File

@@ -74,9 +74,9 @@ export default {
'slideOutLeft',
'slideOutRight',
'slideOutUp',
'hinge',
// 'hinge',
],
inClass: 'animated flipInX',
inClass: 'animated jackInTheBox',
outClass: 'animated flipOutX',
unwatchFn: null,
}

View File

@@ -50,7 +50,7 @@ export default {
singer: '',
},
page: 1,
total: 10,
total: 0,
maxPage: 1,
limit: 20,
isHotLoading: true,

View File

@@ -31,8 +31,6 @@ div(:class="$style.songList")
td(:style="{ width: rowWidth.r6 }" style="padding-left: 0; padding-right: 0;")
material-list-buttons(:index="index" :class="$style.btns"
:remove-btn="false" @btn-click="handleListBtnClick"
:listAdd-btn="assertApiSupport(item.source)"
:play-btn="assertApiSupport(item.source)"
:download-btn="assertApiSupport(item.source)")
//- button.btn-info(type='button' v-if="item._types['128k'] || item._types['192k'] || item._types['320k'] || item._types.flac" @click.stop='openDownloadModal(index)') 下载
//- button.btn-secondary(type='button' v-if="item._types['128k'] || item._types['192k'] || item._types['320k']" @click.stop='testPlay(index)') 试听
@@ -114,6 +112,11 @@ export default {
action: 'download',
disabled: !this.listMenu.itemMenuControl.download,
},
{
name: this.$t('material.song_list.list_play_later'),
action: 'playLater',
disabled: !this.listMenu.itemMenuControl.playLater,
},
{
name: this.$t('material.song_list.list_search'),
action: 'search',
@@ -173,6 +176,7 @@ export default {
itemMenuControl: {
play: true,
addTo: true,
playLater: true,
download: true,
search: true,
sourceDetail: true,
@@ -237,7 +241,7 @@ export default {
this.clickIndex = index
return
}
this.emitEvent(this.assertApiSupport(this.source) ? 'testPlay' : 'search', index)
this.emitEvent('testPlay', index)
this.clickTime = 0
this.clickIndex = -1
},
@@ -334,8 +338,9 @@ export default {
},
handleListItemRigthClick(event, index) {
this.listMenu.itemMenuControl.sourceDetail = !!musicSdk[this.list[index].source].getMusicDetailPageUrl
this.listMenu.itemMenuControl.play =
this.listMenu.itemMenuControl.download =
// this.listMenu.itemMenuControl.play =
// this.listMenu.itemMenuControl.playLater =
this.listMenu.itemMenuControl.download =
this.assertApiSupport(this.list[index].source)
let dom_selected = this.$refs.dom_tbody.querySelector('tr.selected')
if (dom_selected) dom_selected.classList.remove('selected')

View File

@@ -0,0 +1,240 @@
<template lang="pug">
material-modal(:show="visible" bg-close @close="handleClose")
main(:class="$style.main")
h2 {{$t('material.user_api_modal.title')}}
ul.scroll(v-if="apiList.length" :class="$style.content")
li(:class="[$style.listItem, setting.apiSource == api.id ? $style.active : null]" v-for="(api, index) in apiList" :key="api.id")
div(:class="$style.listLeft")
h3 {{api.name}}
p {{api.description}}
material-btn(:class="$style.listBtn" outline :tips="$t('material.user_api_modal.btn_remove')" @click.stop="handleRemove(index)")
svg(version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' viewBox='0 0 212.982 212.982' space='preserve' v-once)
use(xlink:href='#icon-delete')
div(v-else :class="$style.content")
div(:class="$style.noitem") {{$t('material.user_api_modal.noitem')}}
div(:class="$style.note")
p(:class="[$style.ruleLink]")
| {{$t('material.user_api_modal.readme')}}
span.hover.underline(@click="handleOpenUrl('https://github.com/lyswhut/lx-music-desktop/blob/master/FAQ.md')" tips="https://github.com/lyswhut/lx-music-desktop/blob/master/FAQ.md") FAQ.md
p {{$t('material.user_api_modal.note')}}
div(:class="$style.footer")
material-btn(:class="$style.footerBtn" @click="handleImport") {{$t('material.user_api_modal.btn_import')}}
//- material-btn(:class="$style.footerBtn" @click="handleExport") {{$t('material.user_api_modal.btn_export')}}
</template>
<script>
import { mapGetters } from 'vuex'
import { rendererInvoke, NAMES } from '@common/ipc'
import { promises as fsPromises } from 'fs'
import {
selectDir,
openUrl,
} from '../../utils'
import apiSourceInfo from '../../utils/music/api-source-info'
export default {
props: {
visible: {
type: Boolean,
default: false,
},
},
model: {
prop: 'visible',
event: 'toggle',
},
data() {
return {
globalObj: window.globalObj,
}
},
computed: {
...mapGetters(['setting']),
apiList() {
return this.globalObj.userApi.list
},
},
methods: {
handleImport() {
selectDir({
title: this.$t('material.user_api_modal.import_file'),
properties: ['openFile'],
filters: [
{ name: 'LX API File', extensions: ['js'] },
{ name: 'All Files', extensions: ['*'] },
],
}).then(result => {
if (result.canceled) return
return fsPromises.readFile(result.filePaths[0]).then(data => {
return rendererInvoke(NAMES.mainWindow.import_user_api, data.toString()).then(({ apiList }) => {
window.globalObj.userApi.list = apiList
})
})
})
},
handleExport() {
},
async handleRemove(index) {
const api = this.apiList[index]
if (!api) return
if (this.setting.apiSource == api.id) {
let backApi = apiSourceInfo.find(api => !api.disabled)
if (backApi) window.globalObj.apiSource = backApi.id
}
window.globalObj.userApi.list = await rendererInvoke(NAMES.mainWindow.remove_user_api, [api.id])
},
handleClose() {
this.$emit('toggle', false)
},
handleOpenUrl(url) {
openUrl(url)
},
},
}
</script>
<style lang="less" module>
@import '../../assets/styles/layout.less';
.main {
padding: 15px;
max-width: 400px;
min-width: 300px;
display: flex;
flex-flow: column nowrap;
justify-content: center;
min-height: 0;
// max-height: 100%;
// overflow: hidden;
h2 {
font-size: 16px;
color: @color-theme_2-font;
line-height: 1.3;
text-align: center;
}
}
.name {
color: @color-theme;
}
.content {
flex: auto;
min-height: 100px;
max-height: 100%;
margin-top: 15px;
}
.listItem {
display: flex;
flex-flow: row nowrap;
align-items: center;
transition: background-color 0.2s ease;
padding: 10px;
border-radius: @radius-border;
&:hover {
background-color: @color-theme_2-hover;
}
&.active {
background-color: @color-theme_2-active;
}
h3 {
font-size: 15px;
color: @color-theme_2-font;
word-break: break-all;
}
p {
margin-top: 5px;
font-size: 14px;
color: @color-theme_2-font-label;
word-break: break-all;
}
}
.noitem {
height: 100px;
font-size: 18px;
color: @color-theme_2-font-label;
display: flex;
justify-content: center;
align-items: center;
}
.listLeft {
flex: auto;
min-width: 0;
display: flex;
flex-flow: column nowrap;
justify-content: center;
}
.listBtn {
flex: none;
height: 30px;
width: 30px;
padding: 0;
display: flex;
justify-content: center;
align-items: center;
svg {
width: 60%;
}
}
.note {
margin-top: 15px;
font-size: 12px;
line-height: 1.25;
color: @color-theme_2-font;
p {
+ p {
margin-top: 5px;
}
}
}
.footer {
margin-top: 15px;
display: flex;
flex-flow: row nowrap;
}
.footerBtn {
flex: auto;
height: 36px;
line-height: 36px;
padding: 0 10px !important;
width: 150px;
.mixin-ellipsis-1;
+ .footerBtn {
margin-left: 15px;
}
}
.ruleLink {
.mixin-ellipsis-1;
}
each(@themes, {
:global(#container.@{value}) {
.main {
h2 {
color: ~'@{color-@{value}-theme_2-font}';
}
}
.listItem {
&:hover {
background-color: ~'@{color-@{value}-theme_2-hover}';
}
&.active {
background-color: ~'@{color-@{value}-theme_2-active}';
}
h3 {
color: ~'@{color-@{value}-theme_2-font}';
}
p {
color: ~'@{color-@{value}-theme_2-font-label}';
}
}
.noitem {
color: ~'@{color-@{value}-theme_2-font-label}';
}
}
})
</style>

View File

@@ -3,17 +3,17 @@ material-modal(:show="version.showModal" @close="handleClose" v-if="version.newV
main(:class="$style.main" v-if="version.isDownloaded")
h2 🚀程序更新🚀
div.scroll(:class="$style.info")
div.scroll.select(:class="$style.info")
div(:class="$style.current")
h3 最新版本{{version.newVersion.version}}
h3 当前版本{{version.version}}
h3 版本变化
p(:class="$style.desc" v-html="version.newVersion.desc")
pre(:class="$style.desc" v-text="version.newVersion.desc")
div(:class="[$style.history, $style.desc]" v-if="history.length")
h3 历史版本
div(:class="$style.item" v-for="ver in history")
h4 v{{ver.version}}
p(v-html="ver.desc")
pre(v-text="ver.desc")
div(:class="$style.footer")
div(:class="$style.desc")
p 新版本已下载完毕
@@ -27,17 +27,17 @@ material-modal(:show="version.showModal" @close="handleClose" v-if="version.newV
main(:class="$style.main" v-else-if="version.isError && !version.isUnknow && version.newVersion.version != version.version")
h2 版本更新出错
div.scroll(:class="$style.info")
div.scroll.select(:class="$style.info")
div(:class="$style.current")
h3 最新版本{{version.newVersion.version}}
h3 当前版本{{version.version}}
h3 版本变化
p(:class="$style.desc" v-html="version.newVersion.desc")
pre(:class="$style.desc" v-text="version.newVersion.desc")
div(:class="[$style.history, $style.desc]" v-if="history.length")
h3 历史版本
div(:class="$style.item" v-for="ver in history")
h4 v{{ver.version}}
p(v-html="ver.desc")
pre(v-text="ver.desc")
div(:class="$style.footer")
div(:class="$style.desc")
@@ -58,7 +58,7 @@ material-modal(:show="version.showModal" @close="handleClose" v-if="version.newV
main(:class="$style.main" v-else-if="version.isDownloading && version.isTimeOut && !version.isUnknow")
h2 新版本下载超时
div(:class="$style.desc")
p 你当前所在网络访问GitHub较慢导致新版本下载超时已经下了半个钟了😳建议手动更新版本
p 你当前所在网络访问GitHub较慢导致新版本下载超时已经下了半个钟了😳你仍可选择继续等但墙裂建议手动更新版本
p
| 你可以去
material-btn(min @click="handleOpenUrl('https://github.com/lyswhut/lx-music-desktop/releases')" tips="点击打开") 软件发布页
@@ -75,7 +75,7 @@ material-modal(:show="version.showModal" @close="handleClose" v-if="version.newV
main(:class="$style.main" v-else-if="version.isUnknow")
h2 获取最新版本信息失败
div.scroll(:class="$style.info")
div.scroll.select(:class="$style.info")
div(:class="$style.current")
h3 当前版本{{version.version}}
div(:class="$style.desc")
@@ -94,17 +94,17 @@ material-modal(:show="version.showModal" @close="handleClose" v-if="version.newV
main(:class="$style.main" v-else)
h2 🌟发现新版本🌟
div.scroll(:class="$style.info")
div.scroll.select(:class="$style.info")
div(:class="$style.current")
h3 最新版本{{version.newVersion.version}}
h3 当前版本{{version.version}}
h3 版本变化
p(:class="$style.desc" v-html="version.newVersion.desc")
pre(:class="$style.desc" v-text="version.newVersion.desc")
div(:class="[$style.history, $style.desc]" v-if="history.length")
h3 历史版本
div(:class="$style.item" v-for="ver in history")
h4 v{{ver.version}}
p(v-html="ver.desc")
pre(v-text="ver.desc")
div(:class="$style.footer")
div(:class="$style.desc")
@@ -146,7 +146,7 @@ export default {
progress() {
return this.version.downloadProgress
? `${this.version.downloadProgress.percent.toFixed(2)}% - ${sizeFormate(this.version.downloadProgress.transferred)}/${sizeFormate(this.version.downloadProgress.total)} - ${sizeFormate(this.version.downloadProgress.bytesPerSecond)}/s`
: '初始化中...'
: '处理更新中...'
},
isIgnored() {
return this.setting.ignoreVersion == this.version.newVersion.version
@@ -207,6 +207,11 @@ export default {
font-size: 14px;
line-height: 1.3;
}
pre {
white-space: pre-wrap;
text-align: justify;
margin-top: 10px;
}
}
.info {

View File

@@ -1,50 +0,0 @@
<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,6 @@
{
"list_play": "Play",
"list_play_later": "Play later",
"list_add_to": "Add to ...",
"list_download": "Download",
"list_search": "Search",

View File

@@ -0,0 +1,10 @@
{
"title": "Custom Source Management",
"readme": "Source writing instructions: ",
"note": "Tip: Although we have isolated the script's running environment as much as possible, importing scripts containing malicious behaviors may still affect your system. Please import them carefully.",
"btn_remove": "Remove",
"btn_import": "Import",
"btn_export": "Export",
"import_file": "Select music API script file",
"noitem": "There is nothing here...😲"
}

View File

@@ -1,5 +1,6 @@
{
"menu_play": "Play",
"menu_play_later": "Play later",
"menu_start": "Start task",
"menu_pause": "Pause Task",
"menu_file": "Locate File",

View File

@@ -7,6 +7,7 @@
"lists_sync": "Sync",
"lists_remove": "Remove",
"list_play": "Play",
"list_play_later": "Play later",
"list_copy_name": "Copy name",
"list_add_to": "Add to ...",
"list_move_to": "Move to ...",

View File

@@ -1,5 +1,6 @@
{
"list_play": "Play",
"list_play_later": "Play later",
"list_add_to": "Add to ...",
"list_download": "Download",
"list_source_detail": "Song Page",
@@ -10,6 +11,7 @@
"time": "Length",
"lossless": "SQ",
"high_quality": "HQ",
"loding_list": "Loading...",
"no_item": "Search what I want to 😉",
"hot_search": "Top Searches",
"history_search": "History Searches",

View File

@@ -2,7 +2,6 @@
"basic": "General",
"basic_theme": "Theme",
"basic_show_animation": "Show switching animation",
"basic_animation_title": "Animation effect of the pop-up layer",
"basic_animation": "Random pop-up animation",
"basic_source_title": "Choose a music source",
"basic_source_test": "Test API (Available for most software features)",
@@ -12,6 +11,10 @@
"basic_sourcename_real": "Original",
"basic_sourcename_alias": "Aliases",
"basic_sourcename": "Source name",
"basic_source_status_success": "Initialization successful",
"basic_source_status_initing": "Initializing",
"basic_source_status_failed": "Initialization failed",
"basic_source_user_api_btn": "Custom Source Management",
"basic_window_size_title": "Set the window size",
"basic_window_size": "Window size",
"basic_window_size_smaller": "Smaller",
@@ -21,8 +24,7 @@
"basic_window_size_larger": "Larger",
"basic_window_size_oversized": "Oversized",
"basic_window_size_huge": "Huge",
"basic_to_tray_title": "Minimize it to the system tray without closing the software when closing",
"basic_to_tray": "Minimize to system tray when closing",
"basic_to_tray": "Do not exit the software when closing the software and minimize it to the system tray",
"basic_lang_title": "The language displayed in the software",
"basic_lang": "Language",
"basic_control_btn_position": "Control Button Position",
@@ -30,21 +32,14 @@
"basic_control_btn_position_right": "Right",
"play": "Play",
"play_toggle_title": "If none selected, it stopped when the music playing is done.",
"play_toggle": "Playback mode",
"play_toggle_list_loop": "Playlist repeat",
"play_toggle_random": "Playlist shuffle",
"play_toggle_list": "Play in order",
"play_toggle_single_loop": "Single repeat",
"play_save_play_time": "Remember playback progress",
"play_lyric_transition": "Show lyrics translation",
"play_quality_title": "The 320k quality is preferred for playing",
"play_quality": "Prefer High Quality 320k",
"play_task_bar_title": "Show playing progress on the taskbar",
"play_task_bar": "Taskbar play progress bar",
"play_lyric_lxlrc": "Use Karaoke-style lyrics playback (if supported)",
"play_quality": "Play 320K quality songs first (if supported)",
"play_task_bar": "Show playing progress on the taskbar",
"play_mediaDevice_title": "Select a media device for audio output",
"play_mediaDevice": "Audio output",
"play_mediaDevice_remove_stop_play": "Whether to pause playback when the audio output device is changed",
"play_mediaDevice_remove_stop_play_title": "Whether to pause the song when the current sound output device is changed",
"play_mediaDevice_remove_stop_play": "Pause the song when the current sound output device is changed",
"desktop_lyric": "Desktop Lyric Settings",
"desktop_lyric_enable": "Display lyrics",
@@ -53,18 +48,13 @@
"desktop_lyric_lock_screen": "It is not allowed to drag the lyrics window out of the main screen",
"search": "Search",
"search_hot_title": "Select whether to show popular searches",
"search_hot": "Top Searches",
"search_history_title": "Select whether to show search history",
"search_history": "Search history",
"search_focus_search_box_title": "Whether the search box is automatically focused on startup",
"search_focus_search_box": "Whether the search box is focused on startup",
"search_focus_search_box": "Automatically focus the search box on startup",
"list": "List",
"list_source_title": "Select whether to show music source",
"list_source": "Select whether to show music source (for Your Library only)",
"list_scroll_title": "Select whether to remember the playlist scrollbar position",
"list_scroll": "Remember playlist scrolling position (for Your library only)",
"list_source": "Show song source (only valid for my music category)",
"list_scroll": "Remember the position of the scroll bar of the playlist (only valid for my music classification)",
"download": "Download",
"download_enable": "Whether to enable download function",
@@ -136,10 +126,11 @@
"other_tray_theme": "Tray Icon Style",
"other_tray_theme_native": "Solid Color",
"other_tray_theme_origin": "Primary Color",
"other_cache": "Cache size (Not recommended since resources such as pictures after the cache is cleaned need re-downloading. The software will dynamically manage the cache size based on disk space)",
"other_cache_label": "Cache size used: ",
"other_cache_label_title": "Currently used cache size",
"other_cache_clear_btn": "Clear cache",
"other_resource_cache": "Resource cache management (pictures, audios and other caches, pictures and other resources will need to be downloaded again after cleaning up, it is not recommended to clean up, the software will dynamically manage the cache size according to the disk space)",
"other_resource_cache_label": "The software has used cache size: ",
"other_resource_cache_clear_btn": "Clear resource cache",
"other_play_list_cache": "List cache management (links to songs that have been cached in my list, alternative sources for playback, after cleaning up, you need to re-acquire them when you play and download songs, and do not clean up if there are no issues related to song playback)",
"other_play_list_cache_clear_btn": "Clear list cache information",
"update": "Update",
"update_latest_label": "Latest version: ",
@@ -150,7 +141,7 @@
"update_latest": "The software is up-to-date, enjoy yourself!🥂",
"update_open_version_modal_btn": "Open the update window🚀",
"update_checking": "Checking for updates...",
"update_init": "Initializing update...",
"update_init": "Processing update...",
"about": "About lx-music-desktop",

View File

@@ -8,5 +8,6 @@
"tip_2": "If you encounter a link to a playlist that cannot be opened, welcome feedback",
"tip_3": "Kugou source does not support opening with playlist ID, but supports Kugou code opening",
"play_all": "Play",
"play_later": "Play later",
"add_all": "Collect"
}

View File

@@ -1,5 +1,6 @@
{
"list_play": "播放",
"list_play_later": "稍后播放",
"list_add_to": "添加到...",
"list_download": "下载",
"list_source_detail": "歌曲详情页",

View File

@@ -0,0 +1,10 @@
{
"title": "自定义源管理",
"readme": "源编写说明:",
"note": "提示:虽然我们已经尽可能地隔离了脚本的运行环境,但导入包含恶意行为的脚本仍可能会影响你的系统,请谨慎导入。",
"btn_remove": "移除",
"btn_import": "导入",
"btn_export": "导出",
"import_file": "选择音乐API脚本文件",
"noitem": "这里竟然是空的 😲"
}

View File

@@ -1,5 +1,6 @@
{
"menu_play": "播放",
"menu_play_later": "稍后播放",
"menu_start": "开始任务",
"menu_pause": "暂停任务",
"menu_file": "定位文件",

View File

@@ -7,6 +7,7 @@
"lists_sync": "同步",
"lists_remove": "删除",
"list_play": "播放",
"list_play_later": "稍后播放",
"list_copy_name": "复制歌曲名",
"list_source_detail": "歌曲详情页",
"list_add_to": "添加到...",

View File

@@ -1,5 +1,6 @@
{
"list_play": "播放",
"list_play_later": "稍后播放",
"list_add_to": "添加到...",
"list_download": "下载",
"list_source_detail": "歌曲详情页",
@@ -10,6 +11,7 @@
"time": "时长",
"lossless": "无损",
"high_quality": "高品质",
"loding_list": "加载中...",
"no_item": "搜我所想~~😉",
"hot_search": "热门搜索",
"history_search": "历史搜索",

View File

@@ -1,7 +1,6 @@
{
"basic": "基本设置",
"basic_theme": "主题颜色",
"basic_animation_title": "弹出层的动画效果",
"basic_animation": "弹出层随机动画",
"basic_show_animation": "显示切换动画",
"basic_source_title": "选择音乐来源",
@@ -12,6 +11,10 @@
"basic_sourcename_real": "原名",
"basic_sourcename_alias": "别名",
"basic_sourcename": "音源名字",
"basic_source_status_success": "初始化成功",
"basic_source_status_initing": "初始化中",
"basic_source_status_failed": "初始化失败",
"basic_source_user_api_btn": "自定义源管理",
"basic_window_size_title": "设置软件窗口尺寸",
"basic_window_size": "窗口尺寸",
"basic_window_size_smaller": "较小",
@@ -21,8 +24,7 @@
"basic_window_size_larger": "较大",
"basic_window_size_oversized": "超大",
"basic_window_size_huge": "巨大",
"basic_to_tray_title": "关闭时不退出软件将其最小化到系统托盘",
"basic_to_tray": "关闭时最小化到系统托盘",
"basic_to_tray": "关闭软件时不退出软件将其最小化到系统托盘",
"basic_lang_title": "软件显示的语言",
"basic_lang": "语言",
"basic_control_btn_position": "控制按钮位置",
@@ -30,21 +32,14 @@
"basic_control_btn_position_right": "右边",
"play": "播放设置",
"play_toggle_title": "都不选时播放完当前歌曲就停止播放",
"play_toggle": "歌曲切换方式",
"play_toggle_list_loop": "列表循环",
"play_toggle_random": "列表随机",
"play_toggle_list": "顺序播放",
"play_toggle_single_loop": "单曲循环",
"play_save_play_time": "记住播放进度",
"play_lyric_transition": "显示歌词翻译",
"play_quality_title": "启用时将优先播放320K品质的歌曲",
"play_quality": "优先播放高品质音乐",
"play_task_bar_title": "在任务栏上显示当前歌曲播放进度",
"play_task_bar": "任务栏播放进度条",
"play_lyric_lxlrc": "使用卡拉OK式歌词播放如果支持",
"play_quality": "优先播放320K品质的歌曲如果支持",
"play_task_bar": "在任务栏上显示当前歌曲播放进度",
"play_mediaDevice_title": "选择声音输出的媒体设备",
"play_mediaDevice": "音频输出",
"play_mediaDevice_remove_stop_play": "音输出设备被改变时是否暂停播放",
"play_mediaDevice_remove_stop_play_title": "当前的声音输出设备被改变时是否暂停播放歌曲",
"play_mediaDevice_remove_stop_play": "当前的声音输出设备被改变时暂停播放歌曲",
"desktop_lyric": "桌面歌词设置",
"desktop_lyric_enable": "显示歌词",
@@ -53,18 +48,13 @@
"desktop_lyric_lock_screen": "不允许歌词窗口拖出主屏幕之外",
"search": "搜索设置",
"search_hot_title": "是否显示热门搜索",
"search_hot": "热门搜索",
"search_history_title": "是否显示历史搜索记录",
"search_history": "搜索历史",
"search_focus_search_box_title": "启动时是否自动聚焦搜索框",
"search_focus_search_box": "启动时是否聚焦搜索框",
"search_hot": "显示热门搜索",
"search_history": "显示历史搜索记录",
"search_focus_search_box": "启动时自动聚焦搜索框",
"list": "列表设置",
"list_source_title": "是否显示歌曲源",
"list_source": "是否显示歌曲源(仅对我的音乐分类有效)",
"list_scroll_title": "是否记住播放列表滚动条位置",
"list_scroll": "记住列表滚动位置(仅对我的音乐分类有效)",
"list_source": "显示歌曲源(仅对我的音乐分类有效)",
"list_scroll": "记住播放列表滚动条位置(仅对我的音乐分类有效)",
"download": "下载设置",
"download_enable": "是否启用下载功能",
@@ -136,10 +126,11 @@
"other_tray_theme": "托盘图标样式",
"other_tray_theme_native": "纯色",
"other_tray_theme_origin": "原色",
"other_cache": "缓存大小(清理缓存后图片等资源将需要重新下载,不建议清理,软件会根据磁盘空间动态管理缓存大小)",
"other_cache_label": "软件已使用缓存大小:",
"other_cache_label_title": "当前已用缓存",
"other_cache_clear_btn": "清理缓存",
"other_resource_cache": "资源缓存管理(图片、音频等缓存,清理后图片等资源将需要重新下载,不建议清理,软件会根据磁盘空间动态管理缓存大小)",
"other_resource_cache_label": "软件已使用缓存大小:",
"other_resource_cache_clear_btn": "清理资源缓存",
"other_play_list_cache": "列表缓存管理(我的列表中已缓存的歌曲链接、播放代替源,清理后播放、下载歌曲时需要重新获取,没有歌曲播放相关的问题不要清理)",
"other_play_list_cache_clear_btn": "清理列表缓存信息",
"update": "软件更新",
"update_latest_label": "最新版本:",
@@ -150,7 +141,7 @@
"update_latest": "软件已是最新,尽情地体验吧~🥂",
"update_open_version_modal_btn": "打开更新窗口 🚀",
"update_checking": "检查更新中...",
"update_init": "更新初始化中...",
"update_init": "处理更新中...",
"about": "关于洛雪音乐",

View File

@@ -8,5 +8,6 @@
"tip_2": "若遇到无法打开的歌单链接,欢迎反馈",
"tip_3": "酷狗源不支持用歌单ID打开但支持酷狗码打开",
"play_all": "播放",
"play_later": "稍后播放",
"add_all": "收藏"
}

View File

@@ -1,5 +1,6 @@
{
"list_play": "播放",
"list_play_later": "稍後播放",
"list_add_to": "添加到...",
"list_download": "下載",
"list_search": "搜索",

View File

@@ -0,0 +1,10 @@
{
"title": "自定義源管理",
"readme": "源編寫說明:",
"note": "提示:雖然我們已經盡可能地隔離了腳本的運行環境,但導入包含惡意行為的腳本仍可能會影響你的系統,請謹慎導入。",
"btn_remove": "移除",
"btn_import": "導入",
"btn_export": "導出",
"import_file": "選擇音樂API腳本文件",
"noitem": "這裡竟然是空的 😲"
}

View File

@@ -1,5 +1,6 @@
{
"menu_play": "播放",
"menu_play_later": "稍後播放",
"menu_start": "開始任務",
"menu_pause": "暫停任務",
"menu_file": "定位文件",

View File

@@ -7,6 +7,7 @@
"lists_sync": "同步",
"lists_remove": "刪除",
"list_play": "播放",
"list_play_later": "稍後播放",
"list_copy_name": "複製歌曲名",
"list_add_to": "添加到...",
"list_move_to": "移動到...",

View File

@@ -1,5 +1,6 @@
{
"list_play": "播放",
"list_play_later": "稍後播放",
"list_add_to": "添加到...",
"list_download": "下載",
"list_source_detail": "歌曲詳情頁",
@@ -10,6 +11,7 @@
"time": "時長",
"lossless": "無損",
"high_quality": "高品質",
"loding_list": "加載中...",
"no_item": "搜我所想~~😉",
"hot_search": "熱門搜索",
"history_search": "歷史搜索",

View File

@@ -1,7 +1,6 @@
{
"basic": "基本設置",
"basic_theme": "主題顏色",
"basic_animation_title": "彈出層的動畫效果",
"basic_animation": "彈出層隨機動畫",
"basic_show_animation": "顯示切換動畫",
"basic_source_title": "選擇音樂來源",
@@ -12,6 +11,10 @@
"basic_sourcename_real": "原名",
"basic_sourcename_alias": "別名",
"basic_sourcename": "音源名字",
"basic_source_status_success": "初始化成功",
"basic_source_status_initing": "初始化中",
"basic_source_status_failed": "初始化失敗",
"basic_source_user_api_btn": "自定義源管理",
"basic_window_size_title": "設置軟件窗口尺寸",
"basic_window_size": "窗口尺寸",
"basic_window_size_smaller": "較小",
@@ -21,46 +24,38 @@
"basic_window_size_larger": "較大",
"basic_window_size_oversized": "超大",
"basic_window_size_huge": "巨大",
"basic_to_tray_title": "關閉時不退出軟件將其最小化到系統托盤",
"basic_to_tray": "關閉時最小化到系統托盤",
"basic_to_tray": "關閉軟件時不退出軟件將其最小化到系統托盤",
"basic_lang_title": "軟件顯示的語言",
"basic_lang": "語言",
"basic_control_btn_position": "控制按鈕位置",
"basic_control_btn_position_left": "左邊",
"basic_control_btn_position_right": "右邊",
"play": "播放設置",
"play_toggle_title": "都不選時播放完當前歌曲就停止播放",
"play_toggle": "歌曲切換方式",
"play_toggle_list_loop": "列表循環",
"play_toggle_random": "列表隨機",
"play_toggle_list": "順序播放",
"play_toggle_single_loop": "單曲循環",
"play_save_play_time": "記住播放進度",
"play_lyric_transition": "顯示歌詞翻譯",
"play_quality_title": "啟用時將優先播放320K品質的歌曲",
"play_quality": "優先播放高品質音樂",
"play_task_bar_title": "在任務欄上顯示當前歌曲播放進度",
"play_task_bar": "任務欄播放進度條",
"play_lyric_lxlrc": "使用卡拉OK式歌詞播放如果支持",
"play_quality": "優先播放320K品質的歌曲如果支持",
"play_task_bar": "在任務欄上顯示當前歌曲播放進度",
"play_mediaDevice_title": "選擇聲音輸出的媒體設備",
"play_mediaDevice": "音頻輸出",
"play_mediaDevice_remove_stop_play": "音輸出設備被改變時是否暫停播放",
"play_mediaDevice_remove_stop_play_title": "當前的聲音輸出設備被改變時是否暫停播放歌曲",
"play_mediaDevice_remove_stop_play": "當前的聲音輸出設備被改變時暫停播放歌曲",
"desktop_lyric": "桌面歌詞設置",
"desktop_lyric_enable": "顯示歌詞",
"desktop_lyric_lock": "鎖定歌詞",
"desktop_lyric_always_on_top": "使歌詞總是在其他窗口之上",
"desktop_lyric_lock_screen": "不允許歌詞窗口拖出主屏幕之外",
"search": "搜索設置",
"search_hot_title": "是否顯示熱門搜索",
"search_hot": "熱門搜索",
"search_history_title": "是否顯示歷史搜索記錄",
"search_history": "搜索歷史",
"search_focus_search_box_title": "啟動時是否自動聚焦搜索框",
"search_focus_search_box": "啟動時是否聚焦搜索框",
"search_hot": "顯示熱門搜索",
"search_history": "顯示歷史搜索記錄",
"search_focus_search_box": "啟動時自動聚焦搜索框",
"list": "列表設置",
"list_source_title": "是否顯示歌曲源",
"list_source": "是否顯示歌曲源(僅對我的音樂分類有效)",
"list_scroll_title": "是否記住播放列表滾動條位置",
"list_scroll": "記住列表滾動位置(僅對我的音樂分類有效)",
"list_source": "顯示歌曲源(僅對我的音樂分類有效)",
"list_scroll": "記住播放列表滾動條位置(僅對我的音樂分類有效)",
"download": "下載設置",
"download_enable": "是否啟用下載功能",
"download_path_title": "下載歌曲保存的路徑",
@@ -79,6 +74,7 @@
"download_name2": "歌手 - 歌名",
"download_name3": "歌名",
"download_select_save_path": "選擇歌曲保存路徑",
"hot_key": "快捷鍵設置",
"hot_key_local_title": "軟件內快捷鍵",
"hot_key_global_title": "全局快捷鍵",
@@ -98,15 +94,18 @@
"hot_key_desktop_lyric_toggle_visible": "開/關桌面歌詞",
"hot_key_desktop_lyric_toggle_lock": "桌面歌詞鎖定切換",
"hot_key_desktop_lyric_toggle_always_top": "桌面歌詞置頂切換",
"network": "網絡設置",
"network_proxy_title": "HTTP代理設置亂設置軟件將無法聯網",
"network_proxy_host": "主機",
"network_proxy_port": "端口",
"network_proxy_username": "用戶名",
"network_proxy_password": "密碼",
"odc": "強迫症設置",
"odc_clear_search_input": "離開搜索界面時清空搜索框",
"odc_clear_search_list": "離開搜索界面時清空搜索列表",
"backup": "備份與恢復",
"backup_part": "部分數據(列表數據包括試聽列表、收藏列表、用戶自定義列表,設置數據不包括快捷鍵設置)",
"backup_part_import_list": "導入列表",
@@ -121,15 +120,18 @@
"backup_part_import_setting_desc": "選擇配置文件",
"backup_part_export_setting_desc": "選擇設置保存位置",
"backup_part_import_list_desc": "選擇列表文件",
"backup_part_export_list_desc": "選擇列表保存位置",
"backup_part_export_list_desc": "選擇歌單保存位置",
"other": "其他",
"other_tray_theme": "托盤圖標樣式",
"other_tray_theme_native": "純色",
"other_tray_theme_origin": "原色",
"other_cache": "緩存大小(清理緩存後圖片等資源將需要重新下載,不建議清理,軟件會根據磁盤空間動態管理緩存大小)",
"other_cache_label": "軟件已使用緩存大小:",
"other_cache_label_title": "當前已用緩存",
"other_cache_clear_btn": "清理緩存",
"other_resource_cache": "資源緩存管理(圖片、音頻等緩存,清理後圖片等資源將需要重新下載,不建議清理,軟件會根據磁盤空間動態管理緩存大小)",
"other_resource_cache_label": "軟件已使用緩存大小:",
"other_resource_cache_clear_btn": "清理資源緩存",
"other_play_list_cache": "列表緩存管理(我的列表中已緩存的歌曲鏈接、播放代替源,清理後播放、下載歌曲時需要重新獲取,沒有歌曲播放相關的問題不要清理)",
"other_play_list_cache_clear_btn": "清理列表緩存信息",
"update": "軟件更新",
"update_latest_label": "最新版本:",
"update_unknown": "未知",
@@ -139,8 +141,11 @@
"update_latest": "軟件已是最新,盡情地體驗吧~🥂",
"update_open_version_modal_btn": "打開更新窗口 🚀",
"update_checking": "檢查更新中...",
"update_init": "更新初始化中...",
"update_init": "處理更新中...",
"about": "關於洛雪音樂",
"is_enable": "是否啟用",
"is_show": "是否顯示",
"click_open": "點擊打開",

View File

@@ -8,5 +8,6 @@
"tip_2": "若遇到無法打開的歌單鏈接,歡迎反饋",
"tip_3": "酷狗源不支持用歌單ID打開但支持酷狗碼打開",
"play_all": "播放",
"play_later": "稍後播放",
"add_all": "收藏"
}

View File

@@ -19,16 +19,31 @@ export default {
},
getVersionInfo2(state, retryNum = 0) {
return new Promise((resolve, reject) => {
httpGet('https://cdn.stsky.cn/lx-music/desktop/version.json', {
httpGet('https://gitee.com/lyswhut/lx-music-desktop-versions/raw/master/version.json', {
timeout: 20000,
}, (err, resp, body) => {
if (!err && !body.version) err = new Error(JSON.stringify(body))
if (err) {
return ++retryNum > 3
? reject(err)
? this.dispatch('getVersionInfo3').then(resolve).catch(reject)
: this.dispatch('getVersionInfo2', retryNum).then(resolve).catch(reject)
}
resolve(body)
})
})
},
getVersionInfo3(state, retryNum = 0) {
return new Promise((resolve, reject) => {
httpGet('https://cdn.stsky.cn/lx-music/desktop/version.json', {
timeout: 20000,
}, (err, resp, body) => {
if (err) {
return ++retryNum > 3
? reject(err)
: this.dispatch('getVersionInfo3', retryNum).then(resolve).catch(reject)
}
resolve(body)
})
})
},
}

View File

@@ -1,3 +1,5 @@
import musicSdk from '../../utils/music'
let allList = {}
window.allList = allList
@@ -48,7 +50,12 @@ const getters = {
// actions
const actions = {
getOtherSource({ state, commit }, musicInfo) {
return (musicInfo.otherSource && musicInfo.otherSource.length ? Promise.resolve(musicInfo.otherSource) : musicSdk.findMusic(musicInfo)).then(otherSource => {
commit('setOtherSource', { musicInfo, otherSource })
return otherSource
})
},
}
// mitations
@@ -142,9 +149,9 @@ const mutations = {
if (!targetList) return
targetList.list.splice(0, targetList.list.length)
},
updateMusicInfo(state, { id, index, data }) {
updateMusicInfo(state, { id, index, data, musicInfo = {} }) {
let targetList = allList[id]
if (!targetList) return
if (!targetList) return Object.assign(musicInfo, data)
Object.assign(targetList.list[index], data)
},
createUserList(state, { name, id = `userlist_${Date.now()}`, list = [], source, sourceListId }) {
@@ -191,6 +198,22 @@ const mutations = {
targetList.list.splice(sortNum - 1, 0, ...musicInfos)
},
clearCache() {
const lists = Object.values(allList)
for (const { list } of lists) {
for (const item of list) {
if (item.otherSource) item.otherSource = null
if (item.typeUrl['128k']) delete item.typeUrl['128k']
if (item.typeUrl['320k']) delete item.typeUrl['320k']
if (item.typeUrl.flac) delete item.typeUrl.flac
if (item.typeUrl.wav) delete item.typeUrl.wav
// if (item.lxlrc == '') item.lxlrc = null
}
}
},
setOtherSource(state, { musicInfo, otherSource }) {
musicInfo.otherSource = otherSource
},
}
export default {

View File

@@ -1,4 +1,6 @@
import path from 'path'
import music from '../../utils/music'
import { getRandom, checkPath } from '../../utils'
// state
const state = {
@@ -10,20 +12,137 @@ const state = {
changePlay: false,
isShowPlayerDetail: false,
playedList: [],
playMusicInfo: null,
tempPlayList: [],
}
let urlRequest
let picRequest
let lrcRequest
// let picRequest
// let lrcRequest
const filterList = async({ playedList, listInfo, savePath, commit }) => {
// if (this.list.listName === null) return
let list
let canPlayList = []
const filteredPlayedList = playedList.filter(({ listId, isTempPlay }) => listInfo.id === listId && !isTempPlay).map(({ musicInfo }) => musicInfo)
if (listInfo.id == 'download') {
list = []
for (const item of listInfo.list) {
const filePath = path.join(savePath, item.fileName)
if (!await checkPath(filePath) || !item.isComplate || /\.ape$/.test(filePath)) continue
canPlayList.push(item)
// 排除已播放音乐
let index = filteredPlayedList.indexOf(item)
if (index > -1) {
filteredPlayedList.splice(index, 1)
continue
}
list.push(item)
}
} else {
list = listInfo.list.filter(s => {
// if (!assertApiSupport(s.source)) return false
canPlayList.push(s)
let index = filteredPlayedList.indexOf(s)
if (index > -1) {
filteredPlayedList.splice(index, 1)
return false
}
return true
})
}
if (!list.length && playedList.length) {
commit('clearPlayedList')
return canPlayList
}
return list
}
const getPic = function(musicInfo, retryedSource = [], originMusic) {
// console.log(musicInfo.source)
return music[musicInfo.source].getPic(musicInfo).promise.catch(err => {
if (!retryedSource.includes(musicInfo.source)) retryedSource.push(musicInfo.source)
return this.dispatch('list/getOtherSource', musicInfo).then(otherSource => {
if (!originMusic) originMusic = musicInfo
console.log('find otherSource', otherSource)
if (otherSource.length) {
for (const item of otherSource) {
if (retryedSource.includes(item.source)) continue
console.log('try toggle to: ', item.source, item.name, item.singer, item.interval)
return getPic.call(this, item, retryedSource, originMusic)
}
}
return Promise.reject(err)
})
})
}
const getLyric = function(musicInfo, retryedSource = [], originMusic) {
return music[musicInfo.source].getLyric(musicInfo).promise.catch(err => {
if (!retryedSource.includes(musicInfo.source)) retryedSource.push(musicInfo.source)
return this.dispatch('list/getOtherSource', musicInfo).then(otherSource => {
if (!originMusic) originMusic = musicInfo
console.log('find otherSource', otherSource)
if (otherSource.length) {
for (const item of otherSource) {
if (retryedSource.includes(item.source)) continue
console.log('try toggle to: ', item.source, item.name, item.singer, item.interval)
return getLyric.call(this, item, retryedSource, originMusic)
}
}
return Promise.reject(err)
})
})
}
// getters
const getters = {
list: state => state.listInfo.list,
listId: state => state.listInfo.id,
changePlay: satte => satte.changePlay,
playIndex: state => state.playIndex,
playInfo(state) {
if (state.playMusicInfo == null) return { listId: null, playIndex: -1, playListId: null, listPlayIndex: -1, isPlayList: false, musicInfo: null }
const playListId = state.listInfo.id
let listId = state.playMusicInfo.listId
const isTempPlay = !!state.playMusicInfo.isTempPlay
const isPlayList = listId === playListId
let playIndex = -1
let listPlayIndex = state.playIndex
if (listId != '__temp__') {
if (isPlayList) {
playIndex = state.listInfo.list.indexOf(state.playMusicInfo.musicInfo)
if (!isTempPlay) listPlayIndex = playIndex
} else {
let list = window.allList[listId]
if (list) playIndex = list.list.indexOf(state.playMusicInfo.musicInfo)
}
}
// console.log({
// listId,
// playIndex,
// playListId,
// listPlayIndex,
// isPlayList,
// isTempPlay,
// musicInfo: state.playMusicInfo.musicInfo,
// })
return {
listId,
playIndex,
playListId,
listPlayIndex,
isPlayList,
isTempPlay,
musicInfo: state.playMusicInfo.musicInfo,
}
},
isShowPlayerDetail: state => state.isShowPlayerDetail,
playMusicInfo: state => state.playMusicInfo,
playedList: state => state.playedList,
tempPlayList: state => state.tempPlayList,
}
// actions
@@ -36,48 +155,174 @@ const actions = {
// return Promise.reject(new Error('该歌曲没有可播放的音频'))
}
if (urlRequest && urlRequest.cancelHttp) urlRequest.cancelHttp()
if (musicInfo.typeUrl[type] && !isRefresh) return Promise.resolve()
urlRequest = music[musicInfo.source].getMusicUrl(musicInfo, type)
return urlRequest.promise.then(result => {
if (originMusic) commit('setUrl', { musicInfo: originMusic, url: result.url, type })
commit('setUrl', { musicInfo, url: result.url, type })
if (musicInfo.typeUrl[type] && !isRefresh) return Promise.resolve(musicInfo.typeUrl[type])
try {
urlRequest = music[musicInfo.source].getMusicUrl(musicInfo, type)
} catch (err) {
return Promise.reject(err)
}
return urlRequest.promise.then(({ url }) => {
if (originMusic) commit('setUrl', { musicInfo: originMusic, url, type })
commit('setUrl', { musicInfo, url, type })
urlRequest = null
return url
}).catch(err => {
urlRequest = null
return Promise.reject(err)
})
},
getPic({ commit, state }, musicInfo) {
if (picRequest && picRequest.cancelHttp) picRequest.cancelHttp()
picRequest = music[musicInfo.source].getPic(musicInfo)
return picRequest.promise.then(url => {
picRequest = null
// if (picRequest && picRequest.cancelHttp) picRequest.cancelHttp()
// picRequest = music[musicInfo.source].getPic(musicInfo)
return getPic.call(this, musicInfo).then(url => {
// picRequest = null
commit('getPic', { musicInfo, url })
}).catch(err => {
picRequest = null
// picRequest = null
return Promise.reject(err)
})
},
getLrc({ commit, state }, musicInfo) {
if (lrcRequest && lrcRequest.cancelHttp) lrcRequest.cancelHttp()
// if (lrcRequest && lrcRequest.cancelHttp) lrcRequest.cancelHttp()
if (musicInfo.lrc && musicInfo.tlrc != null) {
if (musicInfo.lrc.startsWith('\ufeff[id:$00000000]')) {
let str = musicInfo.lrc.replace('\ufeff[id:$00000000]\n', '')
commit('setLrc', { musicInfo, lyric: str, tlyric: musicInfo.tlrc })
commit('setLrc', { musicInfo, lyric: str, tlyric: musicInfo.tlrc, lxlyric: musicInfo.tlrc })
} else if (musicInfo.lrc.startsWith('[id:$00000000]')) {
let str = musicInfo.lrc.replace('[id:$00000000]\n', '')
commit('setLrc', { musicInfo, lyric: str, tlyric: musicInfo.tlrc, lxlyric: musicInfo.tlrc })
}
if ((musicInfo.lxlrc == null && musicInfo.source != 'kg') || musicInfo.lxlrc != null) {
return Promise.resolve()
}
return Promise.resolve()
}
lrcRequest = music[musicInfo.source].getLyric(musicInfo)
return lrcRequest.promise.then(({ lyric, tlyric }) => {
lrcRequest = null
commit('setLrc', { musicInfo, lyric, tlyric })
// lrcRequest = music[musicInfo.source].getLyric(musicInfo)
return getLyric.call(this, musicInfo).then(({ lyric, tlyric, lxlyric }) => {
// lrcRequest = null
commit('setLrc', { musicInfo, lyric, tlyric, lxlyric })
}).catch(err => {
lrcRequest = null
// lrcRequest = null
return Promise.reject(err)
})
},
async playPrev({ state, rootState, commit, getters }) {
const currentListId = state.listInfo.id
const currentList = state.listInfo.list
if (state.playedList.length) {
// 从已播放列表移除播放列表已删除的歌曲
let index
for (index = state.playedList.indexOf(state.playMusicInfo) - 1; index > -1; index--) {
const playMusicInfo = state.playedList[index]
if (playMusicInfo.listId == currentListId && !currentList.includes(playMusicInfo.musicInfo)) {
commit('removePlayedList', index)
continue
}
break
}
if (index > -1) {
commit('setPlayMusicInfo', state.playedList[index])
return
}
}
let filteredList = await filterList({
listInfo: state.listInfo,
playedList: state.playedList,
savePath: rootState.setting.download.savePath,
commit,
})
if (!filteredList.length) return commit('setPlayMusicInfo', null)
const playInfo = getters.playInfo
let currentIndex = filteredList.indexOf(currentList[playInfo.listPlayIndex])
if (currentIndex == -1) currentIndex = 0
let nextIndex = currentIndex
if (!playInfo.isTempPlay) {
switch (rootState.setting.player.togglePlayMethod) {
case 'random':
nextIndex = getRandom(0, filteredList.length)
break
case 'listLoop':
case 'list':
nextIndex = currentIndex === 0 ? filteredList.length - 1 : currentIndex - 1
break
case 'singleLoop':
break
default:
nextIndex = -1
return
}
if (nextIndex < 0) return
}
commit('setPlayMusicInfo', {
musicInfo: filteredList[nextIndex],
listId: currentListId,
})
},
async playNext({ state, rootState, commit, getters }) {
if (state.tempPlayList.length) {
const playMusicInfo = state.tempPlayList[0]
commit('removeTempPlayList', 0)
commit('setPlayMusicInfo', playMusicInfo)
return
}
const currentListId = state.listInfo.id
const currentList = state.listInfo.list
if (state.playedList.length) {
// 从已播放列表移除播放列表已删除的歌曲
let index
for (index = state.playedList.indexOf(state.playMusicInfo) + 1; index < state.playedList.length; index++) {
const playMusicInfo = state.playedList[index]
if (playMusicInfo.listId == currentListId && !currentList.includes(playMusicInfo.musicInfo)) {
commit('removePlayedList', index)
continue
}
break
}
if (index < state.playedList.length) {
commit('setPlayMusicInfo', state.playedList[index])
return
}
}
let filteredList = await filterList({
listInfo: state.listInfo,
playedList: state.playedList,
savePath: rootState.setting.download.savePath,
commit,
})
if (!filteredList.length) return commit('setPlayMusicInfo', null)
const playInfo = getters.playInfo
const currentIndex = filteredList.indexOf(currentList[playInfo.listPlayIndex])
let nextIndex = currentIndex
switch (rootState.setting.player.togglePlayMethod) {
case 'listLoop':
nextIndex = currentIndex === filteredList.length - 1 ? 0 : currentIndex + 1
break
case 'random':
nextIndex = getRandom(0, filteredList.length)
break
case 'list':
nextIndex = currentIndex === filteredList.length - 1 ? -1 : currentIndex + 1
break
case 'singleLoop':
break
default:
nextIndex = -1
return
}
if (nextIndex < 0) return
commit('setPlayMusicInfo', {
musicInfo: filteredList[nextIndex],
listId: currentListId,
})
},
}
@@ -92,25 +337,31 @@ const mutations = {
setLrc(state, datas) {
datas.musicInfo.lrc = datas.lyric
datas.musicInfo.tlrc = datas.tlyric
datas.musicInfo.lxlrc = datas.lxlyric
},
setList(state, { list, index }) {
state.playMusicInfo = {
musicInfo: list.list[index],
listId: list.id,
}
state.listInfo = list
state.playIndex = index
state.changePlay = true
// console.log(state.playMusicInfo)
if (state.playedList.length) this.commit('player/clearPlayedList')
if (state.tempPlayList.length) this.commit('player/clearTempPlayeList')
},
setPlayIndex(state, index) {
state.playIndex = index
state.changePlay = true
// console.log(state.changePlay)
},
fixPlayIndex(state, index) {
state.playIndex = index
setChangePlay(state) {
state.changePlay = true
},
resetChangePlay(state) {
state.changePlay = false
},
setPlayedList(state, item) {
// console.log(item)
if (state.playedList.includes(item)) return
state.playedList.push(item)
},
@@ -118,11 +369,35 @@ const mutations = {
state.playedList.splice(index, 1)
},
clearPlayedList(state) {
state.playedList = []
state.playedList.splice(0, state.playedList.length)
},
visiblePlayerDetail(state, visible) {
state.isShowPlayerDetail = visible
},
setTempPlayList(state, list) {
state.tempPlayList.push(...list.map(({ musicInfo, listId }) => ({ musicInfo, listId, isTempPlay: true })))
if (!state.playMusicInfo) this.commit('player/playNext')
},
removeTempPlayList(state, index) {
state.tempPlayList.splice(index, 1)
},
clearTempPlayeList(state) {
state.tempPlayList.splice(0, state.tempPlayList.length)
},
setPlayMusicInfo(state, playMusicInfo) {
let playIndex = state.playIndex
if (playMusicInfo == null) {
playIndex = -1
} else {
let listId = playMusicInfo.listId
if (listId != '__temp__' && listId === state.listInfo.id) playIndex = state.listInfo.list.indexOf(state.playMusicInfo.musicInfo)
}
state.playMusicInfo = playMusicInfo
state.playIndex = playIndex
state.changePlay = true
},
}
export default {

View File

@@ -127,7 +127,7 @@ const actions = {
}
}))
}
Promise.all(task).then(results => commit('setLists', { results, page }))
return Promise.all(task).then(results => commit('setLists', { results, page }))
} else {
return music[rootState.setting.search.searchSource].musicSearch.search(text, page, limit).catch(error => {
console.log(error)

View File

@@ -232,6 +232,7 @@ export const objectDeepMerge = (target, source, mergedObj) => {
* @param {*} url
*/
export const openUrl = url => {
if (!/^https?:\/\//.test(url)) return
shell.openExternal(url)
}
@@ -395,3 +396,14 @@ export const getPlayList = () => rendererInvoke(NAMES.mainWindow.get_playlist).c
return rendererInvoke(NAMES.mainWindow.get_playlist, true)
})
// 解析URL参数为对象
export const parseUrlParams = str => {
const params = {}
if (typeof str !== 'string') return params
const paramsArr = str.split('&')
for (const param of paramsArr) {
let [key, value] = param.split('=')
params[key] = value
}
return params
}

View File

@@ -0,0 +1,291 @@
const { getNow, TimeoutTools } = require('./utils')
// const fontFormateRxp = /(?=<\d+,\d+>).*?/g
const fontSplitRxp = /(?=<\d+,\d+>).*?/g
const timeRxp = /<(\d+),(\d+)>/
// Create animation
const createAnimation = (dom, duration) => new window.Animation(new window.KeyframeEffect(dom, [
{ backgroundSize: '0 100%' },
{ backgroundSize: '100% 100%' },
], {
duration,
easing: 'linear',
},
), document.timeline)
// https://jsfiddle.net/ceqpnbky/
// https://jsfiddle.net/ceqpnbky/1/
module.exports = class FontPlayer {
constructor({ lyric = '', translationLyric = '', lineClassName = '', fontClassName = '', translationClassName = '', lineModeClassName = '', shadowContent = false, shadowClassName = '' }) {
this.lyric = lyric
this.translationLyric = translationLyric
this.lineClassName = lineClassName
this.fontClassName = fontClassName
this.translationClassName = translationClassName
this.lineModeClassName = lineModeClassName
this.shadowContent = shadowContent
this.shadowClassName = shadowClassName
this.isPlay = false
this.curFontNum = 0
this.maxFontNum = 0
this._performanceTime = 0
this._performanceOffsetTime = 0
this.fontContent = null
this.timeoutTools = new TimeoutTools()
this.waitPlayTimeout = new TimeoutTools()
this._init()
}
_init() {
if (this.lyric == null) this.lyric = ''
this.isLineMode = false
this.lineContent = document.createElement('div')
if (this.lineClassName) this.lineContent.classList.add(this.lineClassName)
this.fontContent = document.createElement('div')
this.fontContent.style = 'position:relative;display:inline-block;'
if (this.fontClassName) this.fontContent.classList.add(this.fontClassName)
if (this.shadowContent) {
this.fontShadowContent = document.createElement('div')
this.fontShadowContent.style = 'position:absolute;top:0;left:0;width:100%;z-index:-1;'
this.fontShadowContent.className = this.shadowClassName
this.fontContent.appendChild(this.fontShadowContent)
}
this.lineContent.appendChild(this.fontContent)
if (this.translationLyric) {
this.translationContent = document.createElement('div')
this.translationContent.style = 'position:relative;display:inline-block;'
this.translationContent.className = this.translationClassName
this.translationContent.textContent = this.translationLyric
this.lineContent.appendChild(document.createElement('br'))
this.lineContent.appendChild(this.translationContent)
if (this.shadowContent) {
this.translationShadowContent = document.createElement('div')
this.translationShadowContent.style = 'position:absolute;top:0;left:0;width:100%;z-index:-1;'
this.translationShadowContent.className = this.shadowClassName
this.translationShadowContent.textContent = this.translationLyric
this.translationContent.appendChild(this.translationShadowContent)
}
}
this._parseLyric()
}
_parseLyric() {
const fonts = this.lyric.split(fontSplitRxp)
// console.log(fonts)
this.maxFontNum = fonts.length - 1
this.fonts = []
let text
for (const font of fonts) {
text = font.replace(timeRxp, '')
if (RegExp.$2 == '') return this._handleLineParse()
const time = parseInt(RegExp.$2)
const dom = document.createElement('span')
let shadowDom
dom.textContent = text
const animation = createAnimation(dom, time)
this.fontContent.appendChild(dom)
if (this.shadowContent) {
shadowDom = document.createElement('span')
shadowDom.textContent = text
this.fontShadowContent.appendChild(shadowDom)
}
// dom.style = shadowDom.style = this.fontStyle
// dom.className = shadowDom.className = this.fontClassName
this.fonts.push({
text,
startTime: parseInt(RegExp.$1),
time,
dom,
shadowDom,
animation,
})
}
// console.log(this.fonts)
}
_handleLineParse() {
this.isLineMode = true
const dom = document.createElement('span')
let shadowDom
dom.classList.add(this.lineModeClassName)
dom.textContent = this.lyric
if (this.shadowContent) {
shadowDom = document.createElement('span')
shadowDom.textContent = this.lyric
this.fontShadowContent.appendChild(shadowDom)
}
this.fontContent.appendChild(dom)
this.fonts.push({
text: this.lyric,
dom,
shadowDom,
})
}
_currentTime() {
return getNow() - this._performanceTime + this._performanceOffsetTime
}
_findcurFontNum(curTime) {
const length = this.fonts.length
for (let index = 0; index < length; index++) if (curTime <= this.fonts[index].startTime) return index === 0 ? 0 : index - 1
return length - 1
}
_handlePlayMaxFontNum() {
let curFont = this.fonts[this.curFontNum]
// console.log(curFont.text)
const currentTime = this._currentTime()
const driftTime = currentTime - curFont.startTime
if (currentTime > curFont.startTime + curFont.time) {
this._handlePlayFont(curFont, driftTime, true)
this.pause()
} else {
this._handlePlayFont(curFont, driftTime)
this.isPlay = false
}
}
_handlePlayFont(font, currentTime, toFinishe) {
switch (font.animation.playState) {
case 'finished':
break
case 'idle':
font.dom.style.backgroundSize = '100% 100%'
if (!toFinishe) font.animation.play()
break
default:
if (toFinishe) {
font.animation.cancel()
} else {
font.animation.currentTime = currentTime
font.animation.play()
}
break
}
}
_handlePlayLine(isPlayed) {
this.isPlay = false
this.fonts[0].dom.style.backgroundSize = isPlayed ? '100% 100%' : '100% 0'
}
_handlePauseFont(font) {
if (font.animation.playState == 'running') font.animation.pause()
}
_refresh() {
this.curFontNum++
// console.log('curFontNum time', this.fonts[this.curFontNum].time)
if (this.curFontNum === this.maxFontNum) return this._handlePlayMaxFontNum()
let curFont = this.fonts[this.curFontNum]
let nextFont = this.fonts[this.curFontNum + 1]
// console.log(curFont, nextFont, this.curFontNum, this.maxFontNum)
const currentTime = this._currentTime()
// console.log(curFont.text)
const driftTime = currentTime - curFont.startTime
// console.log(currentTime, driftTime)
if (driftTime >= 0 || this.curFontNum == 0) {
this.delay = nextFont.startTime - curFont.startTime - driftTime
if (this.delay > 0) {
this._handlePlayFont(curFont, driftTime)
this.timeoutTools.start(() => {
this._refresh()
}, this.delay)
return
}
} else if (this.curFontNum == 0) {
this.curFontNum--
this.waitPlayTimeout.start(() => {
this._refresh()
}, -driftTime)
return
}
this.curFontNum = this._findcurFontNum(currentTime)
for (let i = 0; i < this.curFontNum; i++) this._handlePlayFont(this.fonts[i], 0, true)
this.curFontNum--
this._refresh()
}
play(curTime = 0) {
// console.log('play', curTime)
if (!this.fonts.length) return
this.pause()
if (this.isLineMode) return this._handlePlayLine(true)
this.isPlay = true
this._performanceTime = getNow() - curTime
this._performanceOffsetTime = 0
if (this._performanceTime < 0) {
this._performanceOffsetTime = -this._performanceTime
this._performanceTime = 0
}
this.curFontNum = this._findcurFontNum(curTime)
for (let i = this.curFontNum; i > -1; i--) {
this._handlePlayFont(this.fonts[i], 0, true)
}
for (let i = this.curFontNum, len = this.fonts.length; i < len; i++) {
let font = this.fonts[i]
font.animation.cancel()
font.dom.style.backgroundSize = '0 100%'
}
this.curFontNum--
this._refresh()
}
pause() {
if (!this.isPlay) return
this.isPlay = false
this.timeoutTools.clear()
this.waitPlayTimeout.clear()
this._handlePauseFont(this.fonts[this.curFontNum])
if (this.curFontNum === this.maxLine) return
const curFontNum = this._findcurFontNum(this._currentTime())
if (this.curFontNum === curFontNum) return
for (let i = 0; i < this.curFontNum; i++) this._handlePlayFont(this.fonts[i], 0, true)
}
finish() {
this.pause()
if (this.isLineMode) return this._handlePlayLine(true)
for (const font of this.fonts) {
font.animation.cancel()
font.dom.style.backgroundSize = '100% 100%'
}
this.curFontNum = this.maxFontNum
}
reset() {
this.pause()
if (this.isLineMode) return this._handlePlayLine(false)
for (const font of this.fonts) {
font.animation.cancel()
font.dom.style.backgroundSize = '0 100%'
}
this.curFontNum = 0
}
}

View File

@@ -0,0 +1,166 @@
const LinePlayer = require('./line-player')
const FontPlayer = require('./font-player')
const fontTimeExp = /<(\d+),(\d+)>/g
module.exports = class Lyric {
constructor({
lyric = '',
translationLyric = '',
offset = 150,
lineClassName = '',
fontClassName = 'font',
translationClassName = 'translation',
activeLineClassName = 'active',
lineModeClassName = 'line',
shadowClassName = '',
shadowContent = false,
onPlay = function() { },
onSetLyric = function() { },
}) {
this.lyric = lyric
this.translationLyric = translationLyric
this.offset = offset
this.onPlay = onPlay
this.onSetLyric = onSetLyric
this.lineClassName = lineClassName
this.fontClassName = fontClassName
this.translationClassName = translationClassName
this.activeLineClassName = activeLineClassName
this.lineModeClassName = lineModeClassName
this.shadowClassName = shadowClassName
this.shadowContent = shadowContent
this.playingLineNum = -1
this.isLineMode = false
}
_init() {
this.playingLineNum = -1
this.isLineMode = false
if (this.linePlayer) {
this.linePlayer.setLyric(this.lyric, this.translationLyric)
} else {
this.linePlayer = new LinePlayer({
lyric: this.lyric,
translationLyric: this.translationLyric,
offset: this.offset,
onPlay: this._handleLinePlayerOnPlay,
onSetLyric: this._handleLinePlayerOnSetLyric,
})
}
}
_handleLinePlayerOnPlay = (num, text, curTime) => {
if (this.isLineMode) {
if (num < this.playingLineNum + 1) {
for (let i = this.playingLineNum; i > num - 1; i--) {
const font = this._lineFonts[i]
font.reset()
font.lineContent.classList.remove(this.activeLineClassName)
}
} else if (num > this.playingLineNum + 1) {
for (let i = Math.max(this.playingLineNum, 0); i < num; i++) {
const font = this._lineFonts[i]
font.reset()
font.lineContent.classList.remove(this.activeLineClassName)
}
} else if (this.playingLineNum > -1) {
const font = this._lineFonts[this.playingLineNum]
font.reset()
font.lineContent.classList.remove(this.activeLineClassName)
}
} else {
if (num < this.playingLineNum + 1) {
for (let i = this.playingLineNum; i > num - 1; i--) {
const font = this._lineFonts[i]
font.lineContent.classList.remove(this.activeLineClassName)
font.reset()
}
} else if (num > this.playingLineNum + 1) {
for (let i = Math.max(this.playingLineNum, 0); i < num; i++) {
const font = this._lineFonts[i]
font.lineContent.classList.remove(this.activeLineClassName)
font.finish()
}
} else if (this.playingLineNum > -1) {
const font = this._lineFonts[this.playingLineNum]
font.lineContent.classList.remove(this.activeLineClassName)
}
}
this.playingLineNum = num
const font = this._lineFonts[num]
font.lineContent.classList.add(this.activeLineClassName)
font.play(curTime - this._lines[num].time)
this.onPlay(num, this._lines[num].text)
}
_handleLinePlayerOnSetLyric = lyricLines => {
// console.log(lyricLines)
// this._lines = lyricsLines
this.isLineMode = lyricLines.length && !/^<\d+,\d+>/.test(lyricLines[0].text)
this._lineFonts = []
if (this.isLineMode) {
this._lines = lyricLines.map(line => {
const fontPlayer = new FontPlayer({
lyric: line.text,
translationLyric: line.translation,
lineClassName: this.lineClassName,
fontClassName: this.fontClassName,
translationClassName: this.translationClassName,
lineModeClassName: this.lineModeClassName,
shadowClassName: this.shadowClassName,
shadowContent: this.shadowContent,
})
this._lineFonts.push(fontPlayer)
return {
text: line.text,
time: line.time,
dom_line: fontPlayer.lineContent,
}
})
} else {
this._lines = lyricLines.map(line => {
const fontPlayer = new FontPlayer({
lyric: line.text,
translationLyric: line.translation,
lineClassName: this.lineClassName,
fontClassName: this.fontClassName,
translationClassName: this.translationClassName,
shadowClassName: this.shadowClassName,
shadowContent: this.shadowContent,
})
this._lineFonts.push(fontPlayer)
return {
text: line.text.replace(fontTimeExp, ''),
time: line.time,
dom_line: fontPlayer.lineContent,
}
})
}
this.onSetLyric(this._lines)
}
play(curTime) {
if (!this.linePlayer) return
this.linePlayer.play(curTime)
}
pause() {
if (!this.linePlayer) return
this.linePlayer.pause()
if (this.playingLineNum > -1) this._lineFonts[this.playingLineNum].pause()
}
setLyric(lyric, translationLyric) {
this.lyric = lyric
this.translationLyric = translationLyric
this._init()
}
}

View File

@@ -0,0 +1,177 @@
const { getNow, TimeoutTools } = require('./utils')
const timeExp = /^\[([\d:.]*)\]{1}/g
const tagRegMap = {
title: 'ti',
artist: 'ar',
album: 'al',
offset: 'offset',
by: 'by',
}
const timeoutTools = new TimeoutTools()
module.exports = class LinePlayer {
constructor({ lyric = '', translationLyric = '', offset = 0, onPlay = function() { }, onSetLyric = function() { } } = {}) {
this.lyric = lyric
this.translationLyric = translationLyric
this.tags = {}
this.lines = null
this.translationLines = null
this.onPlay = onPlay
this.onSetLyric = onSetLyric
this.isPlay = false
this.curLineNum = 0
this.maxLine = 0
this.offset = offset
this.isOffseted = false
this._performanceTime = 0
this._performanceOffsetTime = 0
this._init()
}
_init() {
if (this.lyric == null) this.lyric = ''
if (this.translationLyric == null) this.translationLyric = ''
this._initTag()
this._initLines()
this.onSetLyric(this.lines)
}
_initTag() {
for (let tag in tagRegMap) {
const matches = this.lyric.match(new RegExp(`\\[${tagRegMap[tag]}:([^\\]]*)]`, 'i'))
this.tags[tag] = (matches && matches[1]) || ''
}
}
_initLines() {
this.lines = []
this.translationLines = []
const lines = this.lyric.split('\n')
const linesMap = {}
// const translationLines = this.translationLyric.split('\n')
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim()
let result = timeExp.exec(line)
if (result) {
const text = line.replace(timeExp, '').trim()
if (text) {
const timeStr = RegExp.$1
const timeArr = timeStr.split(':')
if (timeArr.length < 3) timeArr.unshift(0)
if (timeArr[2].indexOf('.') > -1) {
timeArr.push(...timeArr[2].split('.'))
timeArr.splice(2, 1)
}
linesMap[timeStr] = {
time: parseInt(timeArr[0]) * 60 * 60 * 1000 + parseInt(timeArr[1]) * 60 * 1000 + parseInt(timeArr[2]) * 1000 + parseInt(timeArr[3] || 0),
text,
}
}
}
}
const translationLines = this.translationLyric.split('\n')
for (let i = 0; i < translationLines.length; i++) {
const line = translationLines[i].trim()
let result = timeExp.exec(line)
if (result) {
const text = line.replace(timeExp, '').trim()
if (text) {
const timeStr = RegExp.$1
const targetLine = linesMap[timeStr]
if (targetLine) targetLine.translation = text
}
}
}
this.lines = Object.values(linesMap)
this.lines.sort((a, b) => {
return a.time - b.time
})
this.maxLine = this.lines.length - 1
}
_currentTime() {
return getNow() - this._performanceTime + this._performanceOffsetTime
}
_findCurLineNum(curTime) {
const length = this.lines.length
for (let index = 0; index < length; index++) if (curTime <= this.lines[index].time) return index === 0 ? 0 : index - 1
return length - 1
}
_handleMaxLine() {
this.onPlay(this.curLineNum, this.lines[this.curLineNum].text, this._currentTime())
this.pause()
}
_refresh() {
this.curLineNum++
// console.log('curLineNum time', this.lines[this.curLineNum].time)
let curLine = this.lines[this.curLineNum]
let nextLine = this.lines[this.curLineNum + 1]
const currentTime = this._currentTime()
const driftTime = currentTime - curLine.time
if (driftTime >= 0 || this.curLineNum === 0) {
if (this.curLineNum === this.maxLine) return this._handleMaxLine()
this.delay = nextLine.time - curLine.time - driftTime
if (this.delay > 0) {
if (!this.isOffseted && this.delay >= this.offset) {
this._performanceOffsetTime += this.offset
this.delay -= this.offset
this.isOffseted = true
}
timeoutTools.start(() => {
this._refresh()
}, this.delay)
this.onPlay(this.curLineNum, curLine.text, currentTime)
return
}
}
this.curLineNum = this._findCurLineNum(currentTime) - 1
this._refresh()
}
play(curTime = 0) {
if (!this.lines.length) return
this.pause()
this.isPlay = true
this._performanceOffsetTime = 0
this._performanceTime = getNow() - curTime
if (this._performanceTime < 0) {
this._performanceOffsetTime = -this._performanceTime
this._performanceTime = 0
}
this.curLineNum = this._findCurLineNum(curTime) - 1
this._refresh()
}
pause() {
if (!this.isPlay) return
this.isPlay = false
this.isOffseted = false
timeoutTools.clear()
if (this.curLineNum === this.maxLine) return
const currentTime = this._currentTime()
const curLineNum = this._findCurLineNum(currentTime)
if (this.curLineNum !== curLineNum) {
this.curLineNum = curLineNum
this.onPlay(curLineNum, this.lines[curLineNum].text, currentTime)
}
}
setLyric(lyric, translationLyric) {
// console.log(translationLyric)
if (this.isPlay) this.pause()
this.lyric = lyric
this.translationLyric = translationLyric
this._init()
}
}

View File

@@ -0,0 +1,50 @@
const getNow = exports.getNow = typeof performance == 'object' && window.performance.now ? window.performance.now.bind(window.performance) : Date.now.bind(Date)
exports.TimeoutTools = class TimeoutTools {
constructor() {
this.invokeTime = 0
this.animationFrameId = null
this.timeoutId = null
this.callback = null
this.thresholdTime = 200
}
run() {
this.animationFrameId = window.requestAnimationFrame(() => {
this.animationFrameId = null
let diff = this.invokeTime - getNow()
// console.log('diff', diff)
if (diff > 0) {
if (diff < this.thresholdTime) return this.run()
return this.timeoutId = setTimeout(() => {
this.timeoutId = null
this.run()
}, diff - this.thresholdTime)
}
// console.log('diff', diff)
this.callback(diff)
})
}
start(callback = () => {}, timeout = 0) {
// console.log(timeout)
this.callback = callback
this.invokeTime = getNow() + timeout
this.run()
}
clear() {
if (this.animationFrameId) {
window.cancelAnimationFrame(this.animationFrameId)
this.animationFrameId = null
}
if (this.timeoutId) {
window.clearTimeout(this.timeoutId)
this.timeoutId = null
}
}
}

View File

@@ -11,7 +11,6 @@ module.exports = [
tx: ['128k'],
wy: ['128k'],
mg: ['128k'],
xm: ['128k'],
// bd: ['128k'],
},
},

View File

@@ -13,6 +13,7 @@ for (const api of apiSourceInfo) {
const getAPI = source => apiList[`${window.globalObj.apiSource}_api_${source}`]
const apis = source => {
if (/^user_api/.test(window.globalObj.apiSource)) return window.globalObj.userApi.apis[source]
let api = getAPI(source)
if (api) return api
throw new Error('Api is not found')

View File

@@ -11,9 +11,9 @@ export default {
total: 0,
page: 0,
allPage: 1,
musicSearch(str, page) {
musicSearch(str, page, limit) {
if (searchRequest && searchRequest.cancelHttp) searchRequest.cancelHttp()
searchRequest = httpFetch(`http://tingapi.ting.baidu.com/v1/restserver/ting?from=android&version=5.6.5.6&method=baidu.ting.search.merge&format=json&query=${encodeURIComponent(str)}&page_no=${page}&page_size=${this.limit}&type=0&data_source=0&use_cluster=1`)
searchRequest = httpFetch(`http://tingapi.ting.baidu.com/v1/restserver/ting?from=android&version=5.6.5.6&method=baidu.ting.search.merge&format=json&query=${encodeURIComponent(str)}&page_no=${page}&page_size=${limit}&type=0&data_source=0&use_cluster=1`)
return searchRequest.promise.then(({ body }) => body)
},
handleResult(rawData) {
@@ -66,9 +66,9 @@ export default {
},
search(str, page = 1, { limit } = {}, retryNum = 0) {
if (++retryNum > 3) return Promise.reject(new Error('try max num'))
if (limit != null) this.limit = limit
if (limit == null) limit = this.limit
return this.musicSearch(str, page).then(result => {
return this.musicSearch(str, page, limit).then(result => {
if (!result || result.error_code !== 22000) return this.search(str, page, { limit }, retryNum)
let list = this.handleResult(result.result.song_info.song_list)
@@ -76,12 +76,12 @@ export default {
this.total = result.result.song_info.total
this.page = page
this.allPage = Math.ceil(this.total / this.limit)
this.allPage = Math.ceil(this.total / limit)
return Promise.resolve({
list,
allPage: this.allPage,
limit: this.limit,
limit: limit,
total: this.total,
source: 'bd',
})

View File

@@ -50,28 +50,58 @@ const sources = {
export default {
...sources,
init() {
const tasks = []
for (let source of sources.sources) {
let sm = sources[source.id]
sm && sm.init && sm.init()
sm && sm.init && tasks.push(sm.init())
}
return Promise.all(tasks)
},
supportQuality,
async findMusic(musicInfo) {
const tasks = []
const sortSingle = singer => singer.includes('、') ? singer.split('、').sort((a, b) => a.charCodeAt(0) - b.charCodeAt(0)).join('、') : singer
const sortMusic = (arr, callback) => {
const tempResult = []
for (let i = arr.length - 1; i > -1; i--) {
const item = arr[i]
if (callback(item)) {
delete item.sortedSinger
tempResult.push(item)
arr.splice(i, 1)
}
}
tempResult.reverse()
return tempResult
}
const trimStr = str => typeof str == 'string' ? str.trim() : str
const sortedSinger = String(sortSingle(musicInfo.singer)).toLowerCase()
const musicName = trimStr(musicInfo.name)
const lowerCaseName = String(musicName).toLowerCase()
const lowerCaseAlbumName = String(musicInfo.albumName).toLowerCase()
for (const source of sources.sources) {
if (!sources[source.id].musicSearch || source.id === musicInfo.source || source.id === 'xm') continue
tasks.push(sources[source.id].musicSearch.search(`${musicInfo.name} ${musicInfo.singer || ''}`.trim(), 1, { limit: 10 }).then(res => {
tasks.push(sources[source.id].musicSearch.search(`${musicName} ${musicInfo.singer || ''}`.trim(), 1, { limit: 10 }).then(res => {
for (const item of res.list) {
item.sortedSinger = String(sortSingle(item.singer)).toLowerCase()
item.name = trimStr(item.name)
item.lowerCaseName = String(item.name).toLowerCase()
item.lowerCaseAlbumName = String(item.albumName).toLowerCase()
// console.log(lowerCaseName, item.lowerCaseName)
if (
(
item.singer === musicInfo.singer &&
(item.name === musicInfo.name || item.interval === musicInfo.interval)
item.sortedSinger === sortedSinger &&
(item.lowerCaseName === lowerCaseName || item.interval === musicInfo.interval)
) ||
(
item.interval === musicInfo.interval && item.name === musicInfo.name &&
(item.singer.includes(musicInfo.singer) || musicInfo.singer.includes(item.singer))
item.interval === musicInfo.interval && item.lowerCaseName === lowerCaseName &&
(item.sortedSinger.includes(sortedSinger) || sortedSinger.includes(item.sortedSinger))
) ||
(
item.lowerCaseName === lowerCaseName && item.lowerCaseAlbumName === lowerCaseAlbumName &&
item.interval === musicInfo.interval
)
) {
return item
@@ -80,36 +110,16 @@ export default {
return null
}).catch(_ => null))
}
const result = (await Promise.all(tasks)).filter(s => s).reverse()
const result = (await Promise.all(tasks)).filter(s => s)
const newResult = []
if (result.length) {
for (let i = result.length - 1; i > -1; i--) {
const item = result[i]
if (item.singer === musicInfo.singer && item.name === musicInfo.name && item.interval === musicInfo.interval) {
newResult.push(item)
result.splice(i, 1)
}
}
for (let i = result.length - 1; i > -1; i--) {
const item = result[i]
if (item.singer === musicInfo.singer && item.interval === musicInfo.interval) {
newResult.push(item)
result.splice(i, 1)
}
}
for (let i = result.length - 1; i > -1; i--) {
const item = result[i]
if (item.name === musicInfo.name && item.singer === musicInfo.singer && item.albumName === musicInfo.albumName) {
newResult.push(item)
result.splice(i, 1)
}
}
for (let i = result.length - 1; i > -1; i--) {
const item = result[i]
if (item.singer === musicInfo.singer && item.name === musicInfo.name) {
newResult.push(item)
result.splice(i, 1)
}
newResult.push(...sortMusic(result, item => item.sortedSinger === sortedSinger && item.lowerCaseName === lowerCaseName && item.interval === musicInfo.interval))
newResult.push(...sortMusic(result, item => item.sortedSinger === sortedSinger && item.interval === musicInfo.interval))
newResult.push(...sortMusic(result, item => item.lowerCaseName === lowerCaseName && item.sortedSinger === sortedSinger && item.lowerCaseAlbumName === lowerCaseAlbumName))
newResult.push(...sortMusic(result, item => item.sortedSinger === sortedSinger && item.lowerCaseName === lowerCaseName))
for (const item of result) {
delete item.sortedSinger
delete item.lowerCaseName
}
newResult.push(...result)
}

View File

@@ -26,7 +26,7 @@ const kg = {
return pic.getPic(songInfo)
},
getMusicDetailPageUrl(songInfo) {
return `https://www.kugou.com/song/#hash=${songInfo.hash}`
return `https://www.kugou.com/song/#hash=${songInfo.hash}&album_id=${songInfo.albumId}`
},
// getPic(songInfo) {
// return apis('kg').getPic(songInfo)

View File

@@ -2,10 +2,13 @@ import { httpFetch } from '../../request'
import { decodeLyric } from './util'
import { decodeName } from '../..'
const headExp = /^.*\[id:\$\w+\]\n/
const parseLyric = str => {
str = str.replace(/(?:<\d+,\d+,\d+>|\r)/g, '')
if (str.startsWith('\ufeff[id:$00000000]')) str = str.replace('\ufeff[id:$00000000]\n', '')
str = str.replace(/\r/g, '')
if (headExp.test(str)) str = str.replace(headExp, '')
let trans = str.match(/\[language:([\w=\\/+]+)\]/)
let lyric
let tlyric
if (trans) {
str = str.replace(/\[language:[\w=\\/+]+\]\n/, '')
@@ -18,7 +21,7 @@ const parseLyric = str => {
}
}
let i = 0
let lyric = str.replace(/\[((\d+),\d+)\].*/g, str => {
let lxlyric = str.replace(/\[((\d+),\d+)\].*/g, str => {
let result = str.match(/\[((\d+),\d+)\].*/)
let time = parseInt(result[2])
let ms = time % 1000
@@ -31,11 +34,14 @@ const parseLyric = str => {
return str.replace(result[1], time)
})
tlyric = tlyric ? tlyric.join('\n') : ''
lyric = decodeName(lyric)
lxlyric = lxlyric.replace(/<(\d+,\d+),\d+>/g, '<$1>')
lxlyric = decodeName(lxlyric)
lyric = lxlyric.replace(/<\d+,\d+>/g, '')
tlyric = decodeName(tlyric)
return {
lyric,
tlyric,
lxlyric,
}
}
@@ -119,7 +125,7 @@ export default {
let requestObj = this.searchLyric(songInfo.name, songInfo.hash, songInfo._interval || this.getIntv(songInfo.interval))
requestObj.promise = requestObj.promise.then(result => {
if (!result) return { lyric: '', tlyric: '' }
if (!result) return { lyric: null, tlyric: null, lxlyric: null }
let requestObj2 = this.getLyricDownload(result.id, result.accessKey)

View File

@@ -11,69 +11,80 @@ export default {
total: 0,
page: 0,
allPage: 1,
musicSearch(str, page) {
musicSearch(str, page, limit) {
if (searchRequest && searchRequest.cancelHttp) searchRequest.cancelHttp()
searchRequest = httpFetch(`http://ioscdn.kugou.com/api/v3/search/song?keyword=${encodeURIComponent(str)}&page=${page}&pagesize=${this.limit}&showtype=10&plat=2&version=7910&tag=1&correct=1&privilege=1&sver=5`)
searchRequest = httpFetch(`http://ioscdn.kugou.com/api/v3/search/song?keyword=${encodeURIComponent(str)}&page=${page}&pagesize=${limit}&showtype=10&plat=2&version=7910&tag=1&correct=1&privilege=1&sver=5`)
return searchRequest.promise.then(({ body }) => body)
},
filterData(rawData) {
const types = []
const _types = {}
if (rawData.filesize !== 0) {
let size = sizeFormate(rawData.filesize)
types.push({ type: '128k', size, hash: rawData.hash })
_types['128k'] = {
size,
hash: rawData.hash,
}
}
if (rawData['320filesize'] !== 0) {
let size = sizeFormate(rawData['320filesize'])
types.push({ type: '320k', size, hash: rawData['320hash'] })
_types['320k'] = {
size,
hash: rawData['320hash'],
}
}
if (rawData.sqfilesize !== 0) {
let size = sizeFormate(rawData.sqfilesize)
types.push({ type: 'flac', size, hash: rawData.sqhash })
_types.flac = {
size,
hash: rawData.sqhash,
}
}
return {
singer: decodeName(rawData.singername),
name: decodeName(rawData.songname),
albumName: decodeName(rawData.album_name),
albumId: rawData.album_id,
songmid: rawData.hash,
source: 'kg',
interval: formatPlayTime(rawData.duration),
_interval: rawData.duration,
img: null,
lrc: null,
otherSource: null,
hash: rawData.hash,
types,
_types,
audioId: rawData.audio_id + '_' + rawData.hash,
typeUrl: {},
}
},
handleResult(rawData) {
// console.log(rawData)
let ids = new Set()
const list = []
rawData.forEach(item => {
if (ids.has(item.audio_id)) return
ids.add(item.audio_id)
const types = []
const _types = {}
if (item.filesize !== 0) {
let size = sizeFormate(item.filesize)
types.push({ type: '128k', size, hash: item.hash })
_types['128k'] = {
size,
hash: item.hash,
}
const key = item.audio_id + item.hash
if (ids.has(key)) return
ids.add(key)
list.push(this.filterData(item))
for (const childItem of item.group) {
const key = item.audio_id + item.hash
if (ids.has(key)) return
ids.add(key)
list.push(this.filterData(childItem))
}
if (item['320filesize'] !== 0) {
let size = sizeFormate(item['320filesize'])
types.push({ type: '320k', size, hash: item['320hash'] })
_types['320k'] = {
size,
hash: item['320hash'],
}
}
if (item.sqfilesize !== 0) {
let size = sizeFormate(item.sqfilesize)
types.push({ type: 'flac', size, hash: item.sqhash })
_types.flac = {
size,
hash: item.sqhash,
}
}
list.push({
singer: decodeName(item.singername),
name: decodeName(item.songname),
albumName: decodeName(item.album_name),
albumId: item.album_id,
songmid: item.audio_id,
source: 'kg',
interval: formatPlayTime(item.duration),
_interval: item.duration,
img: null,
lrc: null,
otherSource: null,
hash: item.hash,
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
if (limit == null) limit = this.limit
// http://newlyric.kuwo.cn/newlyric.lrc?62355680
return this.musicSearch(str, page).then(result => {
return this.musicSearch(str, page, limit).then(result => {
if (!result || result.errcode !== 0) return this.search(str, page, { limit }, retryNum)
let list = this.handleResult(result.data.info)
@@ -81,12 +92,12 @@ export default {
this.total = result.data.total
this.page = page
this.allPage = Math.ceil(this.total / this.limit)
this.allPage = Math.ceil(this.total / limit)
return Promise.resolve({
list,
allPage: this.allPage,
limit: this.limit,
limit,
total: this.total,
source: 'kg',
})

View File

@@ -37,7 +37,9 @@ export default {
requestObj.promise = requestObj.promise.then(({ body }) => {
if (body.error_code !== 0) return Promise.reject('图片获取失败')
let info = body.data[0].info
return info.imgsize ? info.image.replace('{size}', info.imgsize[0]) : info.image
const img = info.imgsize ? info.image.replace('{size}', info.imgsize[0]) : info.image
if (!img) return Promise.reject('Pic get failed')
return img
})
return requestObj
},

View File

@@ -106,7 +106,7 @@ const kw = {
},
init() {
getToken()
return getToken()
},
}

View File

@@ -16,14 +16,14 @@ export default {
page: 0,
allPage: 1,
// cancelFn: null,
musicSearch(str, page) {
musicSearch(str, page, limit) {
if (this._musicSearchRequestObj != null) {
cancelHttp(this._musicSearchRequestObj)
this._musicSearchPromiseCancelFn(new Error('取消http请求'))
}
return new Promise((resolve, reject) => {
this._musicSearchPromiseCancelFn = reject
this._musicSearchRequestObj = httpGet(`http://search.kuwo.cn/r.s?client=kt&all=${encodeURIComponent(str)}&pn=${page - 1}&rn=${this.limit}&uid=794762570&ver=kwplayer_ar_9.2.2.1&vipver=1&show_copyright_off=1&newver=1&ft=music&cluster=0&strategy=2012&encoding=utf8&rformat=json&vermerge=1&mobi=1&issubtitle=1`, (err, resp, body) => {
this._musicSearchRequestObj = httpGet(`http://search.kuwo.cn/r.s?client=kt&all=${encodeURIComponent(str)}&pn=${page - 1}&rn=${limit}&uid=794762570&ver=kwplayer_ar_9.2.2.1&vipver=1&show_copyright_off=1&newver=1&ft=music&cluster=0&strategy=2012&encoding=utf8&rformat=json&vermerge=1&mobi=1&issubtitle=1`, (err, resp, body) => {
this._musicSearchRequestObj = null
this._musicSearchPromiseCancelFn = null
if (err) {
@@ -125,9 +125,9 @@ export default {
},
search(str, page = 1, { limit } = {}, retryNum = 0) {
if (retryNum > 2) return Promise.reject(new Error('try max num'))
if (limit != null) this.limit = limit
if (limit == null) limit = this.limit
// http://newlyric.kuwo.cn/newlyric.lrc?62355680
return this.musicSearch(str, page).then(result => {
return this.musicSearch(str, page, limit).then(result => {
// console.log(result)
if (!result || (result.TOTAL !== '0' && result.SHOW === '0')) return this.search(str, page, { limit }, ++retryNum)
let list = this.handleResult(result.abslist)
@@ -136,13 +136,13 @@ export default {
this.total = parseInt(result.TOTAL)
this.page = page
this.allPage = Math.ceil(this.total / this.limit)
this.allPage = Math.ceil(this.total / limit)
return Promise.resolve({
list,
allPage: this.allPage,
total: this.total,
limit: this.limit,
limit,
source: 'kw',
})
})

View File

@@ -11,9 +11,9 @@ export default {
total: 0,
page: 0,
allPage: 1,
musicSearch(str, page) {
musicSearch(str, page, limit) {
if (searchRequest && searchRequest.cancelHttp) searchRequest.cancelHttp()
searchRequest = httpFetch(`http://jadeite.migu.cn:7090/music_search/v2/search/searchAll?sid=4f87090d01c84984a11976b828e2b02c18946be88a6b4c47bcdc92fbd40762db&isCorrect=1&isCopyright=1&searchSwitch=%7B%22song%22%3A1%2C%22album%22%3A0%2C%22singer%22%3A0%2C%22tagSong%22%3A1%2C%22mvSong%22%3A0%2C%22bestShow%22%3A1%2C%22songlist%22%3A0%2C%22lyricSong%22%3A0%7D&pageSize=${this.limit}&text=${encodeURIComponent(str)}&pageNo=${page}&sort=0`, {
searchRequest = httpFetch(`http://jadeite.migu.cn:7090/music_search/v2/search/searchAll?sid=4f87090d01c84984a11976b828e2b02c18946be88a6b4c47bcdc92fbd40762db&isCorrect=1&isCopyright=1&searchSwitch=%7B%22song%22%3A1%2C%22album%22%3A0%2C%22singer%22%3A0%2C%22tagSong%22%3A1%2C%22mvSong%22%3A0%2C%22bestShow%22%3A1%2C%22songlist%22%3A0%2C%22lyricSong%22%3A0%7D&pageSize=${limit}&text=${encodeURIComponent(str)}&pageNo=${page}&sort=0`, {
headers: {
sign: 'c3b7ae985e2206e97f1b2de8f88691e2',
timestamp: 1578225871982,
@@ -25,7 +25,7 @@ export default {
'User-Agent': 'okhttp/3.9.1',
},
})
// searchRequest = httpFetch(`https://app.c.nf.migu.cn/MIGUM2.0/v1.0/content/search_all.do?isCopyright=1&isCorrect=1&pageNo=${page}&pageSize=${this.limit}&searchSwitch={%22song%22:1,%22album%22:0,%22singer%22:0,%22tagSong%22:0,%22mvSong%22:0,%22songlist%22:0,%22bestShow%22:0}&sort=0&text=${encodeURIComponent(str)}`)
// searchRequest = httpFetch(`https://app.c.nf.migu.cn/MIGUM2.0/v1.0/content/search_all.do?isCopyright=1&isCorrect=1&pageNo=${page}&pageSize=${limit}&searchSwitch={%22song%22:1,%22album%22:0,%22singer%22:0,%22tagSong%22:0,%22mvSong%22:0,%22songlist%22:0,%22bestShow%22:0}&sort=0&text=${encodeURIComponent(str)}`)
return searchRequest.promise.then(({ body }) => body)
},
getSinger(singers) {
@@ -99,9 +99,9 @@ export default {
},
search(str, page = 1, { limit } = {}, retryNum = 0) {
if (++retryNum > 3) return Promise.reject(new Error('try max num'))
if (limit != null) this.limit = limit
if (limit == null) limit = this.limit
// http://newlyric.kuwo.cn/newlyric.lrc?62355680
return this.musicSearch(str, page).then(result => {
return this.musicSearch(str, page, limit).then(result => {
// console.log(result)
if (!result || result.code !== '000000') return Promise.reject(new Error(result ? result.info : '搜索失败'))
const songResultData = result.songResultData || { resultList: [], totalCount: 0 }
@@ -111,12 +111,12 @@ export default {
this.total = parseInt(songResultData.totalCount)
this.page = page
this.allPage = Math.ceil(this.total / this.limit)
this.allPage = Math.ceil(this.total / limit)
return Promise.resolve({
list,
allPage: this.allPage,
limit: this.limit,
limit,
total: this.total,
source: 'mg',
})

View File

@@ -48,10 +48,12 @@ export default {
return `https://app.c.nf.migu.cn/MIGUM2.0/v1.0/user/queryMusicListSongs.do?musicListId=${id}&pageNo=${page}&pageSize=${this.limit_song}`
},
defaultHeaders: {
language: 'Chinese',
ua: 'Android_migu',
mode: 'android',
version: '6.8.5',
'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1',
Referer: 'https://m.music.migu.cn/',
// language: 'Chinese',
// ua: 'Android_migu',
// mode: 'android',
// version: '6.8.5',
},
/**
@@ -155,6 +157,7 @@ export default {
if (this._requestObj_list) this._requestObj_list.cancelHttp()
if (tryNum > 2) return Promise.reject(new Error('try max num'))
this._requestObj_list = httpFetch(this.getSongListUrl(sortId, tagId, page), {
headers: this.defaultHeaders,
// headers: {
// sign: 'c3b7ae985e2206e97f1b2de8f88691e2',
// timestamp: 1578225871982,
@@ -186,6 +189,7 @@ export default {
// })
// })
return this._requestObj_list.promise.then(({ body }) => {
// console.log(body)
if (body.retCode !== '100000' || body.retMsg.code !== this.successCode) return this.getList(sortId, tagId, page, ++tryNum)
return {
list: this.filterList(body.retMsg.playlist),

Some files were not shown because too many files have changed in this diff Show More