Merge branch 'dev' of github.com:lyswhut/lx-music-desktop into dev

pull/733/head
lyswhut 2021-08-19 19:27:42 +08:00
commit 0c7ead5b21
85 changed files with 5774 additions and 4783 deletions

View File

@ -16,7 +16,9 @@
"no-multiple-empty-lines": [1, {"max": 2}],
"comma-dangle": [2, "always-multiline"],
"standard/no-callback-literal": "off",
"prefer-const": "off"
"prefer-const": "off",
"no-labels": "off",
"node/no-callback-literal": "off"
},
"settings": {
"html/html-extensions": [".html", ".vue"]

View File

@ -52,6 +52,14 @@ jobs:
name: lx-music-desktop-x86-Setup
path: build/* x86 Setup.exe
- name: Build Package Setup arm64
run: npm run pack:win:setup:arm64
- name: Upload Artifact Setup arm64
uses: actions/upload-artifact@v2
with:
name: lx-music-desktop-arm64-Setup
path: build/* arm64 Setup.exe
- name: Build Package Setup x86_64
run: npm run pack:win:setup:x86_64
- name: Upload Artifact Setup x86_64
@ -116,8 +124,8 @@ jobs:
- name: Build Package dmg
run: |
npm run publish:mac:dmg
npm run publish:mac:dmg:arm64
npm run pack:mac:dmg
npm run pack:mac:dmg:arm64
env:
ELECTRON_CACHE: $HOME/.cache/electron
ELECTRON_BUILDERCACHE: $HOME/.cache/electron-builder

View File

@ -40,6 +40,7 @@ jobs:
run: |
npm run publish:win:setup:x64:always
npm run publish:win:setup:x86
npm run publish:win:setup:arm64
npm run publish:win:setup:x86_64
npm run publish:win:7z:x64
npm run publish:win:7z:x86

7
.ncurc.js Normal file
View File

@ -0,0 +1,7 @@
module.exports = {
upgrade: true,
reject: [
'vue-loader'
// 'eslint-config-standard'
]
}

12
.vscode/settings.json vendored
View File

@ -5,5 +5,15 @@
"@lyric/*": "${workspaceFolder}/src/renderer-lyric/*",
"@static/*": "${workspaceFolder}/src/static/*",
"@common/*": "${workspaceFolder}/src/common/*",
}
},
"i18n-ally.localesPaths": [
"src/renderer/lang"
],
"i18n-ally.displayLanguage": "zh-cn",
"i18n-ally.sourceLanguage": "zh-cn",
"i18n-ally.namespace": true,
"i18n-ally.translate.engines": ["google-cn", "google"],
"i18n-ally.pathMatcher": "{locale}/{namespaces}.json",
"i18n-ally.keystyle": "flat",
"i18n-ally.sortKeys": true
}

View File

@ -6,6 +6,78 @@ 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.12.2](https://github.com/lyswhut/lx-music-desktop/compare/v1.12.1...v1.12.2) - 2021-08-11
### 修复
- 修复播放下载列表的歌曲时切歌的问题
- 修复播放下载列表的歌曲时歌词无法显示的问题
- 修复下载列表稍后播放功能无效的问题
- 修复同步服务器启动失败时,关闭同步服务不会清空失败信息的问题
## [1.12.1](https://github.com/lyswhut/lx-music-desktop/compare/v1.12.0...v1.12.1) - 2021-08-08
### 修复
- 修复随机播放下无法切歌的问题
## [1.12.0](https://github.com/lyswhut/lx-music-desktop/compare/v1.11.0...v1.12.0) - 2021-08-08
### 新增
- 新增局域网同步功能实验性首次使用前建议先备份一次列表此功能需要配合PC端使用移动端与PC端处在同一个局域网路由器的网络下时可以多端实时同步歌曲列表使用问题请看"常见问题"。
### 优化
- 添加播放器对系统媒体控制与显示的兼容处理现在在windows下的锁屏界面可以正确显示当前播放的音乐信息及切换歌曲了
### 修复
- 修复导入kg歌单最多只能加载100、500首歌曲的问题。注现在可以加载1000+首歌曲的歌单但出于未知原因会导致部分歌曲无法加载可能是无版权导致的目前酷狗码仍然最多只能加载500首歌
- 修复某些情况下所显示的歌词、封面图片与当前正在播放的歌曲不一致的问题
## [1.11.0](https://github.com/lyswhut/lx-music-desktop/compare/v1.10.2...v1.11.0) - 2021-07-18
### 新增
- 添加 win arm64 架构的安装包构建
- 新增“添加歌曲到列表时的位置”设置,可选项为列表的“顶部”与“底部”
### 优化
- 优化网络请求,尝试去解决无法连接服务器的问题
- 优化mg源打开歌单的链接兼容
### 修复
- 修复mg源搜索失效的问题
### 移除
- 因wy源的歌单列表已没有“最新”排序的选项所以现跟随移除wy源歌单列表按“最新”排序的按钮
### 变更
- 添加歌曲到列表时从原来的底部改为顶部,若你想要将你的列表歌曲顺序反转以适应这一变更,可先按住`shift`键的情况下点击列表的最后一首歌然后再点击列表的第一首歌完成倒序选中最后随便右击列表的任意一首歌在弹出的菜单中选择调整顺序在弹出框输入1后确定即可反转列表。
若你想要恢复原来的行为则可以去更改“添加歌曲到列表时的位置”设置项。
### 其他
- 更新electron到v13.1.7
## [1.10.2](https://github.com/lyswhut/lx-music-desktop/compare/v1.10.1...v1.10.2) - 2021-05-25
### 修复
- 修复企鹅音乐搜索歌曲没有结果的问题
## [1.10.1](https://github.com/lyswhut/lx-music-desktop/compare/v1.10.0...v1.10.1) - 2021-05-25
### 修复
- 修复企鹅音乐搜索歌曲没有结果的问题
- 修复播放在空的歌单列表点击播放全部时报错的问题
## [1.10.0](https://github.com/lyswhut/lx-music-desktop/compare/v1.9.0...v1.10.0) - 2021-05-19
lx music移动端已经发布了使用习惯仍跟桌面版一样不过功能、界面仍比较简单有兴趣的可以去体检一下项目地址

28
FAQ.md
View File

@ -13,7 +13,7 @@
尝试更换网络,如切换到移动网络,若移动网络还是不行则尝试开关下手机的飞行模式后再试,<br>
若使用家庭网络的话可尝试将光猫断电5分钟左右再通电联网后播放。
### 提示`getaddrinfo EAI_AGAIN ...`
### 提示 `getaddrinfo EAI_AGAIN ...``无法连接到服务器`
尝试在在浏览器打开这个地址`http://ts.tempmusic.tk`浏览器显示404是正常的如果不是404那就证明所在网络无法访问接口服务器。
若网页无法打开或打开来不是404则可能是DNS的问题可以尝试以下办法
@ -62,6 +62,32 @@
对于分享出来的歌单若打开失败可尝试先在浏览器中打开后再从浏览器地址栏复制URL地址到软件打开<br>
或者如果你知道歌单 id 也可以直接输入歌单 id 打开。<br>
注:网易源的“我喜欢”歌单无法在未登录的情况下打开,所以你需要手动创建一个歌单后将“我喜欢”里的歌曲移动到该歌单打开
## 更新已收藏的在线歌单
该功能仅对直接从歌单详情页点“收藏”按钮收藏的歌单有效,可右击已收藏的列表名从弹出的菜单中选择“更新”使用该功能,
需要注意的是:这将会覆盖本地的目标列表,歌曲将被替换成最新的在线列表。
## 同步功能的使用(实验性,首次使用前建议先备份一次列表)
**注意:由于同步传输时的数据是明文传输,请在受信任的网络下使用此功能!**<br>
此功能需要配合移动端使用PC端与移动端处在同一个局域网路由器的网络下时可以多端实时同步歌曲列表使用方法
1. 在PC端的设置-数据同步开启同步功能(这时如果出现安全软件、防火墙等提示网络连接弹窗时需要点击允许)
2. 在移动端的设置-同步-同步服务器地址输入PC端显示的同步服务器地址如果显示可以多个则输入与**移动端上显示的本机地址**最相似的那个端口号与PC端的同步端口一致**输入完毕后需要按一下键盘上的回车键使输入的内容生效**
3. 输入完这两项后点击“启动同步”
4. 若连接成功对于首次同步时若两边的设备的列表不为空则PC端会弹出选择列表同步方式的弹窗同步方式的说明弹窗下面有介绍
对于连接同步服务失败的可能原因:
- 此功能需要PC端与移动端都连接在同一个路由器下的网络才能使用
- 路由器若开启了AP隔离则此功能无法使用
- 检查防火墙是否拦截了PC端的服务端口
## 界面异常(界面显示不完整)
### Windows 7 下界面异常

View File

@ -36,7 +36,7 @@
所用技术栈:
- Electron 12
- Electron 13
- Vue 2
已支持的平台:

View File

@ -2,7 +2,8 @@ const isDev = process.env.NODE_ENV === 'development'
module.exports = {
modules: {
localIdentName: isDev ? '[folder]-[name]--[local]--[hash:base64:5]' : '[hash:base64:5]',
localIdentName: isDev ? '[path][name]__[local]--[hash:base64:5]' : '[hash:base64:5]',
exportLocalsConvention: 'camelCase',
},
sourceMap: isDev,
}

View File

@ -18,7 +18,7 @@ module.exports = {
filename: '[name].js',
libraryTarget: 'commonjs2',
path: path.join(__dirname, '../../dist/electron'),
publicPath: './',
publicPath: 'auto',
},
resolve: {
alias: {
@ -97,26 +97,38 @@ module.exports = {
},
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: 'imgs/[name]--[folder].[ext]',
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 10000,
},
},
generator: {
filename: 'imgs/[name]-[contenthash:8][ext]',
},
},
{
test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: 'media/[name]--[folder].[ext]',
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 10000,
},
},
generator: {
filename: 'media/[name]-[contenthash:8][ext]',
},
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: 'fonts/[name]--[folder].[ext]',
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 10000,
},
},
generator: {
filename: 'fonts/[name]-[contenthash:8][ext]',
},
},
],

View File

@ -18,7 +18,7 @@ module.exports = {
filename: '[name].js',
libraryTarget: 'commonjs2',
path: path.join(__dirname, '../../dist/electron'),
publicPath: './',
publicPath: 'auto',
},
resolve: {
alias: {
@ -97,26 +97,38 @@ module.exports = {
},
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: 'imgs/[name]--[folder].[ext]',
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 10000,
},
},
generator: {
filename: 'imgs/[name]-[contenthash:8][ext]',
},
},
{
test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: 'media/[name]--[folder].[ext]',
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 10000,
},
},
generator: {
filename: 'media/[name]-[contenthash:8][ext]',
},
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: 'fonts/[name]--[folder].[ext]',
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 10000,
},
},
generator: {
filename: 'fonts/[name]-[contenthash:8][ext]',
},
},
],

7118
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,15 +1,16 @@
{
"name": "lx-music-desktop",
"version": "1.10.0",
"version": "1.12.2",
"description": "一个免费的音乐查找助手",
"main": "./dist/electron/main.js",
"productName": "lx-music-desktop",
"scripts": {
"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:setup:x64 && npm run pack:win:7z",
"pack:win": "node build-config/pack.js && npm run pack:win:setup:x64 && npm run pack:win:setup:x86 && npm run pack:win:setup:arm64 && npm run pack:win:setup:x86_64 && npm run pack:win:7z",
"pack:win:setup:x86_64": "cross-env TARGET=Setup ARCH=x86_64 electron-builder -w=nsis --x64 --ia32 -p never",
"pack:win:setup:x64": "cross-env TARGET=Setup ARCH=x64 electron-builder -w=nsis --x64 -p never",
"pack:win:setup:x86": "cross-env TARGET=Setup ARCH=x86 electron-builder -w=nsis --ia32 -p never",
"pack:win:setup:arm64": "cross-env TARGET=Setup ARCH=arm64 electron-builder -w=nsis --arm64 -p never",
"pack:win:portable": "npm run pack:win:portable:x86_64 && npm run pack:win:portable:x64 && npm run pack:win:portable:x86",
"pack:win:portable:x86_64": "cross-env TARGET=portable ARCH=x86_64 electron-builder -w=portable --x64 --ia32 -p never",
"pack:win:portable:x64": "cross-env TARGET=portable ARCH=x64 electron-builder -w=portable --x64 -p never",
@ -35,6 +36,7 @@
"publish:win:setup:x64:always": "cross-env TARGET=Setup ARCH=x64 electron-builder -w=nsis --x64 -p always",
"publish:win:setup:x64": "cross-env TARGET=Setup ARCH=x64 electron-builder -w=nsis --x64 -p always",
"publish:win:setup:x86": "cross-env TARGET=Setup ARCH=x86 electron-builder -w=nsis --ia32 -p onTagOrDraft",
"publish:win:setup:arm64": "cross-env TARGET=Setup ARCH=arm64 electron-builder -w=nsis --arm64 -p onTagOrDraft",
"publish:win:setup:x86_64": "cross-env TARGET=Setup ARCH=x86_64 electron-builder -w=nsis --x64 --ia32 -p onTagOrDraft",
"publish:win:portable": "npm run publish:win:portable:x86_64 && npm run publish:win:portable:x64 && npm run publish:win:portable:x86",
"publish:win:portable:x86_64": "cross-env TARGET=portable ARCH=x86_64 electron-builder -w=portable --x64 --ia32 -p onTagOrDraft",
@ -65,13 +67,13 @@
"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",
"dp": "cross-env ELECTRON_GET_USE_PROXY=true GLOBAL_AGENT_HTTPS_PROXY=http://localhost:1081 npm run pack",
"up": "cross-env ELECTRON_GET_USE_PROXY=true GLOBAL_AGENT_HTTPS_PROXY=http://localhost:1081 npm update"
"up": "cross-env ELECTRON_GET_USE_PROXY=true GLOBAL_AGENT_HTTPS_PROXY=http://localhost:1081 npm i"
},
"browserslist": [
"Electron 12.0.8"
"Electron 13.1.7"
],
"engines": {
"node": ">= 12"
"node": ">= 14"
},
"build": {
"appId": "cn.toside.music.desktop",
@ -161,50 +163,50 @@
},
"homepage": "https://github.com/lyswhut/lx-music-desktop#readme",
"devDependencies": {
"@babel/core": "^7.14.3",
"@babel/plugin-proposal-class-properties": "^7.13.0",
"@babel/core": "^7.15.0",
"@babel/plugin-proposal-class-properties": "^7.14.5",
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/plugin-transform-modules-umd": "^7.14.0",
"@babel/plugin-transform-runtime": "^7.14.3",
"@babel/plugin-transform-modules-umd": "^7.14.5",
"@babel/plugin-transform-runtime": "^7.15.0",
"@babel/polyfill": "^7.12.1",
"@babel/preset-env": "^7.14.2",
"@babel/preset-env": "^7.15.0",
"babel-eslint": "^10.1.0",
"babel-loader": "^8.2.2",
"babel-minify-webpack-plugin": "^0.3.1",
"babel-preset-minify": "^0.5.1",
"browserslist": "^4.16.6",
"cfonts": "^2.9.2",
"chalk": "^4.1.1",
"browserslist": "^4.16.7",
"cfonts": "^2.9.3",
"chalk": "^4.1.2",
"changelog-parser": "^2.8.0",
"copy-webpack-plugin": "^8.1.1",
"core-js": "^3.12.1",
"copy-webpack-plugin": "^9.0.1",
"core-js": "^3.16.1",
"cross-env": "^7.0.3",
"css-loader": "^5.2.4",
"css-minimizer-webpack-plugin": "^3.0.0",
"css-loader": "^6.2.0",
"css-minimizer-webpack-plugin": "^3.0.2",
"del": "^6.0.0",
"electron": "^12.0.8",
"electron-builder": "^22.11.4",
"electron": "^13.1.9",
"electron-builder": "^22.11.7",
"electron-debug": "^3.2.0",
"electron-devtools-installer": "^3.2.0",
"eslint": "^7.26.0",
"eslint-config-standard": "^14.1.1",
"electron-to-chromium": "^1.3.801",
"eslint": "^7.32.0",
"eslint-config-standard": "^16.0.3",
"eslint-formatter-friendly": "^7.0.0",
"eslint-loader": "^4.0.2",
"eslint-plugin-html": "^6.1.2",
"eslint-plugin-import": "^2.23.2",
"eslint-plugin-import": "^2.24.0",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^5.1.0",
"eslint-plugin-standard": "^4.1.0",
"file-loader": "^6.2.0",
"friendly-errors-webpack-plugin": "^1.7.0",
"html-webpack-plugin": "^5.3.1",
"html-webpack-plugin": "^5.3.2",
"less": "^4.1.1",
"less-loader": "^9.0.0",
"less-loader": "^10.0.1",
"less-plugin-clean-css": "^1.5.1",
"markdown-it": "^12.0.6",
"mini-css-extract-plugin": "^1.6.0",
"postcss": "^8.2.15",
"postcss-loader": "^5.3.0",
"markdown-it": "^12.2.0",
"mini-css-extract-plugin": "^2.2.0",
"postcss": "^8.3.6",
"postcss-loader": "^6.1.1",
"postcss-pxtorem": "^6.0.0",
"pug": "^3.0.2",
"pug-loader": "^2.4.0",
@ -213,33 +215,38 @@
"rimraf": "^3.0.2",
"spinnies": "^0.5.1",
"stylus": "^0.54.8",
"stylus-loader": "^6.0.0",
"terser-webpack-plugin": "^5.1.2",
"stylus-loader": "^6.1.0",
"terser-webpack-plugin": "^5.1.4",
"url-loader": "^4.1.1",
"vue-loader": "^15.9.7",
"vue-template-compiler": "^2.6.12",
"webpack": "^5.37.0",
"webpack-cli": "^4.7.0",
"vue-loader": "^15.9.8",
"vue-template-compiler": "^2.6.14",
"webpack": "^5.50.0",
"webpack-cli": "^4.7.2",
"webpack-dev-server": "^3.11.2",
"webpack-hot-middleware": "^2.25.0",
"webpack-merge": "^5.7.3"
"webpack-merge": "^5.8.0"
},
"dependencies": {
"crypto-js": "^4.0.0",
"dnscache": "^1.0.2",
"electron-log": "^4.3.5",
"crypto-js": "^4.1.1",
"electron-log": "^4.4.1",
"bufferutil": "^4.0.3",
"eiows": "^3.6.1",
"electron-store": "^8.0.0",
"electron-updater": "^4.3.9",
"iconv-lite": "^0.6.2",
"http-terminator": "^3.0.0",
"iconv-lite": "^0.6.3",
"image-size": "^1.0.0",
"js-htmlencode": "^0.3.0",
"lrc-file-parser": "^1.0.7",
"needle": "^2.6.0",
"koa": "^2.13.1",
"long": "^4.0.0",
"lrc-file-parser": "^1.1.2",
"needle": "^2.8.0",
"node-id3": "^0.2.3",
"request": "^2.88.2",
"vue": "^2.6.12",
"vue-i18n": "^8.24.4",
"vue-router": "^3.5.1",
"socket.io": "^4.1.3",
"utf-8-validate": "^5.0.5",
"vue": "^2.6.14",
"vue-i18n": "^8.25.0",
"vue-router": "^3.5.2",
"vuex": "^3.6.2",
"vuex-router-sync": "^5.0.0"
}

View File

@ -1,21 +1,6 @@
lx music移动端已经发布了使用习惯仍跟桌面版一样不过功能、界面仍比较简单有兴趣的可以去体检一下项目地址
https://github.com/lyswhut/lx-music-mobile#readme
### 新增
- 排行榜界面添加播放、收藏整个排行榜功能,可以右击排行榜名字后,在弹出的右键菜单中使用。注:收藏、播放存在分页的排行榜时需等待操作完成后才能切换排行榜,不然会导致操作中断。
- 新增Mac arm64位dmg包的构建
### 修复
- 修复全局快捷键对桌面歌词无效的问题
- 修复快捷键设置框内的提示问题
- 修复在当前正常播放的列表中使用稍后播放功能时,播放完后稍后播放的歌曲后不会恢复原来播放位置播放的问题
- 修复kw部分歌单无法打开的问题
- 修复wy源的歌曲音质匹配问题
- 修复mg源歌单标签、排行榜歌曲列表无法加载的问题
- 修复了一个歌曲下载失败时不会跳过任务的问题
### 其他
- 更新 Electron 到 12.0.8
- 修复播放下载列表的歌曲时切歌的问题
- 修复播放下载列表的歌曲时歌词无法显示的问题
- 修复下载列表稍后播放功能无效的问题
- 修复同步服务器启动失败时,关闭同步服务不会清空失败信息的问题

File diff suppressed because one or more lines are too long

View File

@ -2,7 +2,7 @@ const path = require('path')
const os = require('os')
const defaultSetting = {
version: '1.0.41',
version: '1.0.43',
player: {
togglePlayMethod: 'listLoop',
highQuality: false,
@ -36,6 +36,7 @@ const defaultSetting = {
isShowSource: true,
prevSelectListId: 'default',
isSaveScrollLocation: true,
addMusicLocationType: 'top',
},
download: {
enable: false,
@ -84,6 +85,10 @@ const defaultSetting = {
isToTray: false,
themeId: 0,
},
sync: {
enable: false,
port: '23332',
},
windowSizeId: 2,
themeId: 0,
langId: null,

View File

@ -66,6 +66,13 @@ const names = {
get_music_url: 'get_music_url',
save_music_url: 'save_music_url',
clear_music_url: 'clear_music_url',
sync_enable: 'sync_enable',
sync_status: 'sync_status',
sync_get_status: 'sync_get_status',
sync_generate_code: 'sync_generate_code',
sync_action_list: 'sync_action_list',
sync_list: 'sync_list',
},
winLyric: {
close: 'close',

View File

@ -10,6 +10,10 @@ class Common extends EventEmitter {
configStatus(name) {
this.emit(COMMON_EVENT_NAME.configStatus, name)
}
saveMyList(data) {
this.emit(COMMON_EVENT_NAME.saveMyList, data)
}
}
module.exports = Common

View File

@ -1,6 +1,7 @@
exports.common = {
initConfig: 'initConfig',
configStatus: 'config',
saveMyList: 'saveMyList',
}
exports.mainWindow = {

View File

@ -7,6 +7,7 @@ const WinLyric = require('./WinLyric')
const HotKey = require('./HotKey')
const { Event: UserApi } = require('../modules/userApi')
const { Event: Sync } = require('../modules/sync')
if (!global.lx_event.common) global.lx_event.common = new Common()
if (!global.lx_event.mainWindow) global.lx_event.mainWindow = new MainWindow()
@ -15,3 +16,4 @@ 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()
if (!global.lx_event.sync) global.lx_event.sync = new Sync()

View File

@ -55,12 +55,12 @@ app.on('web-contents-created', (event, contents) => {
if (!navigationUrlWhiteList.some(url => url.test(navigationUrl))) return event.preventDefault()
console.log('navigation to url:', navigationUrl)
})
contents.on('new-window', async(event, navigationUrl) => {
event.preventDefault()
if (/^devtools/.test(navigationUrl)) return
console.log(navigationUrl)
if (!/^https?:\/\//.test(navigationUrl)) return
await shell.openExternal(navigationUrl)
contents.setWindowOpenHandler(({ url }) => {
if (!/^devtools/.test(url) && /^https?:\/\//.test(url)) {
shell.openExternal(url)
}
console.log(url)
return { action: 'deny' }
})
contents.on('will-attach-webview', (event, webPreferences, params) => {
// Strip away preload scripts if unused or verify their location is legitimate

View File

@ -0,0 +1,23 @@
const { EventEmitter } = require('events')
const SYNC_EVENT_NAME = require('./name')
class Sync extends EventEmitter {
status(status) {
this.emit(SYNC_EVENT_NAME.status, status)
}
sync_list(data) {
this.emit(SYNC_EVENT_NAME.sync_list, data)
}
sync_handle_list(data) {
this.emit(SYNC_EVENT_NAME.sync_handle_list, data)
}
action_list(data) {
this.emit(SYNC_EVENT_NAME.sync_action_list, data)
}
}
module.exports = Sync

View File

@ -0,0 +1,6 @@
module.exports = {
sync_action_list: 'sync_action_list',
sync_list: 'sync_list',
sync_handle_list: 'sync_handle_list',
status: 'status',
}

View File

@ -0,0 +1,15 @@
const Event = require('./event/event')
const eventNames = require('./event/name')
const modules = require('./modules')
const { startServer, stopServer, getStatus, generateCode } = require('./server/server')
module.exports = {
startServer,
stopServer,
getStatus,
generateCode,
Event,
eventNames,
modules,
}

View File

@ -0,0 +1 @@
exports.list = require('./list')

View File

@ -0,0 +1,41 @@
const { encryptMsg, decryptMsg } = require('../server/utils')
let io
const handleListAction = ({ action, data }) => {
// console.log(action, data)
global.lx_event.sync.action_list({ action, data })
}
// const addMusic = (orderId, callback) => {
// // ...
// }
const broadcast = async(action, data, excludeIds = []) => {
if (!io) return
const sockets = await io.fetchSockets()
for (const socket of sockets) {
if (excludeIds.includes(socket.data.keyInfo.clientId)) continue
socket.emit(action, encryptMsg(socket.data.keyInfo, data))
}
}
exports.sendListAction = (action, data) => {
// io.sockets
return broadcast('list:action', JSON.stringify({ action, data }))
}
exports.registerListHandler = (_io, socket) => {
io = _io
socket.on('list:action', msg => {
// console.log(msg)
msg = decryptMsg(socket.data.keyInfo, msg)
if (!msg) return
handleListAction(JSON.parse(msg))
broadcast('list:action', msg, [socket.data.keyInfo.clientId])
// socket.broadcast.emit('list:action', { action: 'list_remove', data: { id: 'default', index: 0 } })
})
// socket.on('list:add', addMusic)
}
exports.unregisterListHandler = () => {
io = null
}

View File

@ -0,0 +1,69 @@
const { aesEncrypt, aesDecrypt, createClientKeyInfo, getClientKeyInfo, setClientKeyInfo } = require('./utils')
const authMsg = 'lx-music auth::'
const helloMsg = 'Hello~::^-^::'
exports.authCode = async(req, res, authCode) => {
let code = 401
let msg = 'Forbidden'
// console.log(req.headers)
if (req.headers.m) {
label:
if (req.headers.i) {
const keyInfo = getClientKeyInfo(req.headers.i)
if (!keyInfo) break label
let text
try {
text = aesDecrypt(req.headers.m, keyInfo.key, keyInfo.iv)
} catch (err) {
break label
}
console.log(text)
if (text.startsWith(authMsg)) {
code = 200
const deviceName = text.replace(authMsg, '') || 'Unknown'
if (deviceName != keyInfo.deviceName) {
keyInfo.deviceName = deviceName
setClientKeyInfo(keyInfo)
}
msg = aesEncrypt(helloMsg, keyInfo.key, keyInfo.iv)
}
} else {
let key = ''.padStart(16, Buffer.from(authCode).toString('hex'))
const iv = Buffer.from(key.split('').reverse().join('')).toString('base64')
key = Buffer.from(key).toString('base64')
// console.log(authCode, key, iv)
let text
try {
text = aesDecrypt(req.headers.m, key, iv)
} catch (err) {
break label
}
console.log(text)
if (text.startsWith(authMsg)) {
code = 200
const deviceName = text.replace(authMsg, '') || 'Unknown'
msg = aesEncrypt(JSON.stringify(createClientKeyInfo(deviceName)), key, iv)
}
}
}
res.writeHead(code)
res.end(msg)
}
exports.authConnect = async req => {
const { i, t } = req._query
label:
if (i && t) {
const keyInfo = getClientKeyInfo(i)
if (!keyInfo) break label
let text
try {
text = aesDecrypt(t, keyInfo.key, keyInfo.iv)
} catch (err) {
break label
}
if (text == 'lx-music connect') return
}
throw new Error('failed')
}

View File

@ -0,0 +1,7 @@
const { startServer, stopServer, getStatus } = require('./server')
module.exports = {
startServer,
stopServer,
getStatus,
}

View File

@ -0,0 +1,172 @@
const http = require('http')
const sio = require('socket.io')
const { createHttpTerminator } = require('http-terminator')
const modules = require('../modules')
const { authCode, authConnect } = require('./auth')
const { getAddress, getServerId, generateCode, getClientKeyInfo } = require('./utils')
const syncList = require('./syncList')
let status = {
status: false,
message: '',
address: [],
code: '',
devices: [],
}
const handleConnection = (io, socket) => {
console.log('connection')
// console.log(socket.handshake.query)
for (const module of Object.values(modules)) {
module.registerListHandler(io, socket)
}
}
const authConnection = (req, callback) => {
// console.log(req.headers)
// // console.log(req.auth)
// console.log(req._query.authCode)
authConnect(req).then(() => {
callback(null, true)
}).catch(err => {
callback(err, false)
})
}
let httpTerminator = null
let io = null
const handleStartServer = (port = 9527) => new Promise((resolve, reject) => {
const httpServer = http.createServer((req, res) => {
// console.log(req.url)
let code
let msg
switch (req.url) {
case '/hello':
code = 200
msg = 'Hello~::^-^::'
break
case '/id':
code = 200
msg = 'OjppZDo6' + getServerId()
break
case '/ah':
authCode(req, res, status.code)
break
default:
code = 401
msg = 'Forbidden'
break
}
if (!code) return
res.writeHead(code)
res.end(msg)
})
httpTerminator = createHttpTerminator({
server: httpServer,
})
io = sio(httpServer, {
path: '/sync',
serveClient: false,
connectTimeout: 10000,
pingTimeout: 30000,
maxHttpBufferSize: 3e6,
allowRequest: authConnection,
transports: ['websocket'],
})
io.on('connection', async socket => {
socket.on('disconnect', reason => {
console.log('disconnect', reason)
status.devices.splice(status.devices.findIndex(k => k.clientId == keyInfo.clientId), 1)
global.lx_event.sync.status(status)
})
const keyInfo = getClientKeyInfo(socket.handshake.query.i)
// socket.lx_keyInfo = keyInfo
socket.data.keyInfo = keyInfo
try {
await syncList(io, socket)
} catch (err) {
console.log(err)
return
}
status.devices.push(keyInfo)
handleConnection(io, socket, keyInfo)
global.lx_event.sync.status(status)
})
httpServer.on('error', error => {
console.log(error)
reject(error)
})
httpServer.on('listening', () => {
const addr = httpServer.address()
const bind = typeof addr === 'string' ? 'pipe ' + addr : 'port ' + addr.port
console.info(`Listening on ${bind}`)
resolve()
})
httpServer.listen(port)
})
const handleStopServer = async() => {
if (!httpTerminator) return
await io.close()
await httpTerminator.terminate().catch(() => {})
io = null
httpTerminator = null
}
exports.stopServer = async() => {
if (!status.status) {
status.status = false
status.message = ''
status.address = []
status.code = ''
global.lx_event.sync.status(status)
return
}
console.log('stoping sync server...')
return handleStopServer().then(() => {
console.log('sync server stoped')
status.status = false
status.message = ''
status.address = []
status.code = ''
}).catch(err => {
console.log(err)
status.message = err.message
}).finally(() => {
global.lx_event.sync.status(status)
})
}
exports.startServer = async port => {
if (status.status) await handleStopServer()
console.log('starting sync server...')
return handleStartServer(port).then(() => {
console.log('sync server started')
status.status = true
status.message = ''
status.address = getAddress()
status.code = generateCode()
}).catch(err => {
console.log(err)
status.status = false
status.message = err.message
status.address = []
status.code = ''
}).finally(() => {
global.lx_event.sync.status(status)
})
}
exports.getStatus = () => status
exports.generateCode = async() => {
status.code = generateCode()
global.lx_event.sync.status(status)
return status.code
}

View File

@ -0,0 +1,394 @@
const path = require('path')
const fs = require('fs')
const fsPromises = fs.promises
const { app } = require('electron')
const { encryptMsg, decryptMsg } = require('./utils')
const SYNC_EVENT_NAMES = require('../event/name')
const { common: COMMON_EVENT_NAME } = require('@main/events/_name')
const { throttle } = require('@common/utils')
let io
let syncingId = null
const wait = (time = 1000) => new Promise((resolve, reject) => setTimeout(resolve, time))
const getRemoteListData = socket => new Promise((resolve, reject) => {
console.log('getRemoteListData')
const handleError = reason => {
reject(new Error(reason))
}
const handleSuccess = enData => {
socket.removeListener('disconnect', handleError)
socket.removeListener('list:sync', handleSuccess)
console.log('getRemoteListData', 'handleSuccess')
const data = JSON.parse(decryptMsg(socket.data.keyInfo, enData))
if (!data) return reject(new Error('Get remote list data failed'))
if (data.action != 'getData') return
resolve(data.data)
}
socket.on('disconnect', handleError)
socket.on('list:sync', handleSuccess)
socket.emit('list:sync', encryptMsg(socket.data.keyInfo, JSON.stringify({ action: 'getData', data: 'all' })))
})
const getLocalListData = () => new Promise((resolve, reject) => {
const handleSuccess = ({ action, data }) => {
if (action !== 'getData') return
global.lx_event.sync.off(SYNC_EVENT_NAMES.sync_handle_list, handleSuccess)
resolve(data)
}
global.lx_event.sync.on(SYNC_EVENT_NAMES.sync_handle_list, handleSuccess)
global.lx_event.sync.sync_list({
action: 'getData',
})
})
const getSyncMode = keyInfo => new Promise((resolve, reject) => {
const handleSuccess = ({ action, data }) => {
if (action !== 'selectMode') return
global.lx_event.sync.off(SYNC_EVENT_NAMES.sync_handle_list, handleSuccess)
resolve(data)
}
global.lx_event.sync.on(SYNC_EVENT_NAMES.sync_handle_list, handleSuccess)
global.lx_event.sync.sync_list({
action: 'selectMode',
data: keyInfo,
})
})
const finishedSync = socket => {
return socket.emit('list:sync', encryptMsg(socket.data.keyInfo, JSON.stringify({
action: 'finished',
})))
}
const setLocalList = listData => {
global.lx_event.sync.sync_list({
action: 'setData',
data: listData,
})
}
const setRemotelList = async(socket, listData) => {
if (!io) return
const sockets = await io.fetchSockets()
for (const socket of sockets) {
// if (excludeIds.includes(socket.data.keyInfo.clientId)) continue
socket.emit('list:sync', encryptMsg(socket.data.keyInfo, JSON.stringify({ action: 'setData', data: listData })))
}
}
let writeFilePromises = {}
const updateSnapshot = (path, data) => {
console.log('updateSnapshot', path)
let writeFilePromise = writeFilePromises[path] || Promise.resolve()
return writeFilePromise.then(() => {
writeFilePromise = writeFilePromises[path] = fsPromises.writeFile(path, data)
return writeFilePromise
})
}
const createListDataObj = listData => {
const listDataObj = {}
for (const list of listData.userList) listDataObj[list.id] = list
return listDataObj
}
const handleMergeList = (sourceList, targetList, addMusicLocationType) => {
let newList
switch (addMusicLocationType) {
case 'top':
newList = [...targetList.list, ...sourceList.list]
break
case 'bottom':
default:
newList = [...sourceList.list, ...targetList.list]
break
}
const map = {}
const ids = []
switch (addMusicLocationType) {
case 'top':
newList = [...targetList.list, ...sourceList.list]
for (let i = newList.length - 1; i > -1; i--) {
const item = newList[i]
if (map[item.songmid]) continue
ids.unshift(item.songmid)
map[item.songmid] = item
}
break
case 'bottom':
default:
newList = [...sourceList.list, ...targetList.list]
for (const item of newList) {
if (map[item.songmid]) continue
ids.push(item.songmid)
map[item.songmid] = item
}
break
}
return {
...sourceList,
list: ids.map(id => map[id]),
}
}
const mergeList = (sourceListData, targetListData) => {
const addMusicLocationType = global.appSetting.list.addMusicLocationType
const newListData = {}
newListData.defaultList = handleMergeList(sourceListData.defaultList, targetListData.defaultList, addMusicLocationType)
newListData.loveList = handleMergeList(sourceListData.loveList, targetListData.loveList, addMusicLocationType)
const listDataObj = createListDataObj(sourceListData)
newListData.userList = [...sourceListData.userList]
for (const list of targetListData.userList) {
const targetList = listDataObj[list.id]
if (targetList) {
targetList.list = handleMergeList(targetList, list, addMusicLocationType).list
} else {
newListData.userList.push(list)
}
}
return newListData
}
const overwriteList = (sourceListData, targetListData) => {
const newListData = {}
newListData.defaultList = sourceListData.defaultList
newListData.loveList = sourceListData.loveList
const listDataObj = createListDataObj(sourceListData)
newListData.userList = [...sourceListData.userList]
for (const list of targetListData.userList) {
const targetList = listDataObj[list.id]
if (targetList) continue
newListData.userList.push(list)
}
return newListData
}
const handleMergeListData = async socket => {
let isSelectingMode = false
const handleDisconnect = () => {
if (!isSelectingMode) return
global.lx_event.sync.sync_list({
action: 'closeSelectMode',
})
}
socket.on('disconnect', handleDisconnect)
isSelectingMode = true
const mode = await getSyncMode(socket.data.keyInfo)
isSelectingMode = false
const [remoteListData, localListData] = await Promise.all([getRemoteListData(socket), getLocalListData()])
console.log('handleMergeListData', 'remoteListData, localListData')
let listData
switch (mode) {
case 'merge_local_remote':
listData = mergeList(localListData, remoteListData)
break
case 'merge_remote_local':
listData = mergeList(remoteListData, localListData)
break
case 'overwrite_local_remote':
listData = overwriteList(localListData, remoteListData)
break
case 'overwrite_remote_local':
listData = overwriteList(remoteListData, localListData)
break
case 'overwrite_local_remote_full':
listData = localListData
break
case 'overwrite_remote_local_full':
listData = remoteListData
break
case 'none': return
case 'cancel':
socket.disconnect(true)
throw new Error('cancel')
}
return listData
}
const handleSyncList = async socket => {
const [remoteListData, localListData] = await Promise.all([getRemoteListData(socket), getLocalListData()])
console.log('handleSyncList', 'remoteListData, localListData')
const listData = {}
if (localListData.defaultList.list.length || localListData.loveList.list.length || localListData.userList.length) {
if (remoteListData.defaultList.list.length || remoteListData.loveList.list.length || remoteListData.userList.length) {
const mergedList = await handleMergeListData(socket)
console.log('handleMergeListData', 'mergedList')
console.log(mergedList)
if (!mergedList) return
listData.defaultList = mergedList.defaultList
listData.loveList = mergedList.loveList
listData.userList = mergedList.userList
setLocalList(mergedList)
setRemotelList(socket, mergedList)
} else {
setRemotelList(socket, localListData)
listData.defaultList = localListData.defaultList
listData.loveList = localListData.loveList
listData.userList = localListData.userList
}
} else {
if (remoteListData.defaultList.list.length || remoteListData.loveList.list.length || remoteListData.userList.length) {
setLocalList(remoteListData)
listData.defaultList = remoteListData.defaultList
listData.loveList = remoteListData.loveList
listData.userList = remoteListData.userList
} else {
listData.defaultList = localListData.defaultList
listData.loveList = localListData.loveList
listData.userList = localListData.userList
}
}
return updateSnapshot(socket.data.snapshotFilePath, JSON.stringify({
defaultList: listData.defaultList,
loveList: listData.loveList,
userList: listData.userList,
})).then(() => {
socket.data.isCreatedSnapshot = true
return listData
})
}
const mergeListDataFromSnapshot = (sourceList, targetList, snapshotList, addMusicLocationType) => {
const removedListIds = new Set()
const sourceListItemIds = new Set()
const targetListItemIds = new Set()
for (const m of sourceList.list) sourceListItemIds.add(m.songmid)
for (const m of targetList.list) targetListItemIds.add(m.songmid)
for (const m of snapshotList.list) {
if (!sourceListItemIds.has(m.songmid)) removedListIds.add(m.songmid)
}
for (const m of snapshotList.list) {
if (!targetListItemIds.has(m.songmid)) removedListIds.add(m.songmid)
}
let newList
const map = {}
const ids = []
switch (addMusicLocationType) {
case 'top':
newList = [...targetList.list, ...sourceList.list]
for (let i = newList.length - 1; i > -1; i--) {
const item = newList[i]
if (map[item.songmid] || removedListIds.has(item.songmid)) continue
ids.unshift(item.songmid)
map[item.songmid] = item
}
break
case 'bottom':
default:
newList = [...sourceList.list, ...targetList.list]
for (const item of newList) {
if (map[item.songmid] || removedListIds.has(item.songmid)) continue
ids.push(item.songmid)
map[item.songmid] = item
}
break
}
return {
...sourceList,
list: ids.map(id => map[id]),
}
}
const handleMergeListDataFromSnapshot = async(socket, snapshot) => {
const addMusicLocationType = global.appSetting.list.addMusicLocationType
const [remoteListData, localListData] = await Promise.all([getRemoteListData(socket), getLocalListData()])
console.log('handleMergeListDataFromSnapshot', 'remoteListData, localListData')
const newListData = {}
newListData.defaultList = mergeListDataFromSnapshot(localListData.defaultList, remoteListData.defaultList, snapshot.defaultList, addMusicLocationType)
newListData.loveList = mergeListDataFromSnapshot(localListData.loveList, remoteListData.loveList, snapshot.loveList, addMusicLocationType)
const localUserListData = createListDataObj(localListData)
const remoteUserListData = createListDataObj(remoteListData)
const snapshotUserListData = createListDataObj(snapshot)
const removedListIds = new Set()
const localUserListIds = new Set()
const remoteUserListIds = new Set()
for (const l of localListData.userList) localUserListIds.add(l.id)
for (const l of remoteListData.userList) remoteUserListIds.add(l.id)
for (const l of snapshot.userList) {
if (!localUserListIds.has(l.id)) removedListIds.add(l.id)
}
for (const l of snapshot.userList) {
if (!remoteUserListIds.has(l.id)) removedListIds.add(l.id)
}
let newUserList = []
for (const list of localListData.userList) {
if (removedListIds.has(list.id)) continue
const remoteList = remoteUserListData[list.id]
let newList
if (remoteList) {
newList = mergeListDataFromSnapshot(list, remoteList, snapshotUserListData[list.id], addMusicLocationType)
} else {
newList = { ...list }
}
newUserList.push(newList)
}
for (const list of remoteListData.userList) {
if (removedListIds.has(list.id) || localUserListData[list.id]) continue
newUserList.push({ ...list })
}
newListData.userList = newUserList
setLocalList(newListData)
setRemotelList(socket, newListData)
return updateSnapshot(socket.data.snapshotFilePath, JSON.stringify({
defaultList: newListData.defaultList,
loveList: newListData.loveList,
userList: newListData.userList,
})).then(() => {
socket.data.isCreatedSnapshot = true
return newListData
})
}
const registerUpdateSnapshotTask = (socket, snapshot) => {
if (!socket.data.isCreatedSnapshot) return
const handleUpdateSnapshot = throttle(({ defaultList, loveList, userList }) => {
if (defaultList != null) snapshot.defaultList = defaultList
if (loveList != null) snapshot.loveList = loveList
if (userList != null) snapshot.userList = userList
updateSnapshot(socket.data.snapshotFilePath, JSON.stringify(snapshot))
}, 10000)
global.lx_event.common.on(COMMON_EVENT_NAME.saveMyList, handleUpdateSnapshot)
socket.on('disconnect', () => {
global.lx_event.common.off(COMMON_EVENT_NAME.saveMyList, handleUpdateSnapshot)
})
}
const syncList = async socket => {
socket.data.snapshotFilePath = path.join(app.getPath('userData'), `snapshot-${Buffer.from(socket.data.keyInfo.clientId).toString('hex').substring(0, 10)}.json`)
let fileData
let isSyncRequired = false
try {
fileData = await fsPromises.readFile(socket.data.snapshotFilePath)
fileData = JSON.parse(fileData)
} catch (err) {
if (err.code !== 'ENOENT') throw err
isSyncRequired = true
}
console.log('isSyncRequired', isSyncRequired)
if (isSyncRequired) return handleSyncList(socket)
return handleMergeListDataFromSnapshot(socket, fileData)
}
const checkSyncQueue = async() => {
if (!syncingId) return
await wait()
return checkSyncQueue()
}
module.exports = async(_io, socket) => {
io = _io
await checkSyncQueue()
syncingId = socket.data.keyInfo.clientId
return syncList(socket).then(newListData => {
registerUpdateSnapshotTask(socket, { ...newListData })
return finishedSync(socket)
}).finally(() => {
syncingId = null
})
}

View File

@ -0,0 +1,93 @@
const { networkInterfaces } = require('os')
const { randomBytes, createCipheriv, createDecipheriv } = require('crypto')
const getStore = require('@common/store')
const STORE_NAME = 'sync'
exports.getAddress = () => {
const nets = networkInterfaces()
const results = []
// console.log(nets)
for (const name of Object.keys(nets)) {
for (const net of nets[name]) {
// Skip over non-IPv4 and internal (i.e. 127.0.0.1) addresses
if (net.family === 'IPv4' && !net.internal) {
results.push(net.address)
}
}
}
return results
}
let serverId
exports.getServerId = () => {
if (serverId) return serverId
const store = getStore(STORE_NAME)
serverId = store.get('serverId')
if (!serverId) {
serverId = randomBytes(4 * 4).toString('base64')
store.set('serverId', serverId)
}
return serverId
}
let keyInfos
exports.createClientKeyInfo = deviceName => {
const keyInfo = {
clientId: randomBytes(4 * 4).toString('base64'),
key: randomBytes(16).toString('base64'),
iv: randomBytes(16).toString('base64'),
deviceName,
}
const store = getStore(STORE_NAME)
if (!keyInfos) keyInfos = store.get('keys') || {}
if (Object.keys(keyInfos).length > 101) throw new Error('max keys')
keyInfos[keyInfo.clientId] = keyInfo
store.set('keys', keyInfos)
return keyInfo
}
exports.setClientKeyInfo = keyInfo => {
keyInfos[keyInfo.clientId] = keyInfo
const store = getStore(STORE_NAME)
store.set('keys', keyInfos)
}
exports.getClientKeyInfo = clientId => {
if (!keyInfos) {
const store = getStore(STORE_NAME)
keyInfos = store.get('keys') || {}
}
return keyInfos[clientId] || null
}
exports.generateCode = () => {
return Math.random().toString().substring(2, 8)
}
exports.aesEncrypt = (buffer, key, iv) => {
const cipher = createCipheriv('aes-128-cbc', Buffer.from(key, 'base64'), Buffer.from(iv, 'base64'))
return Buffer.concat([cipher.update(buffer), cipher.final()]).toString('base64')
}
exports.aesDecrypt = (text, key, iv) => {
const decipher = createDecipheriv('aes-128-cbc', Buffer.from(key, 'base64'), Buffer.from(iv, 'base64'))
return Buffer.concat([decipher.update(Buffer.from(text, 'base64')), decipher.final()]).toString()
}
exports.encryptMsg = (keyInfo, msg) => {
return msg
// if (!keyInfo) return ''
// return exports.aesEncrypt(msg, keyInfo.key, keyInfo.iv)
}
exports.decryptMsg = (keyInfo, enMsg) => {
return enMsg
// if (!keyInfo) return ''
// let msg = ''
// try {
// msg = exports.aesDecrypt(enMsg, keyInfo.key, keyInfo.iv)
// } catch (err) {
// console.log(err)
// }
// return msg
}

View File

@ -70,55 +70,63 @@ const destroyTray = () => {
const createMenu = tray => {
if (!global.modules.tray) return
let menu = []
global.modules.mainWindow && menu.push(global.modules.mainWindow.isVisible() ? {
label: '隐藏主界面',
click() {
global.modules.mainWindow.hide()
},
} : {
label: '显示主界面',
click() {
if (!global.modules.mainWindow) return
if (!global.modules.mainWindow.isVisible()) {
global.modules.mainWindow.show()
global.modules.mainWindow && menu.push(global.modules.mainWindow.isVisible()
? {
label: '隐藏主界面',
click() {
global.modules.mainWindow.hide()
},
}
global.modules.mainWindow.restore()
global.modules.mainWindow.focus()
},
})
menu.push(global.appSetting.desktopLyric.enable ? {
label: '关闭桌面歌词',
click() {
global.lx_core.setAppConfig({ desktopLyric: { enable: false } }, TRAY_EVENT_NAME.name)
},
} : {
label: '开启桌面歌词',
click() {
global.lx_core.setAppConfig({ desktopLyric: { enable: true } }, TRAY_EVENT_NAME.name)
},
})
menu.push(global.appSetting.desktopLyric.isLock ? {
label: '解锁桌面歌词',
click() {
global.lx_core.setAppConfig({ desktopLyric: { isLock: false } }, TRAY_EVENT_NAME.name)
},
} : {
label: '锁定桌面歌词',
click() {
global.lx_core.setAppConfig({ desktopLyric: { isLock: true } }, TRAY_EVENT_NAME.name)
},
})
menu.push(global.appSetting.desktopLyric.isAlwaysOnTop ? {
label: '取消置顶',
click() {
global.lx_core.setAppConfig({ desktopLyric: { isAlwaysOnTop: false } }, TRAY_EVENT_NAME.name)
},
} : {
label: '置顶歌词',
click() {
global.lx_core.setAppConfig({ desktopLyric: { isAlwaysOnTop: true } }, TRAY_EVENT_NAME.name)
},
})
: {
label: '显示主界面',
click() {
if (!global.modules.mainWindow) return
if (!global.modules.mainWindow.isVisible()) {
global.modules.mainWindow.show()
}
global.modules.mainWindow.restore()
global.modules.mainWindow.focus()
},
})
menu.push(global.appSetting.desktopLyric.enable
? {
label: '关闭桌面歌词',
click() {
global.lx_core.setAppConfig({ desktopLyric: { enable: false } }, TRAY_EVENT_NAME.name)
},
}
: {
label: '开启桌面歌词',
click() {
global.lx_core.setAppConfig({ desktopLyric: { enable: true } }, TRAY_EVENT_NAME.name)
},
})
menu.push(global.appSetting.desktopLyric.isLock
? {
label: '解锁桌面歌词',
click() {
global.lx_core.setAppConfig({ desktopLyric: { isLock: false } }, TRAY_EVENT_NAME.name)
},
}
: {
label: '锁定桌面歌词',
click() {
global.lx_core.setAppConfig({ desktopLyric: { isLock: true } }, TRAY_EVENT_NAME.name)
},
})
menu.push(global.appSetting.desktopLyric.isAlwaysOnTop
? {
label: '取消置顶',
click() {
global.lx_core.setAppConfig({ desktopLyric: { isAlwaysOnTop: false } }, TRAY_EVENT_NAME.name)
},
}
: {
label: '置顶歌词',
click() {
global.lx_core.setAppConfig({ desktopLyric: { isAlwaysOnTop: true } }, TRAY_EVENT_NAME.name)
},
})
menu.push({
label: '退出',
click() {

View File

@ -59,6 +59,7 @@ exports.createWindow = async userApi => {
})
}
global.modules.userApiWindow.webContents.session.setPermissionRequestHandler((webContents, permission, callback) => {
// eslint-disable-next-line node/no-callback-literal
callback(false)
})

View File

@ -23,3 +23,4 @@ require('./musicUrl')
require('./kw_decodeLyric')
require('./userApi')
require('./sync')

View File

@ -24,6 +24,7 @@ mainOn(ipcMainWindowNames.save_playlist, (event, { type, data }) => {
switch (type) {
case 'myList':
handleSaveList(data)
global.lx_event.common.saveMyList(data)
break
case 'downloadList':
getStore('downloadList').set('list', data)

View File

@ -0,0 +1,33 @@
const { mainSend, NAMES: { mainWindow: ipcMainWindowNames }, mainOn, mainHandle } = require('@common/ipc')
const { eventNames, modules, startServer, stopServer, getStatus, generateCode } = require('../modules/sync')
mainOn(ipcMainWindowNames.sync_action_list, (event, { action, data }) => {
modules.list.sendListAction(action, data)
})
mainHandle(ipcMainWindowNames.sync_enable, (event, { enable, port }) => {
return enable ? startServer(port) : stopServer()
})
mainHandle(ipcMainWindowNames.sync_get_status, () => {
return getStatus()
})
mainHandle(ipcMainWindowNames.sync_generate_code, () => {
return generateCode()
})
mainOn(ipcMainWindowNames.sync_list, (event, { action, data }) => {
global.lx_event.sync.sync_handle_list({ action, data })
})
global.lx_event.sync.on(eventNames.sync_action_list, ({ action, data }) => {
mainSend(global.modules.mainWindow, ipcMainWindowNames.sync_action_list, { action, data })
})
global.lx_event.sync.on(eventNames.status, status => {
mainSend(global.modules.mainWindow, ipcMainWindowNames.sync_status, status)
})
global.lx_event.sync.on(eventNames.sync_list, ({ action, data }) => {
mainSend(global.modules.mainWindow, ipcMainWindowNames.sync_list, { action, data })
})

View File

@ -1,4 +1,4 @@
const { log } = require('../../common/utils')
const { log, isWin } = require('../../common/utils')
const { autoUpdater } = require('electron-updater')
const { mainOn, mainSend, NAMES: { mainWindow: ipcMainWindowNames } } = require('../../common/ipc')
@ -120,6 +120,11 @@ module.exports = () => {
}, 1000)
})
autoUpdater.checkForUpdates()
// 由于集合安装包中不包含win arm版这将会导致arm版更新失败
if (isWin && process.arch.includes('arm')) {
handleSendEvent({ type: ipcMainWindowNames.update_error, info: 'failed' })
} else {
autoUpdater.checkForUpdates()
}
}

View File

@ -8,6 +8,7 @@
core-icons
material-version-modal(v-show="version.showModal")
material-pact-modal(v-show="!setting.isAgreePact || globalObj.isShowPact")
material-sync-mode-modal(v-show="globalObj.sync.isShowSyncMode")
#container(v-else :class="theme")
core-aside#left
#right
@ -17,24 +18,19 @@
core-icons
material-version-modal(v-show="version.showModal")
material-pact-modal(v-show="!setting.isAgreePact || globalObj.isShowPact")
material-sync-mode-modal(v-show="globalObj.sync.isShowSyncMode")
</template>
<script>
import dnscache from 'dnscache'
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, parseUrlParams, saveSetting } from './utils'
import { base as eventBaseName } from './event/names'
import { base as eventBaseName, sync as eventSyncName } from './event/names'
import apiSourceInfo from './utils/music/api-source-info'
window.ELECTRON_DISABLE_SECURITY_WARNINGS = process.env.ELECTRON_DISABLE_SECURITY_WARNINGS
dnscache({
enable: true,
ttl: 21600,
cachesize: 1000,
})
const listThrottle = (fn, delay = 100) => {
let timer = null
@ -67,6 +63,11 @@ export default {
message: 'initing',
apis: {},
},
sync: {
enable: false,
isShowSyncMode: false,
deviceName: '',
},
},
updateTimeout: null,
envParams: {
@ -109,6 +110,14 @@ export default {
document.body.classList.add(this.isDT ? 'disableTransparent' : 'transparent')
window.eventHub.$emit(eventBaseName.bindKey)
this.init()
window.eventHub.$on(eventSyncName.handle_action_list, this.handleSyncAction)
window.eventHub.$on(eventSyncName.handle_sync_list, this.handleSyncList)
if (this.setting.sync.enable && this.setting.sync.port) {
rendererInvoke(NAMES.mainWindow.sync_enable, {
enable: this.setting.sync.enable,
port: this.setting.sync.port,
})
}
},
watch: {
setting: {
@ -199,7 +208,25 @@ export default {
methods: {
...mapActions(['getVersionInfo']),
...mapMutations(['setNewVersion', 'setVersionModalVisible', 'setDownloadProgress', 'setSetting', 'setDesktopLyricConfig']),
...mapMutations('list', ['initList']),
...mapMutations('list', {
list_initList: 'initList',
list_setList: 'setList',
list_listAdd: 'listAdd',
list_listMove: 'listMove',
list_listAddMultiple: 'listAddMultiple',
list_listMoveMultiple: 'listMoveMultiple',
list_listRemove: 'listRemove',
list_listRemoveMultiple: 'listRemoveMultiple',
list_listClear: 'listClear',
list_updateMusicInfo: 'updateMusicInfo',
list_createUserList: 'createUserList',
list_removeUserList: 'removeUserList',
list_setUserListName: 'setUserListName',
list_moveupUserList: 'moveupUserList',
list_movedownUserList: 'movedownUserList',
list_setMusicPosition: 'setMusicPosition',
list_setSyncListData: 'setSyncListData',
}),
...mapMutations('download', ['updateDownloadList']),
...mapMutations('search', {
setSearchHistoryList: 'setHistory',
@ -278,6 +305,7 @@ export default {
this.listenEvent()
asyncTask.push(this.initData())
asyncTask.push(this.initUserApi())
this.globalObj.sync.enable = this.setting.sync.enable
this.globalObj.apiSource = this.setting.apiSource
if (/^user_api/.test(this.setting.apiSource)) {
rendererInvoke(NAMES.mainWindow.set_user_api, this.setting.apiSource)
@ -334,7 +362,7 @@ export default {
if (!defaultList.list) defaultList.list = []
if (!loveList.list) loveList.list = []
this.initList({ defaultList, loveList, userList })
this.list_initList({ defaultList, loveList, userList })
this.initDownloadList(downloadList) //
this.initPlayInfo()
})
@ -372,7 +400,7 @@ export default {
if (!list || !list.list[info.index]) return
info.list = list.list
}
if (!info.list || !info.list[info.index]) return
window.restorePlayInfo = info
this.setPlayList({
list: {
@ -596,6 +624,84 @@ export default {
event.target.value = ''
event.target.blur()
},
handleSyncAction({ action, data }) {
if (typeof data == 'object') data.isSync = true
// console.log(action, data)
switch (action) {
case 'set_list':
this.list_setList(data)
break
case 'list_add':
this.list_listAdd(data)
break
case 'list_move':
this.list_listMove(data)
break
case 'list_add_multiple':
this.list_listAddMultiple(data)
break
case 'list_move_multiple':
this.list_listMoveMultiple(data)
break
case 'list_remove':
this.list_listRemove(data)
break
case 'list_remove_multiple':
this.list_listRemoveMultiple(data)
break
case 'list_clear':
this.list_listClear(data)
break
case 'update_music_info':
this.list_updateMusicInfo(data)
break
case 'create_user_list':
this.list_createUserList(data)
break
case 'remove_user_list':
this.list_removeUserList(data)
break
case 'set_user_list_name':
this.list_setUserListName(data)
break
case 'moveup_user_list':
this.list_moveupUserList(data)
break
case 'movedown_user_list':
this.list_movedownUserList(data)
break
case 'set_music_position':
this.list_setMusicPosition(data)
break
default:
break
}
},
handleSyncList({ action, data }) {
switch (action) {
case 'getData':
global.eventHub.$emit(eventSyncName.send_sync_list, {
action: 'getData',
data: {
defaultList: this.defaultList,
loveList: this.loveList,
userList: this.userList,
},
})
break
case 'setData':
this.list_setSyncListData(data)
break
case 'selectMode':
this.globalObj.sync.deviceName = data.deviceName
this.globalObj.sync.isShowSyncMode = true
break
case 'closeSelectMode':
this.globalObj.sync.isShowSyncMode = false
break
}
},
},
beforeDestroy() {
this.clearUpdateTimeout()
@ -683,7 +789,7 @@ body {
}
#view {
flex: auto;
height: 0;
min-height: 0;
}
each(@themes, {

View File

@ -123,6 +123,9 @@ small {
.small {
font-size: .9em;
}
.tip {
color: @color-theme_2-font-label;
}
strong {
font-weight: bold;
}
@ -192,6 +195,9 @@ each(@themes, {
button, input, textarea, a {
color: ~'@{color-@{value}-theme_2-font}';
}
.tip {
color: ~'@{color-@{value}-theme_2-font-label}';
}
.hover, a {
&:hover {

View File

@ -236,6 +236,7 @@ export default {
break
}
})
this.registerMediaSessionHandler()
navigator.mediaDevices.addEventListener('devicechange', this.handleMediaListChange)
document.addEventListener('mousemove', this.handleVolumeMsMove)
document.addEventListener('mouseup', this.handleVolumeMsUp)
@ -255,6 +256,8 @@ export default {
if (window.restorePlayInfo) {
this.handleRestorePlay(window.restorePlayInfo)
window.restorePlayInfo = null
navigator.mediaSession.playbackState = 'paused'
this.updateMediaSessionInfo()
return
}
// console.log('changePlay')
@ -265,7 +268,7 @@ export default {
'setting.player.togglePlayMethod'(n) {
audio.loop = n === 'singleLoop'
if (this.playedList.length) this.clearPlayedList()
if (n == 'random') this.setPlayedList(this.playMusicInfo)
if (n == 'random' && !this.playMusicInfo.isTempPlay) this.setPlayedList(this.playMusicInfo)
},
'setting.player.isMute'(n) {
audio.muted = n
@ -411,7 +414,7 @@ export default {
this.restorePlayTime = 0
}
if (!this.targetSong.interval && this.listId != 'download') {
this.updateMusicInfo({ id: this.listId, index: this.playIndex, data: { interval: formatPlayTime2(this.maxPlayTime) }, musicInfo: this.targetSong })
this.updateMusicInfo({ listId: this.listId, id: this.targetSong.songmid, musicInfo: this.targetSong, data: { interval: formatPlayTime2(this.maxPlayTime) } })
}
})
audio.addEventListener('loadstart', () => {
@ -480,10 +483,11 @@ export default {
},
async play() {
this.clearDelayNextTimeout()
this.updateMediaSessionInfo()
const targetSong = this.targetSong
if (this.setting.player.togglePlayMethod == 'random') this.setPlayedList(this.playMusicInfo)
if (this.setting.player.togglePlayMethod == 'random' && !this.playMusicInfo.isTempPlay) this.setPlayedList(this.playMusicInfo)
this.retryNum = 0
this.restorePlayTime = 0
@ -551,20 +555,28 @@ export default {
this.handleUpdateWinLyricInfo('play', audio.currentTime * 1000)
this.setAppTitle()
this.sendProgressEvent(this.progress, 'normal')
navigator.mediaSession.playbackState = 'playing'
},
stopPlay() {
this.isPlay = false
window.lrc.pause()
this.handleUpdateWinLyricInfo('pause')
this.sendProgressEvent(this.progress, 'paused')
this.clearAppTitle()
this.$nextTick(() => {
if (this.playMusicInfo) {
this.sendProgressEvent(this.progress, 'paused')
navigator.mediaSession.playbackState = 'paused'
} else {
this.sendProgressEvent(this.progress, 'none')
navigator.mediaSession.playbackState = 'none'
}
})
},
handleSetProgress(event) {
this.setProgress(event.offsetX / this.pregessWidth)
this.setProgress(event.offsetX / this.pregessWidth * this.maxPlayTime)
},
setProgress(pregress) {
setProgress(time) {
if (!audio.src) return
const time = pregress * this.maxPlayTime
if (this.restorePlayTime) this.restorePlayTime = time
if (this.mediaBuffer.playTime) {
this.clearBufferTimeout()
@ -634,12 +646,15 @@ export default {
if (!this.musicInfo.img) {
this.getPic(targetSong).then(() => {
if (targetSong.songmid !== this.musicInfo.songmid) return
this.musicInfo.img = targetSong.img
this.updateMediaSessionInfo()
})
}
},
setLrc(targetSong) {
this.getLrc(targetSong).then(({ lyric, tlyric, lxlyric }) => {
if (targetSong.songmid !== this.musicInfo.songmid) return
this.musicInfo.lrc = lyric
this.musicInfo.tlrc = tlyric
this.musicInfo.lxlrc = lxlyric
@ -834,7 +849,7 @@ export default {
this.playNext()
break
case 'progress':
this.setProgress(data)
this.handleSetProgress(data)
break
case 'volume':
break
@ -902,6 +917,59 @@ export default {
if (this.setting.player.togglePlayMethod == 'random') this.setPlayedList(this.playMusicInfo)
},
updateMediaSessionInfo() {
const mediaMetadata = {
title: this.targetSong.name,
artist: this.targetSong.singer,
album: this.targetSong.albumName,
}
if (this.targetSong.img) mediaMetadata.artwork = [{ src: this.targetSong.img }]
navigator.mediaSession.metadata = new window.MediaMetadata(mediaMetadata)
},
registerMediaSessionHandler() {
// navigator.mediaSession.setActionHandler('play', () => {
// if (this.isPlay || !this.playMusicInfo) return
// console.log('play')
// this.startPlay()
// })
// navigator.mediaSession.setActionHandler('pause', () => {
// if (!this.isPlay || !this.playMusicInfo) return
// console.log('pause')
// this.stopPlay()
// })
navigator.mediaSession.setActionHandler('stop', () => {
if (!this.isPlay || !this.playMusicInfo) return
console.log('stop')
this.stopPlay()
})
navigator.mediaSession.setActionHandler('seekbackward', details => {
if (!this.isPlay || !this.playMusicInfo) return
console.log('seekbackward')
this.setProgress(Math.max(audio.currentTime - details.seekOffset, 0))
})
navigator.mediaSession.setActionHandler('seekforward', details => {
if (!this.isPlay || !this.playMusicInfo) return
console.log('seekforward')
this.setProgress(Math.min(audio.currentTime + details.seekOffset, audio.duration))
})
navigator.mediaSession.setActionHandler('seekto', details => {
console.log('seekto', details.seekTime)
let time = Math.min(details.seekTime, audio.duration)
time = Math.max(time, 0)
this.setProgress(time)
})
navigator.mediaSession.setActionHandler('previoustrack', () => {
console.log('previoustrack')
this.playPrev()
})
navigator.mediaSession.setActionHandler('nexttrack', () => {
console.log('nexttrack')
this.playNext()
})
// navigator.mediaSession.setActionHandler('skipad', () => {
// console.log('')
// })
},
},
}
</script>

View File

@ -59,7 +59,7 @@
div(:class="$style.progressContent")
div(:class="$style.progress")
//- div(:class="[$style.progressBar, $style.progressBar1]" :style="{ transform: `scaleX(${progress || 0})` }")
div(:class="[$style.progressBar, $style.progressBar2, isActiveTransition ? $style.barTransition : '']" @transitionend="handleTransitionEnd" :style="{ transform: `scaleX(${playInfo.progress || 0})`, willChange: isPlay || isActiveTransition ? 'transform' : 'auto' }")
div(:class="[$style.progressBar, $style.progressBar2, isActiveTransition ? $style.barTransition : '']" @transitionend="handleTransitionEnd" :style="{ transform: `scaleX(${playInfo.progress || 0})` }")
div(:class="$style.progressMask" @click='setProgress' ref="dom_progress")
div(:class="$style.timeLabel")
span(style="margin-left: 15px") {{playInfo.status}}
@ -282,7 +282,7 @@ export default {
setProgress(event) {
this.$emit('action', {
type: 'progress',
data: event.offsetX / this.pregessWidth,
data: event,
})
},
setProgressWidth() {

View File

@ -140,7 +140,7 @@ export default {
box-shadow: 0 0 3px rgba(0, 0, 0, .3);
overflow: hidden;
max-height: 80%;
max-width: 70%;
max-width: 76%;
position: relative;
display: flex;
flex-flow: column nowrap;

View File

@ -0,0 +1,164 @@
<template lang="pug">
material-modal(:show="globalObj.sync.isShowSyncMode" @close="handleClose(false)" :bgClose="false" :close-btn="false")
main(:class="$style.main")
h2 {{$t('material.sync_mode_modal.title', { name: globalObj.sync.deviceName })}}
div.scroll(:class="$style.content")
dl(:class="$style.btnGroup")
dt(:class="$style.label") {{$t('material.sync_mode_modal.merge_label')}}
dd(:class="$style.btns")
material-btn(:class="$style.btn" @click="handleSelectMode('merge_local_remote')") {{$t('material.sync_mode_modal.merge_btn_local_remote')}}
material-btn(:class="$style.btn" @click="handleSelectMode('merge_remote_local')") {{$t('material.sync_mode_modal.merge_btn_remote_local')}}
dl(:class="$style.btnGroup")
dt(:class="$style.label") {{$t('material.sync_mode_modal.overwrite_label')}}
dd(:class="$style.btns")
material-btn(:class="$style.btn" @click="handleSelectMode('overwrite_local_remote')") {{$t('material.sync_mode_modal.overwrite_btn_local_remote')}}
material-btn(:class="$style.btn" @click="handleSelectMode('overwrite_remote_local')") {{$t('material.sync_mode_modal.overwrite_btn_remote_local')}}
dd(style="font-size: 14px; margin-top: 5px;")
material-checkbox(id="sync_mode_modal_isOverwrite" v-model="isOverwrite" :label="$t('material.sync_mode_modal.overwrite')")
dl(:class="$style.btnGroup")
dt(:class="$style.label") {{$t('material.sync_mode_modal.other_label')}}
dd(:class="$style.btns")
material-btn(:class="$style.btn" @click="handleSelectMode('none')") {{$t('material.sync_mode_modal.overwrite_btn_none')}}
material-btn(:class="$style.btn" @click="handleSelectMode('cancel')") {{$t('material.sync_mode_modal.overwrite_btn_cancel')}}
dl(:class="$style.btnGroup")
dd
section(:class="$style.tipGroup")
h3(:class="$style.title") {{$t('material.sync_mode_modal.merge_tip')}}
p(:class="$style.tip") {{$t('material.sync_mode_modal.merge_tip_desc')}}
section(:class="$style.tipGroup")
h3(:class="$style.title") {{$t('material.sync_mode_modal.overwrite_tip')}}
p(:class="$style.tip") {{$t('material.sync_mode_modal.overwrite_tip_desc')}}
section(:class="$style.tipGroup")
h3(:class="$style.title") {{$t('material.sync_mode_modal.other_tip')}}
p(:class="$style.tip") {{$t('material.sync_mode_modal.other_tip_desc')}}
</template>
<script>
import { sync as eventSyncName } from '@renderer/event/names'
export default {
data() {
return {
isOverwrite: false,
globalObj: {
sync: {
isShowSyncMode: false,
deviceName: '',
},
},
}
},
computed: {
},
mounted() {
this.$nextTick(() => {
this.globalObj = window.globalObj
})
},
methods: {
handleSelectMode(mode) {
if (mode.startsWith('overwrite') && this.isOverwrite) mode += '_full'
window.eventHub.$emit(eventSyncName.send_sync_list, {
action: 'selectMode',
data: mode,
})
this.handleClose()
},
handleClose() {
this.globalObj.sync.isShowSyncMode = false
},
},
}
</script>
<style lang="less" module>
@import '../../assets/styles/layout.less';
.main {
padding: 15px;
max-width: 700px;
min-width: 200px;
min-height: 0;
display: flex;
flex-flow: column nowrap;
justify-content: center;
h2 {
font-size: 16px;
color: @color-theme_2-font;
line-height: 1.3;
text-align: center;
}
}
.content {
flex: auto;
padding: 15px 0 5px;
padding-right: 5px;
.btnGroup + .btnGroup {
margin-top: 10px;
}
.label {
color: @color-theme_2-font-label;
font-size: 14px;
line-height: 2;
}
.desc {
line-height: 1.5;
font-size: 14px;
text-align: justify;
}
.tipGroup {
display: flex;
flex-direction: row;
font-size: 12px;
+ .tipGroup {
margin-top: 5px;
}
.title {
white-space: nowrap;
font-weight: bold;
margin-right: 5px;
}
.tip {
line-height: 1.3;
}
}
}
.btns {
display: flex;
align-items: center;
}
.btn {
display: block;
white-space: nowrap;
+.btn {
margin-left: 15px;
}
&:last-child {
margin-bottom: 0;
}
}
each(@themes, {
:global(#container.@{value}) {
.main {
h2 {
color: ~'@{color-@{value}-theme_2-font}';
}
}
.name {
color: ~'@{color-@{value}-theme}';
}
}
})
</style>

View File

@ -1,7 +1,7 @@
import Vue from 'vue'
import keyBind from '../utils/keyBind'
import { rendererOn, rendererSend, NAMES, rendererInvoke } from '../../common/ipc'
import { base as baseName } from './names'
import { base as baseName, sync as syncName } from './names'
import { common as hotKeyNamesCommon } from '../../common/hotKey'
const eventHub = window.eventHub = new Vue()
@ -77,3 +77,18 @@ rendererOn(NAMES.mainWindow.set_hot_key_config, (event, config) => {
}
window.eventHub.$emit(baseName.set_hot_key_config, config)
})
rendererOn(NAMES.mainWindow.sync_action_list, (event, { action, data }) => {
window.eventHub.$emit(syncName.handle_action_list, { action, data })
})
eventHub.$on(syncName.send_action_list, ({ action, data }) => {
if (!window.globalObj.sync.enable) return
rendererSend(NAMES.mainWindow.sync_action_list, { action, data })
})
rendererOn(NAMES.mainWindow.sync_list, (event, { action, data }) => {
window.eventHub.$emit(syncName.handle_sync_list, { action, data })
})
eventHub.$on(syncName.send_sync_list, ({ action, data }) => {
if (!window.globalObj.sync.enable) return
rendererSend(NAMES.mainWindow.sync_list, { action, data })
})

View File

@ -10,6 +10,12 @@ const names = {
set_config: 'set_config',
set_hot_key_config: 'set_hot_key_config',
},
sync: {
send_action_list: 'send_action_list',
handle_action_list: 'handle_action_list',
send_sync_list: 'send_sync_list',
handle_sync_list: 'handle_sync_list',
},
}
for (const item of Object.keys(names)) {
@ -20,3 +26,4 @@ for (const item of Object.keys(names)) {
}
export const base = names.base
export const sync = names.sync

View File

@ -0,0 +1,19 @@
{
"merge_btn_local_remote": "Local list merge remote list",
"merge_btn_remote_local": "Remote list merge local list",
"merge_label": "Merge",
"merge_tip": "Merge:",
"merge_tip_desc": "Merge the two lists together, the same song will be removed (the song of the merged person is removed), and different songs will be added.",
"other_label": "Other",
"other_tip": "Other: ",
"other_tip_desc": "\"Only use real-time synchronization function\" will not modify the lists of both parties, only real-time synchronization operations; \"Cancel synchronization\" will directly disconnect the two parties.",
"overwrite": "Full coverage",
"overwrite_btn_cancel": "Cancel sync",
"overwrite_btn_local_remote": "Local list Overwrite remote list",
"overwrite_btn_none": "Only use real-time synchronization",
"overwrite_btn_remote_local": "Remote list Overwrite local list",
"overwrite_label": "Cover",
"overwrite_tip": "Cover: ",
"overwrite_tip_desc": "The list with the same ID of the covered person and the covered list will be deleted and replaced with the list of the covered person (lists with different list IDs will be merged together). If you check Complete coverage, all lists of the covered person will be moved. \nDivide, and then replace with a list of overriders.",
"title": "Choose how to synchronize the list with {name}"
}

View File

@ -1,154 +1,151 @@
{
"about": "About lx-music-desktop",
"backup": "Backup and restore",
"backup_all": "All data (list data and setting data)",
"backup_all_export": "Export",
"backup_all_export_desc": "Select the backup to...",
"backup_all_import": "Import",
"backup_all_import_desc": "Select a backup file",
"backup_part": "Partial data (list data includes audition list, favorite list, user-defined list, setting data does not include shortcut key settings)",
"backup_part_export_list": "Export lists",
"backup_part_export_list_desc": "Save the list to...",
"backup_part_export_setting": "Export settings",
"backup_part_export_setting_desc": "Save the list to...",
"backup_part_import_list": "Import lists",
"backup_part_import_list_desc": "Select a list backup",
"backup_part_import_setting": "Import settings",
"backup_part_import_setting_desc": "Select the Settings file",
"basic": "General",
"basic_theme": "Theme",
"basic_show_animation": "Show switching animation",
"basic_animation": "Random pop-up animation",
"basic_source_title": "Choose a music source",
"basic_source_test": "Test API (Available for most software features)",
"basic_source_temp": "Temporary API (some features not available; workaround if Test API unavailable)",
"basic_source": "Music source",
"basic_sourcename_title": "Select the name of music source",
"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",
"basic_window_size_small": "Small",
"basic_window_size_medium": "Medium",
"basic_window_size_big": "Large",
"basic_window_size_larger": "Larger",
"basic_window_size_oversized": "Oversized",
"basic_window_size_huge": "Huge",
"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",
"basic_control_btn_position_left": "Left",
"basic_control_btn_position_right": "Right",
"play": "Play",
"play_save_play_time": "Remember playback progress",
"play_lyric_transition": "Show lyrics translation",
"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": "Pause the song when the current sound output device is changed",
"basic_lang": "Language",
"basic_lang_title": "The language displayed in the software",
"basic_show_animation": "Show switching animation",
"basic_source": "Music source",
"basic_source_status_failed": "Initialization failed",
"basic_source_status_initing": "Initializing",
"basic_source_status_success": "Initialization successful",
"basic_source_temp": "Temporary API (some features not available; workaround if Test API unavailable)",
"basic_source_test": "Test API (Available for most software features)",
"basic_source_title": "Choose a music source",
"basic_source_user_api_btn": "Custom Source Management",
"basic_sourcename": "Source name",
"basic_sourcename_alias": "Aliases",
"basic_sourcename_real": "Original",
"basic_sourcename_title": "Select the name of music source",
"basic_theme": "Theme",
"basic_to_tray": "Do not exit the software when closing the software and minimize it to the system tray",
"basic_window_size": "Window size",
"basic_window_size_big": "Large",
"basic_window_size_huge": "Huge",
"basic_window_size_larger": "Larger",
"basic_window_size_medium": "Medium",
"basic_window_size_oversized": "Oversized",
"basic_window_size_small": "Small",
"basic_window_size_smaller": "Smaller",
"basic_window_size_title": "Set the window size",
"click_copy": "Click to copy",
"click_open": "Click to open",
"desktop_lyric": "Desktop Lyric Settings",
"desktop_lyric_always_on_top": "Make the lyrics always above other windows",
"desktop_lyric_enable": "Display lyrics",
"desktop_lyric_lock": "Lock lyrics",
"desktop_lyric_always_on_top": "Make the lyrics always above other windows",
"desktop_lyric_lock_screen": "It is not allowed to drag the lyrics window out of the main screen",
"search": "Search",
"search_hot": "Top Searches",
"search_history": "Search history",
"search_focus_search_box": "Automatically focus the search box on startup",
"list": "List",
"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",
"download_path_title": "Define the path to downloading",
"download_path": "Download path",
"download_path_label": "Current: ",
"download_path_open_label": "Click to open this path",
"download_path_change_btn": "Change",
"download_name_title": "Select the music naming method for downloading",
"download_name": "Music file naming",
"download_data_embed": "Whether to embed the following content in the audio file",
"download_embed_pic": "Embedding cover",
"download_embed_lyric": "Embedding lyric",
"download_lyric_title": "Select whether to download the lyrics file",
"download_embed_pic": "Embedding cover",
"download_enable": "Whether to enable download function",
"download_lyric": "Lyrics download",
"download_lyric_title": "Select whether to download the lyrics file",
"download_name": "Music file naming",
"download_name1": "Title - Artist",
"download_name2": "Artist - Title",
"download_name3": "Title only",
"download_use_other_source": "Automatically change the source to download (when the song cannot be downloaded from the original source, try to switch to another source to download. Note: this function does not 100% guarantee that the version of the song after the source is changed is the same as the original version)",
"download_name_title": "Select the music naming method for downloading",
"download_path": "Download path",
"download_path_change_btn": "Change",
"download_path_label": "Current: ",
"download_path_open_label": "Click to open this path",
"download_path_title": "Define the path to downloading",
"download_select_save_path": "Select the save path",
"download_use_other_source": "Automatically change the source to download (when the song cannot be downloaded from the original source, try to switch to another source to download. Note: this function does not 100% guarantee that the version of the song after the source is changed is the same as the original version)",
"hot_key": "Shortcut Key Settings",
"hot_key_local_title": "Shortcut Keys in Software",
"hot_key_global_title": "Global Shortcut Key",
"hot_key_tip_input": "Please enter a new key",
"hot_key_unset_input": "Not Set",
"hot_key_common_toggle_close": "Quit Program",
"hot_key_common_toggle_min": "Minimize/Restore Program",
"hot_key_common_min": "Minimize the program",
"hot_key_common_toggle_hide": "Show/Hide Program",
"hot_key_common_focus_search_input": "Focus Search Box",
"hot_key_player_toggle_play": "Play/Pause Control",
"hot_key_player_prev": "Previous Song",
"hot_key_common_min": "Minimize the program",
"hot_key_common_toggle_close": "Quit Program",
"hot_key_common_toggle_hide": "Show/Hide Program",
"hot_key_common_toggle_min": "Minimize/Restore Program",
"hot_key_desktop_lyric_toggle_always_top": "Top Desktop Lyrics Switch",
"hot_key_desktop_lyric_toggle_lock": "Desktop Lyric Lock Switch",
"hot_key_desktop_lyric_toggle_visible": "Turn on/off desktop lyrics",
"hot_key_global_title": "Global Shortcut Key",
"hot_key_local_title": "Shortcut Keys in Software",
"hot_key_player_next": "Next Song",
"hot_key_player_volume_up": "Increase Volume",
"hot_key_player_prev": "Previous Song",
"hot_key_player_toggle_play": "Play/Pause Control",
"hot_key_player_volume_down": "Reduce Volume",
"hot_key_player_volume_mute": "Mute Switch",
"hot_key_desktop_lyric_toggle_visible": "Turn on/off desktop lyrics",
"hot_key_desktop_lyric_toggle_lock": "Desktop Lyric Lock Switch",
"hot_key_desktop_lyric_toggle_always_top": "Top Desktop Lyrics Switch",
"hot_key_player_volume_up": "Increase Volume",
"hot_key_tip_input": "Please enter a new key",
"hot_key_unset_input": "Not Set",
"is_enable": "Enabled",
"is_show": "Showed",
"list": "List",
"list_add_music_location_type": "Position when adding a song to the list",
"list_add_music_location_type_bottom": "Bottom",
"list_add_music_location_type_top": "Top",
"list_scroll": "Remember the position of the scroll bar of the playlist (only valid for my music classification)",
"list_source": "Show song source (only valid for my music category)",
"network": "Network",
"network_proxy_title": "HTTP Proxy (False setting would block Internet connections)",
"network_proxy_host": "Host",
"network_proxy_port": "Port",
"network_proxy_username": "Username",
"network_proxy_password": "Password",
"network_proxy_port": "Port",
"network_proxy_title": "HTTP Proxy (False setting would block Internet connections)",
"network_proxy_username": "Username",
"odc": "Auto clear",
"odc_clear_search_input": "Clear the search box when you are not searching",
"odc_clear_search_list": "Clear the search list when you are not searching",
"backup": "Backup and restore",
"backup_part": "Partial data (list data includes audition list, favorite list, user-defined list, setting data does not include shortcut key settings)",
"backup_part_import_list": "Import lists",
"backup_part_export_list": "Export lists",
"backup_part_import_setting": "Import settings",
"backup_part_export_setting": "Export settings",
"backup_all": "All data (list data and setting data)",
"backup_all_import": "Import",
"backup_all_export": "Export",
"backup_all_import_desc": "Select a backup file",
"backup_all_export_desc": "Select the backup to...",
"backup_part_import_setting_desc": "Select the Settings file",
"backup_part_export_setting_desc": "Save the list to...",
"backup_part_import_list_desc": "Select a list backup",
"backup_part_export_list_desc": "Save the list to...",
"other": "Extras",
"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",
"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_clear_btn": "Clear resource cache",
"other_resource_cache_label": "The software has used cache size: ",
"other_tray_theme": "Tray Icon Style",
"other_tray_theme_native": "Solid Color",
"other_tray_theme_origin": "Primary Color",
"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",
"play": "Play",
"play_lyric_lxlrc": "Use Karaoke-style lyrics playback (if supported)",
"play_lyric_transition": "Show lyrics translation",
"play_mediaDevice": "Audio output",
"play_mediaDevice_remove_stop_play": "Pause the song when the current sound output device is changed",
"play_mediaDevice_title": "Select a media device for audio output",
"play_quality": "Play 320K quality songs first (if supported)",
"play_save_play_time": "Remember playback progress",
"play_task_bar": "Show playing progress on the taskbar",
"search": "Search",
"search_focus_search_box": "Automatically focus the search box on startup",
"search_history": "Search history",
"search_hot": "Top Searches",
"sync": "Data synchronization [This is a test function, it is recommended to back up the playlist before using it for the first time]",
"sync_address": "Synchronization service address: {address}",
"sync_auth_code": "Connection code: {code}",
"sync_device": "Connected devices: {devices}",
"sync_enable": "Enable the synchronization function (because the data is transmitted in clear text, please use it under a trusted network)",
"sync_port": "Sync port settings",
"sync_port_tip": "Please enter the synchronization service port number",
"sync_refresh_code": "Refresh the connection code",
"update": "Update",
"update_latest_label": "Latest version: ",
"update_unknown": "Unknown",
"update_checking": "Checking for updates...",
"update_current_label": "Current version: ",
"update_downloading": "Update is found and being downloaded...⏳",
"update_progress": "Download progress: ",
"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": "Processing update...",
"about": "About lx-music-desktop",
"is_enable": "Enabled",
"is_show": "Showed",
"click_open": "Click to open",
"click_copy": "Click to copy"
"update_latest": "The software is up-to-date, enjoy yourself!🥂",
"update_latest_label": "Latest version: ",
"update_open_version_modal_btn": "Open the update window🚀",
"update_progress": "Download progress: ",
"update_unknown": "Unknown"
}

View File

@ -0,0 +1,19 @@
{
"merge_btn_local_remote": "本机列表 合并 远程列表",
"merge_btn_remote_local": "远程列表 合并 本机列表",
"merge_label": "合并",
"merge_tip": "合并:",
"merge_tip_desc": "将两边的列表合并到一起,相同的歌曲将被去掉(去掉的是被合并者的歌曲),不同的歌曲将被添加。",
"other_label": "其他",
"other_tip": "其他:",
"other_tip_desc": "“仅使用实时同步功能”将不修改双方的列表,仅实时同步操作;“取消同步”将直接断开双方的连接。",
"overwrite": "完全覆盖",
"overwrite_btn_cancel": "取消同步",
"overwrite_btn_local_remote": "本机列表 覆盖 远程列表",
"overwrite_btn_none": "仅使用实时同步功能",
"overwrite_btn_remote_local": "远程列表 覆盖 本机列表",
"overwrite_label": "覆盖",
"overwrite_tip": "覆盖:",
"overwrite_tip_desc": "被覆盖者与覆盖者列表ID相同的列表将被删除后替换成覆盖者的列表列表ID不同的列表将被合并到一起若勾选完全覆盖则被覆盖者的所有列表将被移除然后替换成覆盖者的列表。",
"title": "选择与 {name} 的列表同步方式"
}

View File

@ -1,154 +1,151 @@
{
"about": "关于洛雪音乐",
"backup": "备份与恢复",
"backup_all": "所有数据(列表数据与设置数据)",
"backup_all_export": "导出",
"backup_all_export_desc": "选择备份保存位置",
"backup_all_import": "导入",
"backup_all_import_desc": "选择备份文件",
"backup_part": "部分数据(列表数据包括试听列表、收藏列表、用户自定义列表,设置数据不包括快捷键设置)",
"backup_part_export_list": "导出列表",
"backup_part_export_list_desc": "选择歌单保存位置",
"backup_part_export_setting": "导出设置",
"backup_part_export_setting_desc": "选择设置保存位置",
"backup_part_import_list": "导入列表",
"backup_part_import_list_desc": "选择列表文件",
"backup_part_import_setting": "导入设置",
"backup_part_import_setting_desc": "选择配置文件",
"basic": "基本设置",
"basic_theme": "主题颜色",
"basic_animation": "弹出层随机动画",
"basic_show_animation": "显示切换动画",
"basic_source_title": "选择音乐来源",
"basic_source_test": "测试接口(几乎软件的所有功能都可用)",
"basic_source_temp": "临时接口(软件的某些功能不可用,建议测试接口不可用再使用本接口)",
"basic_source": "音乐来源",
"basic_sourcename_title": "选择音源名字类型",
"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": "较小",
"basic_window_size_small": "小",
"basic_window_size_medium": "中",
"basic_window_size_big": "大",
"basic_window_size_larger": "较大",
"basic_window_size_oversized": "超大",
"basic_window_size_huge": "巨大",
"basic_to_tray": "关闭软件时不退出软件将其最小化到系统托盘",
"basic_lang_title": "软件显示的语言",
"basic_lang": "语言",
"basic_control_btn_position": "控制按钮位置",
"basic_control_btn_position_left": "左边",
"basic_control_btn_position_right": "右边",
"play": "播放设置",
"play_save_play_time": "记住播放进度",
"play_lyric_transition": "显示歌词翻译",
"play_lyric_lxlrc": "使用卡拉OK式歌词播放如果支持",
"play_quality": "优先播放320K品质的歌曲如果支持",
"play_task_bar": "在任务栏上显示当前歌曲播放进度",
"play_mediaDevice_title": "选择声音输出的媒体设备",
"play_mediaDevice": "音频输出",
"play_mediaDevice_remove_stop_play": "当前的声音输出设备被改变时暂停播放歌曲",
"basic_lang": "语言",
"basic_lang_title": "软件显示的语言",
"basic_show_animation": "显示切换动画",
"basic_source": "音乐来源",
"basic_source_status_failed": "初始化失败",
"basic_source_status_initing": "初始化中",
"basic_source_status_success": "初始化成功",
"basic_source_temp": "临时接口(软件的某些功能不可用,建议测试接口不可用再使用本接口)",
"basic_source_test": "测试接口(几乎软件的所有功能都可用)",
"basic_source_title": "选择音乐来源",
"basic_source_user_api_btn": "自定义源管理",
"basic_sourcename": "音源名字",
"basic_sourcename_alias": "别名",
"basic_sourcename_real": "原名",
"basic_sourcename_title": "选择音源名字类型",
"basic_theme": "主题颜色",
"basic_to_tray": "关闭软件时不退出软件将其最小化到系统托盘",
"basic_window_size": "窗口尺寸",
"basic_window_size_big": "大",
"basic_window_size_huge": "巨大",
"basic_window_size_larger": "较大",
"basic_window_size_medium": "中",
"basic_window_size_oversized": "超大",
"basic_window_size_small": "小",
"basic_window_size_smaller": "较小",
"basic_window_size_title": "设置软件窗口尺寸",
"click_copy": "点击复制",
"click_open": "点击打开",
"desktop_lyric": "桌面歌词设置",
"desktop_lyric_always_on_top": "使歌词总是在其他窗口之上",
"desktop_lyric_enable": "显示歌词",
"desktop_lyric_lock": "锁定歌词",
"desktop_lyric_always_on_top": "使歌词总是在其他窗口之上",
"desktop_lyric_lock_screen": "不允许歌词窗口拖出主屏幕之外",
"search": "搜索设置",
"search_hot": "显示热门搜索",
"search_history": "显示历史搜索记录",
"search_focus_search_box": "启动时自动聚焦搜索框",
"list": "列表设置",
"list_source": "显示歌曲源(仅对我的音乐分类有效)",
"list_scroll": "记住播放列表滚动条位置(仅对我的音乐分类有效)",
"download": "下载设置",
"download_enable": "是否启用下载功能",
"download_path_title": "下载歌曲保存的路径",
"download_path": "下载路径",
"download_path_label": "当前下载路径:",
"download_path_open_label": "点击打开当前路径",
"download_path_change_btn": "更改",
"download_name_title": "下载歌曲时的命名方式",
"download_name": "文件命名方式",
"download_data_embed": "是否将以下内容嵌入到音频文件中",
"download_embed_pic": "封面嵌入",
"download_embed_lyric": "歌词嵌入",
"download_lyric_title": "是否同时下载歌词文件",
"download_embed_pic": "封面嵌入",
"download_enable": "是否启用下载功能",
"download_lyric": "歌词下载",
"download_lyric_title": "是否同时下载歌词文件",
"download_name": "文件命名方式",
"download_name1": "歌名 - 歌手",
"download_name2": "歌手 - 歌名",
"download_name3": "歌名",
"download_use_other_source": "自动换源下载当无法从歌曲的原始源下载时尝试切换到其他源下载此功能不100%保证换源后的歌曲版本与原版一致)",
"download_name_title": "下载歌曲时的命名方式",
"download_path": "下载路径",
"download_path_change_btn": "更改",
"download_path_label": "当前下载路径:",
"download_path_open_label": "点击打开当前路径",
"download_path_title": "下载歌曲保存的路径",
"download_select_save_path": "选择歌曲保存路径",
"download_use_other_source": "自动换源下载当无法从歌曲的原始源下载时尝试切换到其他源下载此功能不100%保证换源后的歌曲版本与原版一致)",
"hot_key": "快捷键设置",
"hot_key_local_title": "软件内快捷键",
"hot_key_global_title": "全局快捷键",
"hot_key_tip_input": "请输入新的按键",
"hot_key_unset_input": "未设置",
"hot_key_common_toggle_close": "退出程序",
"hot_key_common_toggle_min": "最小化/还原程序",
"hot_key_common_min": "最小化程序",
"hot_key_common_toggle_hide": "显示/隐藏程序",
"hot_key_common_focus_search_input": "聚焦搜索框",
"hot_key_player_toggle_play": "播放/暂停控制",
"hot_key_player_prev": "上一首歌曲",
"hot_key_common_min": "最小化程序",
"hot_key_common_toggle_close": "退出程序",
"hot_key_common_toggle_hide": "显示/隐藏程序",
"hot_key_common_toggle_min": "最小化/还原程序",
"hot_key_desktop_lyric_toggle_always_top": "桌面歌词置顶切换",
"hot_key_desktop_lyric_toggle_lock": "桌面歌词锁定切换",
"hot_key_desktop_lyric_toggle_visible": "开/关桌面歌词",
"hot_key_global_title": "全局快捷键",
"hot_key_local_title": "软件内快捷键",
"hot_key_player_next": "下一首歌曲",
"hot_key_player_volume_up": "增加音量",
"hot_key_player_prev": "上一首歌曲",
"hot_key_player_toggle_play": "播放/暂停控制",
"hot_key_player_volume_down": "减少音量",
"hot_key_player_volume_mute": "静音切换",
"hot_key_desktop_lyric_toggle_visible": "开/关桌面歌词",
"hot_key_desktop_lyric_toggle_lock": "桌面歌词锁定切换",
"hot_key_desktop_lyric_toggle_always_top": "桌面歌词置顶切换",
"hot_key_player_volume_up": "增加音量",
"hot_key_tip_input": "请输入新的按键",
"hot_key_unset_input": "未设置",
"is_enable": "是否启用",
"is_show": "是否显示",
"list": "列表设置",
"list_add_music_location_type": "添加歌曲到列表时的位置",
"list_add_music_location_type_bottom": "底部",
"list_add_music_location_type_top": "顶部",
"list_scroll": "记住播放列表滚动条位置(仅对我的音乐分类有效)",
"list_source": "显示歌曲源(仅对我的音乐分类有效)",
"network": "网络设置",
"network_proxy_title": "HTTP代理设置乱设置软件将无法联网",
"network_proxy_host": "主机",
"network_proxy_port": "端口",
"network_proxy_username": "用户名",
"network_proxy_password": "密码",
"network_proxy_port": "端口",
"network_proxy_title": "HTTP代理设置乱设置软件将无法联网",
"network_proxy_username": "用户名",
"odc": "强迫症设置",
"odc_clear_search_input": "离开搜索界面时清空搜索框",
"odc_clear_search_list": "离开搜索界面时清空搜索列表",
"backup": "备份与恢复",
"backup_part": "部分数据(列表数据包括试听列表、收藏列表、用户自定义列表,设置数据不包括快捷键设置)",
"backup_part_import_list": "导入列表",
"backup_part_export_list": "导出列表",
"backup_part_import_setting": "导入设置",
"backup_part_export_setting": "导出设置",
"backup_all": "所有数据(列表数据与设置数据)",
"backup_all_import": "导入",
"backup_all_export": "导出",
"backup_all_import_desc": "选择备份文件",
"backup_all_export_desc": "选择备份保存位置",
"backup_part_import_setting_desc": "选择配置文件",
"backup_part_export_setting_desc": "选择设置保存位置",
"backup_part_import_list_desc": "选择列表文件",
"backup_part_export_list_desc": "选择歌单保存位置",
"other": "其他",
"other_play_list_cache": "列表缓存管理(我的列表中已缓存的歌曲链接、播放代替源,清理后播放、下载歌曲时需要重新获取,没有歌曲播放相关的问题不要清理)",
"other_play_list_cache_clear_btn": "清理列表缓存信息",
"other_resource_cache": "资源缓存管理(图片、音频等缓存,清理后图片等资源将需要重新下载,不建议清理,软件会根据磁盘空间动态管理缓存大小)",
"other_resource_cache_clear_btn": "清理资源缓存",
"other_resource_cache_label": "软件已使用缓存大小:",
"other_tray_theme": "托盘图标样式",
"other_tray_theme_native": "纯色",
"other_tray_theme_origin": "原色",
"other_resource_cache": "资源缓存管理(图片、音频等缓存,清理后图片等资源将需要重新下载,不建议清理,软件会根据磁盘空间动态管理缓存大小)",
"other_resource_cache_label": "软件已使用缓存大小:",
"other_resource_cache_clear_btn": "清理资源缓存",
"other_play_list_cache": "列表缓存管理(我的列表中已缓存的歌曲链接、播放代替源,清理后播放、下载歌曲时需要重新获取,没有歌曲播放相关的问题不要清理)",
"other_play_list_cache_clear_btn": "清理列表缓存信息",
"play": "播放设置",
"play_lyric_lxlrc": "使用卡拉OK式歌词播放如果支持",
"play_lyric_transition": "显示歌词翻译",
"play_mediaDevice": "音频输出",
"play_mediaDevice_remove_stop_play": "当前的声音输出设备被改变时暂停播放歌曲",
"play_mediaDevice_title": "选择声音输出的媒体设备",
"play_quality": "优先播放320K品质的歌曲如果支持",
"play_save_play_time": "记住播放进度",
"play_task_bar": "在任务栏上显示当前歌曲播放进度",
"search": "搜索设置",
"search_focus_search_box": "启动时自动聚焦搜索框",
"search_history": "显示历史搜索记录",
"search_hot": "显示热门搜索",
"sync": "数据同步 [此为测试功能,首次使用前建议先备份一次歌单]",
"sync_address": "同步服务地址:{address}",
"sync_auth_code": "连接码:{code}",
"sync_device": "已连接的设备:{devices}",
"sync_enable": "启用同步功能(由于数据是明文传输,请在受信任的网络下使用)",
"sync_port": "同步端口设置",
"sync_port_tip": "请输入同步服务端口号",
"sync_refresh_code": "刷新连接码",
"update": "软件更新",
"update_latest_label": "最新版本:",
"update_unknown": "未知",
"update_checking": "检查更新中...",
"update_current_label": "当前版本:",
"update_downloading": "发现新版本并在努力下载中,请稍后...⏳",
"update_progress": "下载进度:",
"update_latest": "软件已是最新,尽情地体验吧~🥂",
"update_open_version_modal_btn": "打开更新窗口 🚀",
"update_checking": "检查更新中...",
"update_init": "处理更新中...",
"about": "关于洛雪音乐",
"is_enable": "是否启用",
"is_show": "是否显示",
"click_open": "点击打开",
"click_copy": "点击复制"
"update_latest": "软件已是最新,尽情地体验吧~🥂",
"update_latest_label": "最新版本:",
"update_open_version_modal_btn": "打开更新窗口 🚀",
"update_progress": "下载进度:",
"update_unknown": "未知"
}

View File

@ -0,0 +1,19 @@
{
"merge_btn_local_remote": "本機列表 合併 遠程列表",
"merge_btn_remote_local": "遠程列表 合併 本機列表",
"merge_label": "合併",
"merge_tip": "合併:",
"merge_tip_desc": "將兩邊的列表合併到一起,相同的歌曲將被去掉(去掉的是被合併者的歌曲),不同的歌曲將被添加。",
"other_label": "其他",
"other_tip": "其他:",
"other_tip_desc": "“僅使用實時同步功能”將不修改雙方的列表,僅實時同步操作;“取消同步”將直接斷開雙方的連接。",
"overwrite": "完全覆蓋",
"overwrite_btn_cancel": "取消同步",
"overwrite_btn_local_remote": "本機列表 覆蓋 遠程列表",
"overwrite_btn_none": "僅使用實時同步功能",
"overwrite_btn_remote_local": "遠程列表 覆蓋 本機列表",
"overwrite_label": "覆蓋",
"overwrite_tip": "覆蓋:",
"overwrite_tip_desc": "被覆蓋者與覆蓋者列表ID相同的列表將被刪除後替換成覆蓋者的列表列表ID不同的列表將被合併到一起若勾選完全覆蓋則被覆蓋者的所有列表將被移除然後替換成覆蓋者的列表。",
"title": "選擇與 {name} 的列表同步方式"
}

View File

@ -1,154 +1,151 @@
{
"about": "關於洛雪音樂",
"backup": "備份與恢復",
"backup_all": "所有數據(列表數據與設置數據)",
"backup_all_export": "導出",
"backup_all_export_desc": "選擇備份保存位置",
"backup_all_import": "導入",
"backup_all_import_desc": "選擇備份文件",
"backup_part": "部分數據(列表數據包括試聽列表、收藏列表、用戶自定義列表,設置數據不包括快捷鍵設置)",
"backup_part_export_list": "導出列表",
"backup_part_export_list_desc": "選擇歌單保存位置",
"backup_part_export_setting": "導出設置",
"backup_part_export_setting_desc": "選擇設置保存位置",
"backup_part_import_list": "導入列表",
"backup_part_import_list_desc": "選擇列表文件",
"backup_part_import_setting": "導入設置",
"backup_part_import_setting_desc": "選擇配置文件",
"basic": "基本設置",
"basic_theme": "主題顏色",
"basic_animation": "彈出層隨機動畫",
"basic_show_animation": "顯示切換動畫",
"basic_source_title": "選擇音樂來源",
"basic_source_test": "測試接口(幾乎軟件的所有功能都可用)",
"basic_source_temp": "臨時接口(軟件的某些功能不可用,建議測試接口不可用再使用本接口)",
"basic_source": "音樂來源",
"basic_sourcename_title": "選擇音源名字類型",
"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": "較小",
"basic_window_size_small": "小",
"basic_window_size_medium": "中",
"basic_window_size_big": "大",
"basic_window_size_larger": "較大",
"basic_window_size_oversized": "超大",
"basic_window_size_huge": "巨大",
"basic_to_tray": "關閉軟件時不退出軟件將其最小化到系統托盤",
"basic_lang_title": "軟件顯示的語言",
"basic_lang": "語言",
"basic_control_btn_position": "控制按鈕位置",
"basic_control_btn_position_left": "左邊",
"basic_control_btn_position_right": "右邊",
"play": "播放設置",
"play_save_play_time": "記住播放進度",
"play_lyric_transition": "顯示歌詞翻譯",
"play_lyric_lxlrc": "使用卡拉OK式歌詞播放如果支持",
"play_quality": "優先播放320K品質的歌曲如果支持",
"play_task_bar": "在任務欄上顯示當前歌曲播放進度",
"play_mediaDevice_title": "選擇聲音輸出的媒體設備",
"play_mediaDevice": "音頻輸出",
"play_mediaDevice_remove_stop_play": "當前的聲音輸出設備被改變時暫停播放歌曲",
"basic_lang": "語言",
"basic_lang_title": "軟件顯示的語言",
"basic_show_animation": "顯示切換動畫",
"basic_source": "音樂來源",
"basic_source_status_failed": "初始化失敗",
"basic_source_status_initing": "初始化中",
"basic_source_status_success": "初始化成功",
"basic_source_temp": "臨時接口(軟件的某些功能不可用,建議測試接口不可用再使用本接口)",
"basic_source_test": "測試接口(幾乎軟件的所有功能都可用)",
"basic_source_title": "選擇音樂來源",
"basic_source_user_api_btn": "自定義源管理",
"basic_sourcename": "音源名字",
"basic_sourcename_alias": "別名",
"basic_sourcename_real": "原名",
"basic_sourcename_title": "選擇音源名字類型",
"basic_theme": "主題顏色",
"basic_to_tray": "關閉軟件時不退出軟件將其最小化到系統托盤",
"basic_window_size": "窗口尺寸",
"basic_window_size_big": "大",
"basic_window_size_huge": "巨大",
"basic_window_size_larger": "較大",
"basic_window_size_medium": "中",
"basic_window_size_oversized": "超大",
"basic_window_size_small": "小",
"basic_window_size_smaller": "較小",
"basic_window_size_title": "設置軟件窗口尺寸",
"click_copy": "點擊複製",
"click_open": "點擊打開",
"desktop_lyric": "桌面歌詞設置",
"desktop_lyric_always_on_top": "使歌詞總是在其他窗口之上",
"desktop_lyric_enable": "顯示歌詞",
"desktop_lyric_lock": "鎖定歌詞",
"desktop_lyric_always_on_top": "使歌詞總是在其他窗口之上",
"desktop_lyric_lock_screen": "不允許歌詞窗口拖出主屏幕之外",
"search": "搜索設置",
"search_hot": "顯示熱門搜索",
"search_history": "顯示歷史搜索記錄",
"search_focus_search_box": "啟動時自動聚焦搜索框",
"list": "列表設置",
"list_source": "顯示歌曲源(僅對我的音樂分類有效)",
"list_scroll": "記住播放列表滾動條位置(僅對我的音樂分類有效)",
"download": "下載設置",
"download_enable": "是否啟用下載功能",
"download_path_title": "下載歌曲保存的路徑",
"download_path": "下載路徑",
"download_path_label": "當前下載路徑:",
"download_path_open_label": "點擊打開當前路徑",
"download_path_change_btn": "更改",
"download_name_title": "下載歌曲時的命名方式",
"download_name": "文件命名方式",
"download_data_embed": "是否將以下內容嵌入到音頻文件中",
"download_embed_pic": "封面嵌入",
"download_embed_lyric": "歌詞嵌入",
"download_lyric_title": "是否同時下載歌詞文件",
"download_embed_pic": "封面嵌入",
"download_enable": "是否啟用下載功能",
"download_lyric": "歌詞下載",
"download_lyric_title": "是否同時下載歌詞文件",
"download_name": "文件命名方式",
"download_name1": "歌名 - 歌手",
"download_name2": "歌手 - 歌名",
"download_name3": "歌名",
"download_use_other_source": "自動換源下載當無法從歌曲的原始源下載時嘗試切換到其他源下載此功能不100%保證換源後的歌曲版本與原版一致)",
"download_name_title": "下載歌曲時的命名方式",
"download_path": "下載路徑",
"download_path_change_btn": "更改",
"download_path_label": "當前下載路徑:",
"download_path_open_label": "點擊打開當前路徑",
"download_path_title": "下載歌曲保存的路徑",
"download_select_save_path": "選擇歌曲保存路徑",
"download_use_other_source": "自動換源下載當無法從歌曲的原始源下載時嘗試切換到其他源下載此功能不100%保證換源後的歌曲版本與原版一致)",
"hot_key": "快捷鍵設置",
"hot_key_local_title": "軟件內快捷鍵",
"hot_key_global_title": "全局快捷鍵",
"hot_key_tip_input": "請輸入新的按鍵",
"hot_key_unset_input": "未設置",
"hot_key_common_toggle_close": "退出程序",
"hot_key_common_toggle_min": "最小化/還原程序",
"hot_key_common_min": "最小化程序",
"hot_key_common_toggle_hide": "顯示/隱藏程序",
"hot_key_common_focus_search_input": "聚焦搜索框",
"hot_key_player_toggle_play": "播放/暫停控制​​",
"hot_key_player_prev": "上一首歌曲",
"hot_key_common_min": "最小化程序",
"hot_key_common_toggle_close": "退出程序",
"hot_key_common_toggle_hide": "顯示/隱藏程序",
"hot_key_common_toggle_min": "最小化/還原程序",
"hot_key_desktop_lyric_toggle_always_top": "桌面歌詞置頂切換",
"hot_key_desktop_lyric_toggle_lock": "桌面歌詞鎖定切換",
"hot_key_desktop_lyric_toggle_visible": "開/關桌面歌詞",
"hot_key_global_title": "全局快捷鍵",
"hot_key_local_title": "軟件內快捷鍵",
"hot_key_player_next": "下一首歌曲",
"hot_key_player_volume_up": "增加音量",
"hot_key_player_prev": "上一首歌曲",
"hot_key_player_toggle_play": "播放/暫停控制​​",
"hot_key_player_volume_down": "減少音量",
"hot_key_player_volume_mute": "靜音切換",
"hot_key_desktop_lyric_toggle_visible": "開/關桌面歌詞",
"hot_key_desktop_lyric_toggle_lock": "桌面歌詞鎖定切換",
"hot_key_desktop_lyric_toggle_always_top": "桌面歌詞置頂切換",
"hot_key_player_volume_up": "增加音量",
"hot_key_tip_input": "請輸入新的按鍵",
"hot_key_unset_input": "未設置",
"is_enable": "是否啟用",
"is_show": "是否顯示",
"list": "列表設置",
"list_add_music_location_type": "添加歌曲到列表時的位置",
"list_add_music_location_type_bottom": "底部",
"list_add_music_location_type_top": "頂部",
"list_scroll": "記住播放列表滾動條位置(僅對我的音樂分類有效)",
"list_source": "顯示歌曲源(僅對我的音樂分類有效)",
"network": "網絡設置",
"network_proxy_title": "HTTP代理設置亂設置軟件將無法聯網",
"network_proxy_host": "主機",
"network_proxy_port": "端口",
"network_proxy_username": "用戶名",
"network_proxy_password": "密碼",
"network_proxy_port": "端口",
"network_proxy_title": "HTTP代理設置亂設置軟件將無法聯網",
"network_proxy_username": "用戶名",
"odc": "強迫症設置",
"odc_clear_search_input": "離開搜索界面時清空搜索框",
"odc_clear_search_list": "離開搜索界面時清空搜索列表",
"backup": "備份與恢復",
"backup_part": "部分數據(列表數據包括試聽列表、收藏列表、用戶自定義列表,設置數據不包括快捷鍵設置)",
"backup_part_import_list": "導入列表",
"backup_part_export_list": "導出列表",
"backup_part_import_setting": "導入設置",
"backup_part_export_setting": "導出設置",
"backup_all": "所有數據(列表數據與設置數據)",
"backup_all_import": "導入",
"backup_all_export": "導出",
"backup_all_import_desc": "選擇備份文件",
"backup_all_export_desc": "選擇備份保存位置",
"backup_part_import_setting_desc": "選擇配置文件",
"backup_part_export_setting_desc": "選擇設置保存位置",
"backup_part_import_list_desc": "選擇列表文件",
"backup_part_export_list_desc": "選擇歌單保存位置",
"other": "其他",
"other_play_list_cache": "列表緩存管理(我的列表中已緩存的歌曲鏈接、播放代替源,清理後播放、下載歌曲時需要重新獲取,沒有歌曲播放相關的問題不要清理)",
"other_play_list_cache_clear_btn": "清理列表緩存信息",
"other_resource_cache": "資源緩存管理(圖片、音頻等緩存,清理後圖片等資源將需要重新下載,不建議清理,軟件會根據磁盤空間動態管理緩存大小)",
"other_resource_cache_clear_btn": "清理資源緩存",
"other_resource_cache_label": "軟件已使用緩存大小:",
"other_tray_theme": "托盤圖標樣式",
"other_tray_theme_native": "純色",
"other_tray_theme_origin": "原色",
"other_resource_cache": "資源緩存管理(圖片、音頻等緩存,清理後圖片等資源將需要重新下載,不建議清理,軟件會根據磁盤空間動態管理緩存大小)",
"other_resource_cache_label": "軟件已使用緩存大小:",
"other_resource_cache_clear_btn": "清理資源緩存",
"other_play_list_cache": "列表緩存管理(我的列表中已緩存的歌曲鏈接、播放代替源,清理後播放、下載歌曲時需要重新獲取,沒有歌曲播放相關的問題不要清理)",
"other_play_list_cache_clear_btn": "清理列表緩存信息",
"play": "播放設置",
"play_lyric_lxlrc": "使用卡拉OK式歌詞播放如果支持",
"play_lyric_transition": "顯示歌詞翻譯",
"play_mediaDevice": "音頻輸出",
"play_mediaDevice_remove_stop_play": "當前的聲音輸出設備被改變時暫停播放歌曲",
"play_mediaDevice_title": "選擇聲音輸出的媒體設備",
"play_quality": "優先播放320K品質的歌曲如果支持",
"play_save_play_time": "記住播放進度",
"play_task_bar": "在任務欄上顯示當前歌曲播放進度",
"search": "搜索設置",
"search_focus_search_box": "啟動時自動聚焦搜索框",
"search_history": "顯示歷史搜索記錄",
"search_hot": "顯示熱門搜索",
"sync": "數據同步 [此為測試功能,首次使用前建議先備份一次歌單]",
"sync_address": "同步服務地址:{address}",
"sync_auth_code": "連接碼:{code}",
"sync_device": "已連接的設備:{devices}",
"sync_enable": "啟用同步功能(由於數據是明文傳輸,請在受信任的網絡下使用)",
"sync_port": "同步端口設置",
"sync_port_tip": "請輸入同步服務端口號",
"sync_refresh_code": "刷新連接碼",
"update": "軟件更新",
"update_latest_label": "最新版本:",
"update_unknown": "未知",
"update_checking": "檢查更新中...",
"update_current_label": "當前版本:",
"update_downloading": "發現新版本並在努力下載中,請稍後...⏳",
"update_progress": "下載進度:",
"update_latest": "軟件已是最新,盡情地體驗吧~🥂",
"update_open_version_modal_btn": "打開更新窗口 🚀",
"update_checking": "檢查更新中...",
"update_init": "處理更新中...",
"about": "關於洛雪音樂",
"is_enable": "是否啟用",
"is_show": "是否顯示",
"click_open": "點擊打開",
"click_copy": "點擊複製"
"update_latest": "軟件已是最新,盡情地體驗吧~🥂",
"update_latest_label": "最新版本:",
"update_open_version_modal_btn": "打開更新窗口 🚀",
"update_progress": "下載進度:",
"update_unknown": "未知"
}

View File

@ -13,9 +13,10 @@ import {
assertApiSupport,
} from '../../utils'
window.downloadList = []
// state
const state = {
list: [],
list: window.downloadList,
waitingList: [],
downloadStatus: {
RUN: 'run',
@ -191,13 +192,13 @@ const getMusicUrl = async function(downloadInfo, isUseOtherSource, isRefresh) {
return cachedUrl && !isRefresh
? cachedUrl
: (
isUseOtherSource
? handleGetMusicUrl.call(this, downloadInfo.musicInfo, downloadInfo.type)
: music[downloadInfo.musicInfo.source].getMusicUrl(downloadInfo.musicInfo, downloadInfo.type).promise
).then(({ url }) => {
setMusicUrl(downloadInfo.musicInfo, downloadInfo.type, url)
return url
})
isUseOtherSource
? handleGetMusicUrl.call(this, downloadInfo.musicInfo, downloadInfo.type)
: music[downloadInfo.musicInfo.source].getMusicUrl(downloadInfo.musicInfo, downloadInfo.type).promise
).then(({ url }) => {
setMusicUrl(downloadInfo.musicInfo, downloadInfo.type, url)
return url
})
}
const getPic = function(musicInfo, retryedSource = [], originMusic) {
// console.log(musicInfo.source)
@ -263,29 +264,29 @@ const saveMeta = function(downloadInfo, filePath, isUseOtherSource, isEmbedPic,
? downloadInfo.musicInfo.img
? Promise.resolve(downloadInfo.musicInfo.img)
: (
isUseOtherSource
? getPic.call(this, downloadInfo.musicInfo)
: music[downloadInfo.musicInfo.source].getPic(downloadInfo.musicInfo).promise
).catch(err => {
console.log(err)
return null
})
isUseOtherSource
? getPic.call(this, downloadInfo.musicInfo)
: music[downloadInfo.musicInfo.source].getPic(downloadInfo.musicInfo).promise
).catch(err => {
console.log(err)
return null
})
: Promise.resolve(),
isEmbedLyric
? getLyricFromStorage(downloadInfo.musicInfo).then(lrcInfo => {
return lrcInfo.lyric
? Promise.resolve({ lyric: lrcInfo.lyric, tlyric: lrcInfo.tlyric || '' })
: (
isUseOtherSource
? getLyric.call(this, downloadInfo.musicInfo)
: music[downloadInfo.musicInfo.source].getLyric(downloadInfo.musicInfo).promise
).then(({ lyric, tlyric, lxlyric }) => {
setLyric(downloadInfo.musicInfo, { lyric, tlyric, lxlyric })
return { lyric, tlyric, lxlyric }
}).catch(err => {
console.log(err)
return null
})
isUseOtherSource
? getLyric.call(this, downloadInfo.musicInfo)
: music[downloadInfo.musicInfo.source].getLyric(downloadInfo.musicInfo).promise
).then(({ lyric, tlyric, lxlyric }) => {
setLyric(downloadInfo.musicInfo, { lyric, tlyric, lxlyric })
return { lyric, tlyric, lxlyric }
}).catch(err => {
console.log(err)
return null
})
})
: Promise.resolve(),
]
@ -369,6 +370,7 @@ const actions = {
status: state.downloadStatus.WAITING,
statusText: '待下载',
url: null,
// songmid: musicInfo.songmid,
fileName: `${rootState.setting.download.fileName
.replace('歌名', musicInfo.name)
.replace('歌手', musicInfo.singer)}.${ext}`.replace(filterFileName, ''),
@ -667,7 +669,7 @@ const mutations = {
downloadInfo.order = order
},
updateDownloadList(state, list) {
state.list = list
state.list = window.downloadList = list
},
updateUrl(state, { downloadInfo, url }) {
downloadInfo.url = url

View File

@ -1,10 +1,14 @@
import musicSdk from '../../utils/music'
import { clearLyric, clearMusicUrl } from '../../utils'
import { sync as eventSyncName } from '@renderer/event/names'
let allList = {}
window.allList = allList
const allListInit = (defaultList, loveList, userList) => {
for (const id of Object.keys(allList)) {
delete allList[id]
}
allList[defaultList.id] = defaultList
allList[loveList.id] = loveList
for (const list of userList) allList[list.id] = list
@ -67,8 +71,28 @@ const mutations = {
if (userList != null) state.userList = userList
allListInit(state.defaultList, state.loveList, state.userList)
state.isInitedList = true
// if (!isSync) {
// window.eventHub.$emit(eventSyncName.send_action_list, {
// action: 'init_list',
// data: { defaultList, loveList, userList },
// })
// }
},
setList(state, { id, list, name, location, source, sourceListId }) {
setSyncListData(state, { defaultList, loveList, userList }) {
state.defaultList.list.splice(0, state.defaultList.list.length, ...defaultList.list)
state.loveList.list.splice(0, state.loveList.list.length, ...loveList.list)
state.userList = userList
allListInit(state.defaultList, state.loveList, state.userList)
},
setList(state, { id, list, name, location, source, sourceListId, isSync }) {
if (!isSync) {
window.eventHub.$emit(eventSyncName.send_action_list, {
action: 'set_list',
data: { id, list, name, location, source, sourceListId },
})
}
const targetList = allList[id]
if (targetList) {
if (name && targetList.name === name) {
@ -90,46 +114,127 @@ const mutations = {
state.userList.push(newList)
allListUpdate(newList)
},
listAdd(state, { id, musicInfo }) {
listAdd(state, { id, musicInfo, addMusicLocationType, isSync }) {
if (!addMusicLocationType) addMusicLocationType = this.state.setting.list.addMusicLocationType
if (!isSync) {
window.eventHub.$emit(eventSyncName.send_action_list, {
action: 'list_add',
data: { id, musicInfo, addMusicLocationType },
})
}
const targetList = allList[id]
if (!targetList) return
if (targetList.list.some(s => s.songmid === musicInfo.songmid)) return
targetList.list.push(musicInfo)
switch (addMusicLocationType) {
case 'top':
targetList.list.unshift(musicInfo)
break
case 'bottom':
default:
targetList.list.push(musicInfo)
break
}
},
listMove(state, { fromId, musicInfo, toId }) {
listMove(state, { fromId, musicInfo, toId, isSync }) {
if (!isSync) {
window.eventHub.$emit(eventSyncName.send_action_list, {
action: 'list_move',
data: { fromId, musicInfo, toId },
})
}
const fromList = allList[fromId]
const toList = allList[toId]
if (!fromList || !toList) return
fromList.list.splice(fromList.list.indexOf(musicInfo), 1)
fromList.list.splice(fromList.list.findIndex(s => s.songmid === musicInfo.songmid), 1)
let index = toList.list.findIndex(s => s.songmid === musicInfo.songmid)
if (index < 0) toList.list.push(musicInfo)
if (index < 0) {
switch (this.state.setting.list.addMusicLocationType) {
case 'top':
toList.list.unshift(musicInfo)
break
case 'bottom':
default:
toList.list.push(musicInfo)
break
}
}
},
listAddMultiple(state, { id, list }) {
listAddMultiple(state, { id, list, addMusicLocationType, isSync }) {
if (!addMusicLocationType) addMusicLocationType = this.state.setting.list.addMusicLocationType
if (!isSync) {
window.eventHub.$emit(eventSyncName.send_action_list, {
action: 'list_add_multiple',
data: { id, list, addMusicLocationType },
})
}
let targetList = allList[id]
if (!targetList) return
let newList = [...targetList.list, ...list]
let map = {}
let ids = []
for (const item of newList) {
if (map[item.songmid]) continue
ids.push(item.songmid)
map[item.songmid] = item
let newList
const map = {}
const ids = []
switch (addMusicLocationType) {
case 'top':
newList = [...list, ...targetList.list]
for (let i = newList.length - 1; i > -1; i--) {
const item = newList[i]
if (map[item.songmid]) continue
ids.unshift(item.songmid)
map[item.songmid] = item
}
break
case 'bottom':
default:
newList = [...targetList.list, ...list]
for (const item of newList) {
if (map[item.songmid]) continue
ids.push(item.songmid)
map[item.songmid] = item
}
break
}
targetList.list.splice(0, targetList.list.length, ...ids.map(id => map[id]))
},
// { fromId, toId, list }
listMoveMultiple(state, { fromId, toId, list }) {
listMoveMultiple(state, { fromId, toId, list, isSync }) {
if (!isSync) {
window.eventHub.$emit(eventSyncName.send_action_list, {
action: 'list_move_multiple',
data: { fromId, toId, list },
})
}
// console.log(state.commit)
this.commit('list/listRemoveMultiple', { id: fromId, list })
this.commit('list/listAddMultiple', { id: toId, list })
this.commit('list/listRemoveMultiple', { listId: fromId, ids: list.map(s => s.songmid), isSync: true })
this.commit('list/listAddMultiple', { id: toId, list, isSync: true })
},
listRemove(state, { id, index }) {
let targetList = allList[id]
listRemove(state, { listId, id, isSync }) {
if (!isSync) {
window.eventHub.$emit(eventSyncName.send_action_list, {
action: 'list_remove',
data: { listId, id },
})
}
let targetList = allList[listId]
if (!targetList) return
const index = targetList.list.findIndex(item => item.songmid == id)
if (index < 0) return
targetList.list.splice(index, 1)
},
listRemoveMultiple(state, { id, list }) {
let targetList = allList[id]
listRemoveMultiple(state, { listId, ids: musicIds, isSync }) {
if (!isSync) {
window.eventHub.$emit(eventSyncName.send_action_list, {
action: 'list_remove_multiple',
data: { listId, ids: musicIds },
})
}
let targetList = allList[listId]
if (!targetList) return
let map = {}
let ids = []
@ -137,25 +242,50 @@ const mutations = {
ids.push(item.songmid)
map[item.songmid] = item
}
for (const item of list) {
if (map[item.songmid]) delete map[item.songmid]
for (const songmid of musicIds) {
if (map[songmid]) delete map[songmid]
}
let newList = []
for (const id of ids) if (map[id]) newList.push(map[id])
targetList.list.splice(0, targetList.list.length, ...newList)
},
listClear(state, id) {
listClear(state, { id, isSync }) {
if (!isSync) {
window.eventHub.$emit(eventSyncName.send_action_list, {
action: 'list_clear',
data: { id },
})
}
let targetList = allList[id]
if (!targetList) return
targetList.list.splice(0, targetList.list.length)
},
updateMusicInfo(state, { id, index, data, musicInfo = {} }) {
let targetList = allList[id]
if (!targetList) return Object.assign(musicInfo, data)
Object.assign(targetList.list[index], data)
updateMusicInfo(state, { listId, id, data, musicInfo, isSync }) {
if (!isSync) {
window.eventHub.$emit(eventSyncName.send_action_list, {
action: 'update_music_info',
data: { listId, id, data, musicInfo },
})
}
let targetList = allList[listId]
if (!targetList) {
if (musicInfo) Object.assign(musicInfo, data)
return
}
const targetMusicInfo = targetList.list.find(item => item.songmid == id)
if (targetMusicInfo) Object.assign(targetMusicInfo, data)
},
createUserList(state, { name, id = `userlist_${Date.now()}`, list = [], source, sourceListId }) {
createUserList(state, { name, id = `userlist_${Date.now()}`, list = [], source, sourceListId, isSync }) {
if (!isSync) {
window.eventHub.$emit(eventSyncName.send_action_list, {
action: 'create_user_list',
data: { name, id, list, source, sourceListId },
})
}
let newList = state.userList.find(item => item.id === id)
if (!newList) {
newList = {
@ -169,35 +299,75 @@ const mutations = {
state.userList.push(newList)
allListUpdate(newList)
}
this.commit('list/listAddMultiple', { id, list })
this.commit('list/listAddMultiple', { id, list, isSync: true })
},
removeUserList(state, index) {
removeUserList(state, { id, isSync }) {
if (!isSync) {
window.eventHub.$emit(eventSyncName.send_action_list, {
action: 'remove_user_list',
data: { id },
})
}
const index = state.userList.findIndex(l => l.id === id)
if (index < 0) return
let list = state.userList.splice(index, 1)[0]
allListRemove(list)
},
setUserListName(state, { index, name }) {
let list = state.userList[index]
setUserListName(state, { id, name, isSync }) {
if (!isSync) {
window.eventHub.$emit(eventSyncName.send_action_list, {
action: 'set_user_list_name',
data: { id, name },
})
}
let list = allList[id]
if (!list) return
list.name = name
},
moveupUserList(state, index) {
let targetList = state.userList[index]
moveupUserList(state, { id, isSync }) {
if (!isSync) {
window.eventHub.$emit(eventSyncName.send_action_list, {
action: 'moveup_user_list',
data: { id },
})
}
const index = state.userList.findIndex(l => l.id == id)
if (index < 0) return
let targetList = allList[id]
state.userList.splice(index, 1)
state.userList.splice(index - 1, 0, targetList)
},
movedownUserList(state, index) {
let targetList = state.userList[index]
movedownUserList(state, { id, isSync }) {
if (!isSync) {
window.eventHub.$emit(eventSyncName.send_action_list, {
action: 'movedown_user_list',
data: { id },
})
}
const index = state.userList.findIndex(l => l.id == id)
if (index < 0) return
let targetList = allList[id]
state.userList.splice(index, 1)
state.userList.splice(index + 1, 0, targetList)
},
setListScroll(state, { id, location }) {
if (allList[id]) allList[id].location = location
},
sortList(state, { id, sortNum, musicInfos }) {
let targetList = allList[id]
this.commit('list/listRemoveMultiple', { id, list: musicInfos })
setMusicPosition(state, { id, position, list, isSync }) {
if (!isSync) {
window.eventHub.$emit(eventSyncName.send_action_list, {
action: 'set_music_position',
data: { id, position, list },
})
}
targetList.list.splice(sortNum - 1, 0, ...musicInfos)
let targetList = allList[id]
this.commit('list/listRemoveMultiple', { listId: id, ids: list.map(m => m.songmid), isSync: true })
targetList.list.splice(position - 1, 0, ...list)
},
clearCache() {
const lists = Object.values(allList)

View File

@ -40,7 +40,7 @@ const filterList = async({ playedList, listInfo, savePath, commit }) => {
canPlayList.push(item)
// 排除已播放音乐
let index = filteredPlayedList.indexOf(item)
let index = filteredPlayedList.findIndex(m => (m.songmid || m.musicInfo.songmid) == item.musicInfo.songmid)
if (index > -1) {
filteredPlayedList.splice(index, 1)
continue
@ -52,7 +52,7 @@ const filterList = async({ playedList, listInfo, savePath, commit }) => {
// if (!assertApiSupport(s.source)) return false
canPlayList.push(s)
let index = filteredPlayedList.indexOf(s)
let index = filteredPlayedList.findIndex(m => (m.songmid || m.musicInfo.songmid) == s.songmid)
if (index > -1) {
filteredPlayedList.splice(index, 1)
return false
@ -141,6 +141,7 @@ const getLyric = function(musicInfo, retryedSource = [], originMusic) {
})
}
let prevListPlayIndex
// getters
const getters = {
list: state => state.listInfo.list,
@ -152,17 +153,31 @@ const getters = {
const isTempPlay = !!state.playMusicInfo.isTempPlay
const isPlayList = listId === playListId
let playIndex = -1
let listPlayIndex = state.playIndex
let listPlayIndex = Math.min(state.playIndex, state.listInfo.list.length - 1)
if (listId != '__temp__') {
const currentSongmid = state.playMusicInfo.musicInfo.songmid || state.playMusicInfo.musicInfo.musicInfo.songmid
if (isPlayList) {
playIndex = state.listInfo.list.indexOf(state.playMusicInfo.musicInfo)
playIndex = state.listInfo.list.findIndex(m => (m.songmid || m.musicInfo.songmid) == currentSongmid)
if (!isTempPlay) listPlayIndex = playIndex
} else if (listId == 'download') {
playIndex = window.downloadList.findIndex(m => m.musicInfo.songmid == currentSongmid)
} else {
let list = window.allList[listId]
if (list) playIndex = list.list.indexOf(state.playMusicInfo.musicInfo)
if (list) playIndex = list.list.findIndex(m => m.songmid == currentSongmid)
}
}
if (listPlayIndex >= 0) prevListPlayIndex = listPlayIndex
// if (listPlayIndex < 0) {
// let length = state.listInfo.list.length
// if (length) {
// let index = Math.min(prevListPlayIndex, 0)
// if (index > length - 1) index = length - 1
// listPlayIndex = prevListPlayIndex = index
// }
// } else {
// prevListPlayIndex = listPlayIndex
// }
// console.log({
// listId,
// playIndex,
@ -170,7 +185,7 @@ const getters = {
// listPlayIndex,
// isPlayList,
// isTempPlay,
// musicInfo: state.playMusicInfo.musicInfo,
// // musicInfo: state.playMusicInfo.musicInfo,
// })
return {
listId,
@ -247,12 +262,21 @@ const actions = {
async playPrev({ state, rootState, commit, getters }) {
const currentListId = state.listInfo.id
const currentList = state.listInfo.list
const playInfo = getters.playInfo
if (state.playedList.length) {
let currentSongmid
if (state.playMusicInfo.isTempPlay) {
const musicInfo = currentList[playInfo.listPlayIndex]
if (musicInfo) currentSongmid = musicInfo.songmid || musicInfo.musicInfo.songmid
} else {
currentSongmid = state.playMusicInfo.musicInfo.songmid || state.playMusicInfo.musicInfo.musicInfo.songmid
}
// 从已播放列表移除播放列表已删除的歌曲
let index
for (index = state.playedList.indexOf(state.playMusicInfo) - 1; index > -1; index--) {
for (index = state.playedList.findIndex(m => (m.musicInfo.songmid || m.musicInfo.musicInfo.songmid) === currentSongmid) - 1; index > -1; index--) {
const playMusicInfo = state.playedList[index]
if (playMusicInfo.listId == currentListId && !currentList.includes(playMusicInfo.musicInfo)) {
const currentSongmid = playMusicInfo.musicInfo.songmid || playMusicInfo.musicInfo.musicInfo.songmid
if (playMusicInfo.listId == currentListId && !currentList.some(m => (m.songmid || m.musicInfo.songmid) === currentSongmid)) {
commit('removePlayedList', index)
continue
}
@ -272,9 +296,14 @@ const actions = {
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 listPlayIndex = playInfo.listPlayIndex
const currentListLength = state.listInfo.list.length - 1
if (listPlayIndex == -1 && currentListLength) {
listPlayIndex = prevListPlayIndex >= currentListLength ? 0 : prevListPlayIndex + 1
}
let currentIndex = listPlayIndex
if (currentIndex < 0) currentIndex = 0
let nextIndex = currentIndex
if (!playInfo.isTempPlay) {
switch (rootState.setting.player.togglePlayMethod) {
@ -308,12 +337,22 @@ const actions = {
}
const currentListId = state.listInfo.id
const currentList = state.listInfo.list
const playInfo = getters.playInfo
if (state.playedList.length) {
let currentSongmid
if (state.playMusicInfo.isTempPlay) {
const musicInfo = currentList[playInfo.listPlayIndex]
if (musicInfo) currentSongmid = musicInfo.songmid || musicInfo.musicInfo.songmid
} else {
currentSongmid = state.playMusicInfo.musicInfo.songmid || state.playMusicInfo.musicInfo.musicInfo.songmid
}
// 从已播放列表移除播放列表已删除的歌曲
let index
for (index = state.playedList.indexOf(state.playMusicInfo) + 1; index < state.playedList.length; index++) {
for (index = state.playedList.findIndex(m => (m.musicInfo.songmid || m.musicInfo.musicInfo.songmid) === currentSongmid) + 1; index < state.playedList.length; index++) {
const playMusicInfo = state.playedList[index]
if (playMusicInfo.listId == currentListId && !currentList.includes(playMusicInfo.musicInfo)) {
const currentSongmid = playMusicInfo.musicInfo.songmid || playMusicInfo.musicInfo.musicInfo.songmid
if (playMusicInfo.listId == currentListId && !currentList.some(m => (m.songmid || m.musicInfo.songmid) === currentSongmid)) {
commit('removePlayedList', index)
continue
}
@ -333,9 +372,14 @@ const actions = {
})
if (!filteredList.length) return commit('setPlayMusicInfo', null)
const playInfo = getters.playInfo
const currentIndex = filteredList.indexOf(currentList[playInfo.listPlayIndex])
let listPlayIndex = playInfo.listPlayIndex
const currentListLength = state.listInfo.list.length - 1
if (listPlayIndex == -1 && currentListLength) {
listPlayIndex = prevListPlayIndex > currentListLength ? currentListLength : prevListPlayIndex - 1
}
const currentIndex = listPlayIndex
let nextIndex = currentIndex
switch (rootState.setting.player.togglePlayMethod) {
case 'listLoop':
nextIndex = currentIndex === filteredList.length - 1 ? 0 : currentIndex + 1
@ -381,6 +425,7 @@ const mutations = {
})
},
setList(state, { list, index }) {
if (!(list && list.list && list.list[index])) return
state.playMusicInfo = {
musicInfo: list.list[index],
listId: list.id,
@ -432,7 +477,10 @@ const mutations = {
playIndex = -1
} else {
let listId = playMusicInfo.listId
if (listId != '__temp__' && !playMusicInfo.isTempPlay && listId === state.listInfo.id) playIndex = state.listInfo.list.indexOf(playMusicInfo.musicInfo)
if (listId != '__temp__' && !playMusicInfo.isTempPlay && listId === state.listInfo.id) {
const currentSongmid = playMusicInfo.musicInfo.songmid || playMusicInfo.musicInfo.musicInfo.songmid
playIndex = state.listInfo.list.findIndex(m => (m.songmid || m.musicInfo.songmid) == currentSongmid)
}
}
state.playMusicInfo = playMusicInfo

View File

@ -80,7 +80,7 @@ const encodeNames = {
'&apos;': "'",
'&#039;': "'",
}
export const decodeName = (str = '') => str.replace(/(?:&amp;|&lt;|&gt;|&quot;|&apos;|&#039;)/gm, s => encodeNames[s])
export const decodeName = (str = '') => str?.replace(/(?:&amp;|&lt;|&gt;|&quot;|&apos;|&#039;)/gm, s => encodeNames[s]) || ''
const easeInOutQuad = (t, b, c, d) => {
t /= d / 2

View File

@ -1,6 +1,7 @@
import { httpFetch } from '../../request'
import { requestMsg } from '../../message'
import { headers, timeout } from '../options'
import { dnsLookup } from '../utils'
const api_test = {
getMusicUrl(songInfo, type) {
@ -8,6 +9,7 @@ const api_test = {
method: 'get',
timeout,
headers,
lookup: dnsLookup,
family: 4,
})
requestObj.promise = requestObj.promise.then(({ body }) => {

View File

@ -1,6 +1,6 @@
import { httpFetch } from '../../request'
// import { formatPlayTime } from '../../index'
// import jshtmlencode from 'js-htmlencode'
const boardList = [
// { id: 'bd__601', name: '歌单榜', bangid: '601' },

View File

@ -1,5 +1,5 @@
// import '../../polyfill/array.find'
// import jshtmlencode from 'js-htmlencode'
import { httpFetch } from '../../request'
import { formatPlayTime } from '../../index'
// import { debug } from '../../utils/env'

View File

@ -1,6 +1,7 @@
import { httpFetch } from '../../request'
import { requestMsg } from '../../message'
import { headers, timeout } from '../options'
import { dnsLookup } from '../utils'
const api_test = {
getMusicUrl(songInfo, type) {
@ -8,6 +9,7 @@ const api_test = {
method: 'get',
timeout,
headers,
lookup: dnsLookup,
family: 4,
})
requestObj.promise = requestObj.promise.then(({ body }) => {

View File

@ -63,17 +63,19 @@ export default {
reply: [],
}
return item.pcontent ? {
id: item.id,
text: decodeName(item.pcontent).split('\n'),
time: null,
userName: item.puser,
avatar: null,
userId: item.puser_id,
likedCount: null,
replyNum: null,
reply: [data],
} : data
return item.pcontent
? {
id: item.id,
text: decodeName(item.pcontent).split('\n'),
time: null,
userName: item.puser,
avatar: null,
userId: item.puser_id,
likedCount: null,
replyNum: null,
reply: [data],
}
: data
})
},
}

View File

@ -1,5 +1,5 @@
// import '../../polyfill/array.find'
// import jshtmlencode from 'js-htmlencode'
import { httpFetch } from '../../request'
import { decodeName, formatPlayTime, sizeFormate } from '../../index'
// import { debug } from '../../utils/env'

View File

@ -165,7 +165,9 @@ export default {
: result.body.err_code
) !== 0)
) return this.createHttp(url, options, ++retryNum)
return result.body.data || result.body.info
if (result.body.data) return result.body.data
if (Array.isArray(result.body.info)) return result.body
return result.body.info
},
createTask(hashs) {
@ -183,9 +185,9 @@ export default {
let list = hashs
let tasks = []
while (list.length) {
tasks.push(Object.assign({ data: list.slice(0, 20) }, data))
if (list.length < 20) break
list = list.slice(20)
tasks.push(Object.assign({ data: list.slice(0, 100) }, data))
if (list.length < 100) break
list = list.slice(100)
}
let url = 'http://kmr.service.kugou.com/v2/album_audio/audio'
return tasks.map(task => this.createHttp(url, {
@ -267,6 +269,68 @@ export default {
}
},
deDuplication(datas) {
let ids = new Set()
return datas.filter(({ hash }) => {
if (ids.has(hash)) return false
ids.add(hash)
return true
})
},
async getUserListDetailByLink({ info }, link) {
let listInfo = info['0']
let total = listInfo.count
let tasks = []
let page = 0
while (total) {
const limit = total > 90 ? 90 : total
total -= limit
page += 1
tasks.push(this.createHttp(link.replace(/pagesize=\d+/, 'pagesize=' + limit).replace(/page=\d+/, 'page=' + page), {
headers: {
'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1',
Referer: link,
},
}).then(data => data.list.info))
}
let result = await Promise.all(tasks).then(([...datas]) => datas.flat())
result = await Promise.all(this.createTask(this.deDuplication(result).map(item => ({ hash: item.hash })))).then(([...datas]) => datas.flat())
// console.log(result)
return {
list: this.filterData2(result) || [],
page,
limit: this.listDetailLimit,
total: listInfo.count,
source: 'kg',
info: {
name: listInfo.name,
img: listInfo.pic && listInfo.pic.replace('{size}', 240),
// desc: body.result.info.list_desc,
author: listInfo.list_create_username,
// play_count: this.formatPlayCount(listInfo.count),
},
}
},
createGetListDetail2Task(id, total) {
let tasks = []
let page = 0
while (total) {
const limit = total > 300 ? 300 : total
total -= limit
page += 1
tasks.push(this.createHttp('https://mobiles.kugou.com/api/v5/special/song_v2?appid=1058&global_specialid=' + id + '&specialid=0&plat=0&version=8000&page=' + page + '&pagesize=' + limit + '&srcappid=2919&clientver=20000&clienttime=1586163263991&mid=1586163263991&uuid=1586163263991&dfid=-&signature=' + toMD5('NVPh5oo715z5DIWAeQlhMDsWXXQV4hwtappid=1058clienttime=1586163263991clientver=20000dfid=-global_specialid=' + id + 'mid=1586163263991page=' + page + 'pagesize=' + limit + 'plat=0specialid=0srcappid=2919uuid=1586163263991version=8000NVPh5oo715z5DIWAeQlhMDsWXXQV4hwt'), {
headers: {
mid: '1586163263991',
Referer: 'https://m3ws.kugou.com/share/index.php',
'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1',
dfid: '-',
clienttime: '1586163263991',
},
}).then(data => data.info))
}
return Promise.all(tasks).then(([...datas]) => datas.flat())
},
async getUserListDetail2(global_collection_id) {
let id = global_collection_id
if (id.length > 1000) throw new Error('get list error')
@ -279,22 +343,14 @@ export default {
clienttime: '1586163242519',
},
})
let songInfo = await this.createHttp('https://mobiles.kugou.com/api/v5/special/song_v2?appid=1058&global_specialid=' + id + '&specialid=0&plat=0&version=8000&pagesize=' + info.songcount + '&srcappid=2919&clientver=20000&clienttime=1586163263991&mid=1586163263991&uuid=1586163263991&dfid=-&signature=' + toMD5('NVPh5oo715z5DIWAeQlhMDsWXXQV4hwtappid=1058clienttime=1586163263991clientver=20000dfid=-global_specialid=' + id + 'mid=1586163263991pagesize=' + info.songcount + 'plat=0specialid=0srcappid=2919uuid=1586163263991version=8000NVPh5oo715z5DIWAeQlhMDsWXXQV4hwt'), {
headers: {
mid: '1586163263991',
Referer: 'https://m3ws.kugou.com/share/index.php',
'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1',
dfid: '-',
clienttime: '1586163263991',
},
})
let result = await Promise.all(this.createTask(songInfo.info.map(item => ({ hash: item.hash })))).then(([...datas]) => datas.flat())
const songInfo = await this.createGetListDetail2Task(id, info.songcount)
let result = await Promise.all(this.createTask(this.deDuplication(songInfo).map(item => ({ hash: item.hash })))).then(([...datas]) => datas.flat())
// console.log(info, songInfo)
return {
list: this.filterData2(result) || [],
page: 1,
limit: songInfo.total,
total: songInfo.total,
limit: this.listDetailLimit,
total: info.songcount,
source: 'kg',
info: {
name: info.specialname,
@ -333,10 +389,9 @@ export default {
// console.log(body, location)
if (statusCode > 400) return this.getUserListDetail(link, page, ++retryNum)
if (location) {
// console.log(location)
if (location.includes('global_collection_id')) return this.getUserListDetail2(location.replace(/^.*?global_collection_id=(\w+)(?:&.*$|#.*$|$)/, '$1'))
if (location.includes('chain=')) return this.getUserListDetail3(location.replace(/^.*?chain=(\w+)(?:&.*$|#.*$|$)/, '$1'), page)
// console.log('location', location)
if (location.includes('.html')) {
if (location.includes('zlist.html')) {
let link = location.replace(/^(.*)zlist\.html/, 'https://m3ws.kugou.com/zlist/list')
@ -348,27 +403,12 @@ export default {
return this.getUserListDetail(link, page, ++retryNum)
} else return this.getUserListDetail3(location.replace(/.+\/(\w+).html(?:\?.*|&.*$|#.*$|$)/, '$1'), page)
}
// console.log('location', location)
return this.getUserListDetail(link, page, ++retryNum)
}
if (typeof body == 'string') return this.getUserListDetail2(body.replace(/^[\s\S]+?"global_collection_id":"(\w+)"[\s\S]+?$/, '$1'))
if (body.errcode !== 0) return this.getUserListDetail(link, page, ++retryNum)
let listInfo = body.info['0']
let result = body.list.info.map(item => ({ hash: item.hash }))
result = await Promise.all(this.createTask(result)).then(([...datas]) => datas.flat())
return {
list: this.filterData2(result) || [],
page,
limit: this.listDetailLimit,
total: listInfo.count,
source: 'kg',
info: {
name: listInfo.name,
img: listInfo.pic && listInfo.pic.replace('{size}', 240),
// desc: body.result.info.list_desc,
author: listInfo.list_create_username,
// play_count: this.formatPlayCount(listInfo.count),
},
}
return this.getUserListDetailByLink(body, link)
},
getListDetail(id, page, tryNum = 0) { // 获取歌曲列表内的音乐

View File

@ -1,5 +1,6 @@
import { httpFetch } from '../../request'
import { headers, timeout } from '../options'
import { dnsLookup } from '../utils'
const api_temp = {
getMusicUrl(songInfo, type) {
@ -7,6 +8,7 @@ const api_temp = {
method: 'get',
headers,
timeout,
lookup: dnsLookup,
family: 4,
})
requestObj.promise = requestObj.promise.then(({ body }) => {

View File

@ -1,6 +1,7 @@
import { httpFetch } from '../../request'
import { requestMsg } from '../../message'
import { headers, timeout } from '../options'
import { dnsLookup } from '../utils'
const api_test = {
// getMusicUrl(songInfo, type) {
@ -19,6 +20,7 @@ const api_test = {
method: 'get',
timeout,
headers,
lookup: dnsLookup,
family: 4,
})
requestObj.promise = requestObj.promise.then(({ body }) => {

View File

@ -44,18 +44,20 @@ export default {
likedCount: item.like_num,
reply: [],
}
return item.reply ? {
id: item.id,
rootId: item.reply.id,
text: item.reply.msg.split('\n'),
time: item.reply.time,
timeStr: dateFormat2(new Date(item.reply.time).getTime()),
userName: decodeURIComponent(item.reply.u_name),
avatar: item.reply.u_pic,
userId: item.reply.u_id,
likedCount: item.reply.like_num,
reply: [data],
} : data
return item.reply
? {
id: item.id,
rootId: item.reply.id,
text: item.reply.msg.split('\n'),
time: item.reply.time,
timeStr: dateFormat2(new Date(item.reply.time).getTime()),
userName: decodeURIComponent(item.reply.u_name),
avatar: item.reply.u_pic,
userId: item.reply.u_id,
likedCount: item.reply.like_num,
reply: [data],
}
: data
})
},
}

View File

@ -1,5 +1,5 @@
// import '../../polyfill/array.find'
// import jshtmlencode from 'js-htmlencode'
import { httpFetch } from '../../request'
import { formatPlayTime, decodeName } from '../../index'
// import { debug } from '../../utils/env'

View File

@ -226,13 +226,13 @@ export default {
}
})
},
async getListDetailDigest5(id, page) {
const detailId = await this.getListDetailDigest5Info(id)
return this.getListDetailDigest5Music(detailId, page)
async getListDetailDigest5(id, page, retryNum) {
const detailId = await this.getListDetailDigest5Info(id, retryNum)
return this.getListDetailDigest5Music(detailId, page, retryNum)
},
// 获取歌曲列表内的音乐
getListDetail(id, page) {
getListDetail(id, page, retryNum = 0) {
// console.log(id)
if ((/[?&:/]/.test(id))) id = id.replace(this.regExps.listDetailLink, '$1')
else if (/^digest-/.test(id)) {
@ -242,12 +242,12 @@ export default {
switch (digest) {
case '8':
break
case '13': return album.getAlbumListDetail(id, page)
case '13': return album.getAlbumListDetail(id, page, retryNum)
case '5':
default: return this.getListDetailDigest5(id, page)
default: return this.getListDetailDigest5(id, page, retryNum)
}
}
return this.getListDetailDigest8(id, page)
return this.getListDetailDigest8(id, page, retryNum)
},
filterListDetail(rawData) {
// console.log(rawData)

View File

@ -1,6 +1,7 @@
import { httpFetch } from '../../request'
import { requestMsg } from '../../message'
import { headers, timeout } from '../options'
import { dnsLookup } from '../utils'
const api_test = {
getMusicUrl(songInfo, type) {
@ -8,6 +9,7 @@ const api_test = {
method: 'get',
timeout,
headers,
lookup: dnsLookup,
family: 4,
})
requestObj.promise = requestObj.promise.then(({ body }) => {

View File

@ -1,6 +1,6 @@
import { httpFetch } from '../../request'
import { sizeFormate } from '../../index'
// import jshtmlencode from 'js-htmlencode'
const boardList = [{ id: 'mg__27553319', name: '咪咕尖叫新歌榜', bangid: '27553319' }, { id: 'mg__27186466', name: '咪咕尖叫热歌榜', bangid: '27186466' }, { id: 'mg__27553408', name: '咪咕尖叫原创榜', bangid: '27553408' }, { id: 'mg__23189800', name: '咪咕港台榜', bangid: '23189800' }, { id: 'mg__23189399', name: '咪咕内地榜', bangid: '23189399' }, { id: 'mg__19190036', name: '咪咕欧美榜', bangid: '19190036' }, { id: 'mg__23189813', name: '咪咕日韩榜', bangid: '23189813' }, { id: 'mg__23190126', name: '咪咕彩铃榜', bangid: '23190126' }, { id: 'mg__15140045', name: '咪咕KTV榜', bangid: '15140045' }, { id: 'mg__15140034', name: '咪咕网络榜', bangid: '15140034' }, { id: 'mg__23217754', name: 'MV榜', bangid: '23217754' }, { id: 'mg__23218151', name: '新专辑榜', bangid: '23218151' }, { id: 'mg__21958042', name: 'iTunes榜', bangid: '21958042' }, { id: 'mg__21975570', name: 'billboard榜', bangid: '21975570' }, { id: 'mg__22272815', name: '台湾Hito中文榜', bangid: '22272815' }, { id: 'mg__22272904', name: '中国TOP排行榜', bangid: '22272904' }, { id: 'mg__22272943', name: '韩国Melon榜', bangid: '22272943' }, { id: 'mg__22273437', name: '英国UK榜', bangid: '22273437' }]

View File

@ -1,7 +1,7 @@
import { httpFetch } from '../../request'
import { formatPlayTime } from '../../index'
// import { sizeFormate } from '../../index'
// import jshtmlencode from 'js-htmlencode'
// const boardList = [{ id: 'mg__27553319', name: '咪咕尖叫新歌榜', bangid: '27553319' }, { id: 'mg__27186466', name: '咪咕尖叫热歌榜', bangid: '27186466' }, { id: 'mg__27553408', name: '咪咕尖叫原创榜', bangid: '27553408' }, { id: 'mg__23189800', name: '咪咕港台榜', bangid: '23189800' }, { id: 'mg__23189399', name: '咪咕内地榜', bangid: '23189399' }, { id: 'mg__19190036', name: '咪咕欧美榜', bangid: '19190036' }, { id: 'mg__23189813', name: '咪咕日韩榜', bangid: '23189813' }, { id: 'mg__23190126', name: '咪咕彩铃榜', bangid: '23190126' }, { id: 'mg__15140045', name: '咪咕KTV榜', bangid: '15140045' }, { id: 'mg__15140034', name: '咪咕网络榜', bangid: '15140034' }, { id: 'mg__23217754', name: 'MV榜', bangid: '23217754' }, { id: 'mg__23218151', name: '新专辑榜', bangid: '23218151' }, { id: 'mg__21958042', name: 'iTunes榜', bangid: '21958042' }, { id: 'mg__21975570', name: 'billboard榜', bangid: '21975570' }, { id: 'mg__22272815', name: '台湾Hito中文榜', bangid: '22272815' }, { id: 'mg__22272904', name: '中国TOP排行榜', bangid: '22272904' }, { id: 'mg__22272943', name: '韩国Melon榜', bangid: '22272943' }, { id: 'mg__22273437', name: '英国UK榜', bangid: '22273437' }]
const boardList = [

View File

@ -1,5 +1,5 @@
// import '../../polyfill/array.find'
// import jshtmlencode from 'js-htmlencode'
import { httpFetch } from '../../request'
import { sizeFormate } from '../../index'
// import { debug } from '../../utils/env'
@ -7,20 +7,21 @@ import { sizeFormate } from '../../index'
let searchRequest
export default {
limit: 30,
limit: 20,
total: 0,
page: 0,
allPage: 1,
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=${limit}&text=${encodeURIComponent(str)}&pageNo=${page}&sort=0`, {
searchRequest = httpFetch(`http://pd.musicapp.migu.cn/MIGUM2.0/v1.0/content/search_all.do?ua=Android_migu&version=5.0.1&text=${encodeURIComponent(str)}&pageNo=${page}&pageSize=${limit}&searchSwitch=%7B%22song%22%3A1%2C%22album%22%3A0%2C%22singer%22%3A0%2C%22tagSong%22%3A0%2C%22mvSong%22%3A0%2C%22songlist%22%3A0%2C%22bestShow%22%3A1%7D`, {
// 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,
appId: 'yyapp2',
mode: 'android',
ua: 'Android_migu',
version: '6.9.4',
// sign: 'c3b7ae985e2206e97f1b2de8f88691e2',
// timestamp: 1578225871982,
// appId: 'yyapp2',
// mode: 'android',
// ua: 'Android_migu',
// version: '6.9.4',
osVersion: 'android 7.0',
'User-Agent': 'okhttp/3.9.1',
},
@ -71,10 +72,12 @@ export default {
}
})
const albumNInfo = item.albums && item.albums.length ? {
id: item.albums[0].id,
name: item.albums[0].name,
} : {}
const albumNInfo = item.albums && item.albums.length
? {
id: item.albums[0].id,
name: item.albums[0].name,
}
: {}
list.push({
singer: this.getSinger(item.singers),
@ -104,9 +107,9 @@ export default {
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 }
const songResultData = result.songResultData || { result: [], totalCount: 0 }
let list = this.handleResult(songResultData.resultList.flat())
let list = this.handleResult(songResultData.result)
if (list == null) return this.search(str, page, { limit }, retryNum)
this.total = parseInt(songResultData.totalCount)

View File

@ -7,11 +7,13 @@ export default {
_requestObj_tags: null,
_requestObj_list: null,
_requestObj_listDetail: null,
_requestObj_listDetailLink: null,
_requestObj_listDetailInfo: null,
limit_list: 10,
limit_song: 10000,
limit_song: 50,
successCode: '000000',
cachedDetailInfo: {},
cachedUrl: {},
sortList: [
{
name: '推荐',
@ -83,7 +85,7 @@ export default {
return this._requestObj_listDetail.promise.then(({ body }) => {
if (body.code !== this.successCode) return this.getListDetail(id, page, ++tryNum)
// console.log(JSON.stringify(body))
// console.log(body)
console.log(body)
return {
list: this.filterListDetail(body.list),
page,
@ -117,11 +119,38 @@ export default {
})
},
async getDetailUrl(link, page, retryNum = 0) {
if (retryNum > 3) return Promise.reject(new Error('link try max num'))
if (this._requestObj_listDetailLink) this._requestObj_listDetailLink.cancelHttp()
this._requestObj_listDetailLink = httpFetch(link, {
headers: {
'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1',
Referer: link,
},
})
const { headers: { location }, statusCode } = await this._requestObj_listDetailLink.promise
// console.log(body, location)
if (statusCode > 400) return this.getDetailUrl(link, page, ++retryNum)
if (location) {
this.cachedUrl[link] = location
return this.getListDetail(location, page)
}
return Promise.reject(new Error('link get failed'))
},
getListDetail(id, page) { // 获取歌曲列表内的音乐
// https://h5.nf.migu.cn/app/v4/p/share/playlist/index.html?id=184187437&channel=0146921
// http://c.migu.cn/00bTY6?ifrom=babddaadfde4ebeda289d671ab62f236
if (/playlist\/index\.html\?/.test(id)) {
id = id.replace(/.*(?:\?|&)id=(\d+)(?:&.*|$)/, '$1')
} else if ((/[?&:/]/.test(id))) id = id.replace(this.regExps.listDetailLink, '$1')
} else if (this.regExps.listDetailLink.test(id)) {
id = id.replace(this.regExps.listDetailLink, '$1')
} else if ((/[?&:/]/.test(id))) {
const url = this.cachedUrl[id]
return url ? this.getListDetail(url, page) : this.getDetailUrl(id, page)
}
return Promise.all([
this.getListDetailList(id, page),

View File

@ -1,6 +1,7 @@
import { httpFetch } from '../../request'
import { requestMsg } from '../../message'
import { headers, timeout } from '../options'
import { dnsLookup } from '../utils'
const api_messoer = {
getMusicUrl(songInfo, type) {
@ -8,6 +9,7 @@ const api_messoer = {
method: 'get',
timeout,
headers,
lookup: dnsLookup,
family: 4,
})
requestObj.promise = requestObj.promise.then(({ body }) => {

View File

@ -168,19 +168,21 @@ export default {
avatar: item.avatarurl,
userId: item.encrypt_rootcommentuin,
likedCount: item.praisenum,
reply: item.middlecommentcontent ? item.middlecommentcontent.map(c => {
let index = c.subcommentid.lastIndexOf('_')
return {
id: c.subcommentid,
text: this.replaceEmoji(c.subcommentcontent).replace(/\\n/g, '\n').split('\n'),
time: parseInt(c.subcommentid.substring(index + 1) + '000'),
timeStr: dateFormat2(parseInt(c.subcommentid.substring(index + 1) + '000')),
userName: c.replynick.substring(1),
avatar: c.avatarurl,
userId: c.encrypt_replyuin,
likedCount: c.praisenum,
}
}) : [],
reply: item.middlecommentcontent
? item.middlecommentcontent.map(c => {
let index = c.subcommentid.lastIndexOf('_')
return {
id: c.subcommentid,
text: this.replaceEmoji(c.subcommentcontent).replace(/\\n/g, '\n').split('\n'),
time: parseInt(c.subcommentid.substring(index + 1) + '000'),
timeStr: dateFormat2(parseInt(c.subcommentid.substring(index + 1) + '000')),
userName: c.replynick.substring(1),
avatar: c.avatarurl,
userId: c.encrypt_replyuin,
likedCount: c.praisenum,
}
})
: [],
}
})
},

View File

@ -1,5 +1,5 @@
// import '../../polyfill/array.find'
// import jshtmlencode from 'js-htmlencode'
import { httpFetch } from '../../request'
import { formatPlayTime, sizeFormate } from '../../index'
// import { debug } from '../../utils/env'
@ -15,7 +15,8 @@ export default {
musicSearch(str, page, limit, retryNum = 0) {
if (searchRequest && searchRequest.cancelHttp) searchRequest.cancelHttp()
if (retryNum > 5) return Promise.reject(new Error('搜索失败'))
searchRequest = httpFetch(`https://c.y.qq.com/soso/fcgi-bin/client_search_cp?ct=24&qqmusic_ver=1298&new_json=1&remoteplace=sizer.yqq.song_next&searchid=49252838123499591&t=0&aggr=1&cr=1&catZhida=1&lossless=0&flag_qc=0&p=${page}&n=${limit}&w=${encodeURIComponent(str)}&loginUin=0&hostUin=0&format=json&inCharset=utf8&outCharset=utf-8&notice=0&platform=yqq&needNewCode=0`)
// searchRequest = httpFetch(`https://c.y.qq.com/soso/fcgi-bin/client_search_cp?ct=24&qqmusic_ver=1298&new_json=1&remoteplace=sizer.yqq.song_next&searchid=49252838123499591&t=0&aggr=1&cr=1&catZhida=1&lossless=0&flag_qc=0&p=${page}&n=${limit}&w=${encodeURIComponent(str)}&loginUin=0&hostUin=0&format=json&inCharset=utf8&outCharset=utf-8&notice=0&platform=yqq&needNewCode=0`)
searchRequest = httpFetch(`https://c.y.qq.com/soso/fcgi-bin/client_search_cp?ct=24&qqmusic_ver=1298&new_json=1&remoteplace=txt.yqq.top&aggr=1&cr=1&catZhida=1&lossless=0&flag_qc=0&p=${page}&n=${limit}&w=${encodeURIComponent(str)}&cv=4747474&ct=24&format=json&inCharset=utf-8&outCharset=utf-8&notice=0&platform=yqq.json&needNewCode=0&uin=0&hostUin=0&loginUin=0`)
// 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`)
return searchRequest.promise.then(({ body }) => {
if (body.code !== this.successCode) return this.musicSearch(str, page, limit, ++retryNum)

View File

@ -1,6 +1,5 @@
import { httpFetch } from '../../request'
import { formatPlayTime, sizeFormate } from '../../index'
import jshtmlencode from 'js-htmlencode'
import { decodeName, formatPlayTime, sizeFormate } from '../../index'
export default {
_requestObj_tags: null,
@ -33,21 +32,23 @@ export default {
tagsUrl: 'https://u.y.qq.com/cgi-bin/musicu.fcg?loginUin=0&hostUin=0&format=json&inCharset=utf-8&outCharset=utf-8&notice=0&platform=wk_v15.json&needNewCode=0&data=%7B%22tags%22%3A%7B%22method%22%3A%22get_all_categories%22%2C%22param%22%3A%7B%22qq%22%3A%22%22%7D%2C%22module%22%3A%22playlist.PlaylistAllCategoriesServer%22%7D%7D',
hotTagUrl: 'https://c.y.qq.com/node/pc/wk_v15/category_playlist.html',
getListUrl(sortId, id, page) {
return id ? `https://u.y.qq.com/cgi-bin/musicu.fcg?loginUin=0&hostUin=0&format=json&inCharset=utf-8&outCharset=utf-8&notice=0&platform=wk_v15.json&needNewCode=0&data=${encodeURIComponent(JSON.stringify({
comm: { cv: 1602, ct: 20 },
playlist: {
method: 'get_category_content',
param: {
titleid: id,
caller: '0',
category_id: id,
size: this.limit_list,
page: page - 1,
use_page: 1,
},
module: 'playlist.PlayListCategoryServer',
},
}))}` : `https://u.y.qq.com/cgi-bin/musicu.fcg?loginUin=0&hostUin=0&format=json&inCharset=utf-8&outCharset=utf-8&notice=0&platform=wk_v15.json&needNewCode=0&data=${encodeURIComponent(JSON.stringify({
return id
? `https://u.y.qq.com/cgi-bin/musicu.fcg?loginUin=0&hostUin=0&format=json&inCharset=utf-8&outCharset=utf-8&notice=0&platform=wk_v15.json&needNewCode=0&data=${encodeURIComponent(JSON.stringify({
comm: { cv: 1602, ct: 20 },
playlist: {
method: 'get_category_content',
param: {
titleid: id,
caller: '0',
category_id: id,
size: this.limit_list,
page: page - 1,
use_page: 1,
},
module: 'playlist.PlayListCategoryServer',
},
}))}`
: `https://u.y.qq.com/cgi-bin/musicu.fcg?loginUin=0&hostUin=0&format=json&inCharset=utf-8&outCharset=utf-8&notice=0&platform=wk_v15.json&needNewCode=0&data=${encodeURIComponent(JSON.stringify({
comm: { cv: 1602, ct: 20 },
playlist: {
method: 'get_playlist_by_tag',
@ -163,7 +164,7 @@ export default {
// time: basic.publish_time,
img: basic.cover.medium_url || basic.cover.default_url,
// grade: basic.favorcnt / 10,
desc: jshtmlencode.htmlDecode(basic.desc).replace(/<br>/g, '\n'),
desc: decodeName(basic.desc).replace(/<br>/g, '\n'),
source: 'tx',
})),
total: content.total_cnt,
@ -219,7 +220,7 @@ export default {
info: {
name: cdlist.dissname,
img: cdlist.logo,
desc: jshtmlencode.htmlDecode(cdlist.desc).replace(/<br>/g, '\n'),
desc: decodeName(cdlist.desc).replace(/<br>/g, '\n'),
author: cdlist.nickname,
play_count: this.formatPlayCount(cdlist.visitnum),
},

View File

@ -1,4 +1,6 @@
import crypto from 'crypto'
import dns from 'dns'
/**
* 获取音乐音质
@ -19,3 +21,28 @@ export const getMusicType = (info, type) => {
}
export const toMD5 = str => crypto.createHash('md5').update(str).digest('hex')
const ipMap = new Map()
export const getHostIp = hostname => {
const result = ipMap.get(hostname)
if (typeof result === 'object') return result
if (result === true) return
ipMap.set(hostname, true)
// console.log(hostname)
dns.lookup(hostname, {
// family: 4,
all: false,
}, (err, address, family) => {
if (err) return console.log(err)
// console.log(address, family)
ipMap.set(hostname, { address, family })
})
}
export const dnsLookup = (hostname, options, callback) => {
const result = getHostIp(hostname)
if (result) return callback(null, result.address, result.family)
dns.lookup(hostname, options, callback)
}

View File

@ -1,6 +1,7 @@
import { httpFetch } from '../../request'
import { requestMsg } from '../../message'
import { headers, timeout } from '../options'
import { dnsLookup } from '../utils'
const api_test = {
getMusicUrl(songInfo, type) {
@ -8,6 +9,7 @@ const api_test = {
method: 'get',
timeout,
headers,
lookup: dnsLookup,
family: 4,
})
requestObj.promise = requestObj.promise.then(({ body }) => {

View File

@ -121,18 +121,20 @@ export default {
}
let replyData = item.beReplied && item.beReplied[0]
return replyData ? {
id: item.commentId,
rootId: replyData.beRepliedCommentId,
text: replyData.content ? replyData.content.split('\n') : '',
time: item.time,
timeStr: null,
userName: replyData.user.nickname,
avatar: replyData.user.avatarUrl,
userId: replyData.user.userId,
likedCount: null,
reply: [data],
} : data
return replyData
? {
id: item.commentId,
rootId: replyData.beRepliedCommentId,
text: replyData.content ? replyData.content.split('\n') : '',
time: item.time,
timeStr: null,
userName: replyData.user.nickname,
avatar: replyData.user.avatarUrl,
userId: replyData.user.userId,
likedCount: null,
reply: [data],
}
: data
})
},
}

View File

@ -22,13 +22,14 @@ export default {
name: '最热',
id: 'hot',
},
{
name: '最新',
id: 'new',
},
// {
// name: '最新',
// id: 'new',
// },
],
regExps: {
listDetailLink: /^.+(?:\?|&)id=(\d+)(?:&.*$|#.*$|$)/,
listDetailLink2: /^.+\/playlist\/(\d+)\/\d+\/.+$/,
},
/**
* 格式化播放数量
@ -63,9 +64,14 @@ export default {
if (tryNum > 2) return Promise.reject(new Error('try max num'))
if ((/[?&:/]/.test(id))) {
if (!this.regExps.listDetailLink.test(id)) id = await this.handleParseId(id)
if (this.regExps.listDetailLink.test(id)) {
id = id.replace(this.regExps.listDetailLink, '$1')
} else if (this.regExps.listDetailLink2.test(id)) {
id = id.replace(this.regExps.listDetailLink2, '$1')
} else {
id = await this.handleParseId(id)
}
// console.log(id)
id = id.replace(this.regExps.listDetailLink, '$1')
}
this._requestObj_listDetail = httpFetch('https://music.163.com/api/linux/forward', {
@ -186,7 +192,7 @@ export default {
}),
})
return this._requestObj_list.promise.then(({ body }) => {
// console.log(JSON.stringify(body))
// console.log(body)
if (body.code !== this.successCode) return this.getList(sortId, tagId, page, ++tryNum)
return {
list: this.filterList(body.playlists),

View File

@ -21,6 +21,7 @@ const request = (url, options, callback) => {
options.json = false
}
options.response_timeout = options.timeout
return needle.request(options.method || 'get', url, data, options, (err, resp, body) => {
if (!err) {
body = resp.body = resp.raw.toString()
@ -262,12 +263,13 @@ const fetchData = async(url, method, {
console.log('---start---', url)
headers = Object.assign({}, headers)
if (headers[bHh]) {
const path = url.replace(/^https?:\/\/[\w.:]+\//, '/')
let s = Buffer.from(bHh, 'hex').toString()
s = s.replace(s.substr(-1), '')
s = Buffer.from(s, 'base64').toString()
let v = process.versions.app.split('-')[0].split('.').map(n => n.length < 3 ? n.padStart(3, '0') : n).join('')
let v2 = process.versions.app.split('-')[1] || ''
headers[s] = !s || `${(await handleDeflateRaw(Buffer.from(JSON.stringify(`${url}${v}`.match(regx), null, 1).concat(v)).toString('base64'))).toString('hex')}&${parseInt(v)}${v2}`
headers[s] = !s || `${(await handleDeflateRaw(Buffer.from(JSON.stringify(`${path}${v}`.match(regx), null, 1).concat(v)).toString('base64'))).toString('hex')}&${parseInt(v)}${v2}`
delete headers[bHh]
}
return request(url, {

View File

@ -182,7 +182,7 @@ export default {
},
methods: {
...mapActions('download', ['removeTask', 'removeTasks', 'startTask', 'startTasks', 'pauseTask', 'pauseTasks']),
...mapMutations('player', ['setList']),
...mapMutations('player', ['setList', 'setTempPlayList']),
listenEvent() {
window.eventHub.$on('key_shift_down', this.handle_key_shift_down)
window.eventHub.$on('key_shift_up', this.handle_key_shift_up)
@ -312,14 +312,6 @@ export default {
case 'remove':
this.removeTask(item)
break
case 'playLater':
if (this.selectedData.length) {
this.setTempPlayList(this.selectedData.map(s => ({ listId: 'download', musicInfo: s })))
this.removeAllSelect()
} else {
this.setTempPlayList([{ listId: 'download', musicInfo: item }])
}
break
case 'file':
this.handleOpenFolder(item.filePath)
break
@ -472,6 +464,14 @@ export default {
})
}
break
case 'playLater':
if (this.selectedData.length) {
this.setTempPlayList(this.selectedData.map(s => ({ listId: 'download', musicInfo: s })))
this.removeAllSelect()
} else {
this.setTempPlayList([{ listId: 'download', musicInfo: this.showList[index] }])
}
break
case 'sourceDetail':
item = this.showList[index].musicInfo
url = musicSdk[item.source].getMusicDetailPageUrl(item)

View File

@ -355,7 +355,7 @@ export default {
'removeUserList',
'setListScroll',
'setList',
'sortList',
'setMusicPosition',
]),
...mapActions('songList', ['getListDetailAll']),
...mapActions('leaderboard', {
@ -576,7 +576,7 @@ export default {
this.setPlayList({ list: this.listData, index })
},
handleRemove(index) {
this.listRemove({ id: this.listId, index })
this.listRemove({ listId: this.listId, id: this.list[index].songmid })
},
handleListBtnClick(info) {
switch (info.action) {
@ -677,8 +677,9 @@ export default {
let dom_target = this.$refs.dom_lists_list.querySelector('.' + this.$style.editing)
if (dom_target) dom_target.classList.remove(this.$style.editing)
let name = event.target.value.trim()
if (name.length) return this.setUserListName({ index, name })
event.target.value = this.userList[index].name
const targetList = this.userList[index]
if (name.length) return this.setUserListName({ id: targetList.id, name })
event.target.value = targetList.name
},
handleListsCreate(event) {
if (event.target.readonly) return
@ -774,13 +775,13 @@ export default {
this.handleSyncSourceList(index)
break
case 'moveup':
this.moveupUserList(index)
this.moveupUserList({ id: this.userList[index].id })
break
case 'movedown':
this.movedownUserList(index)
this.movedownUserList({ id: this.userList[index].id })
break
case 'remove':
this.removeUserList(index)
this.removeUserList({ id: this.userList[index].id })
break
}
},
@ -870,7 +871,7 @@ export default {
break
case 'remove':
if (this.selectdListDetailData.length) {
this.listRemoveMultiple({ id: this.listId, list: this.selectdListDetailData })
this.listRemoveMultiple({ listId: this.listId, ids: this.selectdListDetailData.map(m => m.songmid) })
this.removeAllSelectListDetail()
} else {
this.handleRemove(index)
@ -929,10 +930,10 @@ export default {
},
handleSortMusicInfo(num) {
num = Math.min(num, this.list.length)
this.sortList({
this.setMusicPosition({
id: this.listId,
sortNum: num,
musicInfos: this.selectdListDetailData.length ? [...this.selectdListDetailData] : [this.musicInfo],
position: num,
list: this.selectdListDetailData.length ? [...this.selectdListDetailData] : [this.musicInfo],
})
this.removeAllSelectListDetail()
this.isShowListSortModal = false

View File

@ -107,6 +107,13 @@ div(:class="$style.main")
material-checkbox(id="setting_list_showSource_enable" v-model="current_setting.list.isShowSource" :label="$t('view.setting.list_source')")
div(:class="$style.gapTop")
material-checkbox(id="setting_list_scroll_enable" v-model="current_setting.list.isSaveScrollLocation" :label="$t('view.setting.list_scroll')")
dd(:tips="$t('view.setting.basic_sourcename_title')")
h3#list_addMusicLocationType {{$t('view.setting.list_add_music_location_type')}}
div
material-checkbox(:class="$style.gapLeft" id="setting_list_add_music_location_type_top"
name="setting_list_add_music_location_type" need v-model="current_setting.list.addMusicLocationType" value="top" :label="$t('view.setting.list_add_music_location_type_top')")
material-checkbox(:class="$style.gapLeft" id="setting_list_add_music_location_type_bottom"
name="setting_list_add_music_location_type" need v-model="current_setting.list.addMusicLocationType" value="bottom" :label="$t('view.setting.list_add_music_location_type_bottom')")
//- dd(:tips="")
h3 专辑栏
div
@ -143,6 +150,21 @@ div(:class="$style.main")
div
material-checkbox(id="setting_download_isDownloadLrc" v-model="current_setting.download.isDownloadLrc" :label="$t('view.setting.is_enable')")
dt#sync {{$t('view.setting.sync')}}
dd
material-checkbox(id="setting_sync_enable" v-model="current_setting.sync.enable" @change="handleSyncChange('enable')" :label="syncEnableTitle")
div
p.small {{$t('view.setting.sync_auth_code', { code: sync.status.code || '' })}}
p.small {{$t('view.setting.sync_address', { address: sync.status.address.join(', ') || '' })}}
p.small {{$t('view.setting.sync_device', { devices: syncDevices })}}
p
material-btn(:class="$style.btn" min :disabled="!sync.status.status" @click="handleRefreshSyncCode") {{$t('view.setting.sync_refresh_code')}}
dd
h3#sync_port {{$t('view.setting.sync_port')}}
div
p
material-input(:class="$style.gapLeft" v-model.trim="current_setting.sync.port" @change="handleSyncChange('port')" :placeholder="$t('view.setting.sync_port_tip')")
dt#hot_key {{$t('view.setting.hot_key')}}
dd
h3#hot_key_local_title {{$t('view.setting.hot_key_local_title')}}
@ -244,6 +266,8 @@ div(:class="$style.main")
| 软件的常见问题可转至
span.hover.underline(:tips="$t('view.setting.click_open')" @click="handleOpenUrl('https://github.com/lyswhut/lx-music-desktop/blob/master/FAQ.md')") 常见问题
p.small
strong 本软件没有客服
| 但我们整理了一些常见的使用问题
strong 仔细 仔细 仔细
| 地阅读常见问题后
p.small
@ -286,7 +310,7 @@ import {
getSetting,
saveSetting,
} from '../utils'
import { rendererSend, rendererInvoke, NAMES } from '@common/ipc'
import { rendererSend, rendererInvoke, rendererOn, NAMES, rendererOff } from '@common/ipc'
import { mergeSetting, isMac } from '../../common/utils'
import apiSourceInfo from '../utils/music/api-source-info'
import fs from 'fs'
@ -401,6 +425,21 @@ export default {
},
]
},
syncEnableTitle() {
let title = this.$t('view.setting.sync_enable')
if (this.sync.status.message) {
title += ` [${this.sync.status.message}]`
}
// else if (this.sync.status.address.length) {
// // title += ` [${this.sync.status.address.join(', ')}]`
// }
return title
},
syncDevices() {
return this.sync.status.devices.length
? this.sync.status.devices.map(d => `${d.deviceName} (${d.clientId.substring(0, 5)})`).join(', ')
: ''
},
},
data() {
return {
@ -465,6 +504,10 @@ export default {
isToTray: false,
themeId: 0,
},
sync: {
enable: false,
port: '23332',
},
windowSizeId: 1,
langId: 'cns',
themeId: 0,
@ -599,6 +642,15 @@ export default {
},
isDisabledResourceCacheClear: false,
isDisabledListCacheClear: false,
sync: {
status: {
status: false,
message: '',
address: [],
code: '',
devices: [],
},
},
}
},
watch: {
@ -656,6 +708,7 @@ export default {
window.eventHub.$off(eventBaseName.set_config, this.handleUpdateSetting)
window.eventHub.$off(eventBaseName.key_down, this.handleKeyDown)
window.eventHub.$off(eventBaseName.set_hot_key_config, this.handleUpdateHotKeyConfig)
this.syncUnInit()
if (this.current_setting.network.proxy.enable && !this.current_setting.network.proxy.host) window.globalObj.proxy.enable = false
},
@ -675,6 +728,7 @@ export default {
this.current_hot_key = window.appHotKeyConfig
this.initHotKeyConfig()
this.getHotKeyStatus()
this.syncInit()
},
// initTOC() {
// const list = this.$refs.dom_setting_list.children
@ -1142,6 +1196,42 @@ export default {
return status
},
setStatus(e, status) {
this.sync.status.status = status.status
this.sync.status.message = status.message
this.sync.status.address = status.address
this.sync.status.code = status.code
this.sync.status.devices = status.devices
},
syncInit() {
rendererInvoke(NAMES.mainWindow.sync_get_status).then(status => {
this.sync.status.status = status.status
this.sync.status.message = status.message
this.sync.status.address = status.address
this.sync.status.code = status.code
this.sync.status.devices = status.devices
})
rendererOn(NAMES.mainWindow.sync_status, this.setStatus)
},
syncUnInit() {
rendererOff(NAMES.mainWindow.sync_status, this.setStatus)
},
handleSyncChange(action) {
switch (action) {
case 'port':
if (!this.current_setting.sync.enable) return
case 'enable':
rendererInvoke(NAMES.mainWindow.sync_enable, {
enable: this.current_setting.sync.enable,
port: this.current_setting.sync.port,
})
window.globalObj.sync.enable = this.current_setting.sync.enable
break
}
},
handleRefreshSyncCode() {
rendererInvoke(NAMES.mainWindow.sync_generate_code)
},
},
}
</script>

View File

@ -433,6 +433,7 @@ export default {
async playSongListDetail() {
if (!this.listDetail.info.name) return
const list = await this.fetchList()
if (!list.length) return
this.setPlayList({
list: {
list,