Merge branch 'dev'

pull/590/head v1.12.0
lyswhut 2021-08-08 10:29:09 +08:00
commit 0164936038
48 changed files with 2918 additions and 1223 deletions

View File

@ -16,7 +16,8 @@
"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"
},
"settings": {
"html/html-extensions": [".html", ".vue"]

View File

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

@ -6,6 +6,21 @@ 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.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
### 新增

93
FAQ.md
View File

@ -78,6 +78,99 @@
对于一些完全无法正常显示界面、开启了AERO后问题仍未解决的情况请阅读下面的 **软件启动后,界面无法显示** 解决。
# lx-music-mobile 常见问题
在阅读本常见问题后仍然无法解决你的问题请提交issue或者加企鹅群`830125506`反馈(无事勿加,入群先看群公告),反馈时请**注明**已阅读常见问题!
## 歌曲无法试听与下载
### 所有歌曲都提示 `请求异常😮,可以多试几次,若还是不行就换一首吧。。。`
尝试更换网络,如切换到移动网络,若移动网络还是不行则尝试开关下手机的飞行模式后再试,<br>
若使用家庭网络的话可尝试将光猫断电5分钟左右再通电联网后播放。
### 其他情况
尝试在在浏览器打开这个地址`http://ts.tempmusic.tk`浏览器显示404是正常的如果不是404那就证明所在网络无法访问接口服务器对于此类情况请尝试切换其他网络。
### 通用解决方法
尝试按以下顺序解决:
1. 尝试更新到最新版本
2. 尝试切换其他歌曲(或直接搜索该歌曲),若全部歌曲都无法试听与下载则进行下一步
3. 尝试到 设置-音乐来源 切换到其他接口
4. 尝试切换网络,比如用手机开热点(所有歌曲都提示请求异常时可通过此方法解决,或等一两天后再试)
5. 若还不行请到这个链接查看详情:<https://github.com/lyswhut/lx-music-desktop/issues/5>
6. 若没有在第5条链接中的第一条评论中看到接口无法使用的说明则应该是你网络无法访问接口服务器的问题如果接口有问题我会在那里说明。
想要知道是不是自己网络的问题可以看看`http://ts.tempmusic.tk`能不能在浏览器打开浏览器显示404是正常的如果不是404那就证明所在网络无法访问接口服务器。
若网页无法打开或打来不是404则应该是DNS的问题可以尝试以下办法
1. 将DNS改成自动获取试试
2. 手动把DNS改一下不要用360的DNS可以把DNS改成`114.114.114.114`、`8.8.8.8`
## 列表多选
长按列表将会进入多选模式。
- 例子一想要选中1-5项进入多选模式后取消所有选中的内容切换到区间点击第一项再点击第五项即可完成选择
- 例子二想要选中1项与第3项进入多选模式后点击第一项再点击第三项即可完成选择
- 例子三:想要选中当前列表的全部内容,进入多选模式后,点击全选即可完成选择(注:由于**在线列表**使用分页加载,全选只会选择目前已加载的内容,若要完整选择整个在线列表的内容则需要往下滑动将列表加载完毕再进行全选)。
注:选完后可用歌曲列表三个点的菜单操作已选的内容
## 无法打开外部歌单
不支持垮源打开歌单,请**确认**你需要打开的歌单平台是否与软件标签所写的**歌单源**对应(不一样的话请通过右上角切换歌单源);<br>
对于分享出来的歌单若打开失败可尝试先在浏览器中打开后再从浏览器地址栏复制URL地址到软件打开<br>
或者如果你知道歌单 id 也可以直接输入歌单 id 打开。<br>
注:网易源的“我喜欢”歌单无法在未登录的情况下打开,所以你需要手动创建一个歌单后将“我喜欢”里的歌曲移动到该歌单打开
## 播放整个歌单或排行榜
播放在线列表内的歌曲需要将它们都添加到我的列表才能播放,你可以全选列表内的歌曲然后添加到现有列表或者新创建的列表,然后去播放该列表内的歌曲。
## 无法打开外部歌单
不支持垮源打开歌单,请**确认**你需要打开的歌单平台是否与软件标签所写的**歌单源**对应(不一样的话请通过右上角切换歌单源);<br>
对于分享出来的歌单若打开失败可尝试先在浏览器中打开后再从浏览器地址栏复制URL地址到软件打开<br>
或者如果你知道歌单 id 也可以直接输入歌单 id 打开。<br>
## 同步功能的使用(实验性,首次使用前建议先备份一次列表)
**注意:由于同步传输时的数据是明文传输,请在受信任的网络下使用此功能!**<br>
此功能需要配合PC端使用移动端与PC端处在同一个局域网路由器的网络下时可以多端实时同步歌曲列表使用方法
1. 在PC端的设置-数据同步开启同步功能(这时如果出现安全软件、防火墙等提示网络连接弹窗时需要点击允许)
2. 在移动端的设置-同步-同步服务器地址输入PC端显示的同步服务器地址如果显示可以多个则输入与**移动端上显示的本机地址**最相似的那个端口号与PC端的同步端口一致
3. 输入完这两项后点击“启动同步”
4. 若连接成功对于首次同步时若两边的设备的列表不为空则PC端会弹出选择列表同步方式的弹窗同步方式的说明弹窗下面有介绍
对于连接同步失败的可能原因:
- 此功能需要PC端与移动端都连接在同一个路由器下的网络才能使用
- 路由器若开启了AP隔离则此功能无法使用
- 检查防火墙是否拦截了PC端的服务端口
## 更新已收藏的在线歌单
该功能仅对直接从歌单详情页点“收藏”按钮收藏的歌单有效,可右击已收藏的列表名从弹出的菜单中选择“更新”使用该功能,
需要注意的是:这将会覆盖本地的目标列表,歌曲将被替换成最新的在线列表。
## 杀毒软件提示有病毒或恶意行为
本人只能保证我写的代码不包含任何**恶意代码**、**收集用户信息**的行为并且软件代码已开源请自行查阅软件安装包也是由CI拉取源代码构建构建日志[GitHub Actions](https://github.com/lyswhut/lx-music-mobile/actions)<br>
尽管如此但这不意味着软件是100%安全的,由于软件使用了第三方依赖,当这些依赖存在恶意行为时([供应链攻击](https://docs.microsoft.com/zh-cn/windows/security/threat-protection/intelligence/supply-chain-malware)),软件也将会受到牵连,所以我只能尽量选择使用较多人用、信任度较高的依赖。<br>
当然,以上说明建立的前提是在你所用的安装包是从**本项目主页上写的链接**下载的,或者有相关能力者还可以下载源代码自己构建安装包。
最后,若出现杀毒软件报毒、存在恶意行为,请自行判断选择是否继续使用本软件!
### Linux 下界面异常
根据Electron里issue的[解决方案](https://github.com/electron/electron/issues/2170#issuecomment-736223269)<br>

View File

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

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

2040
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "lx-music-desktop",
"version": "1.11.0",
"version": "1.12.0",
"description": "一个免费的音乐查找助手",
"main": "./dist/electron/main.js",
"productName": "lx-music-desktop",
@ -163,33 +163,32 @@
},
"homepage": "https://github.com/lyswhut/lx-music-desktop#readme",
"devDependencies": {
"@babel/core": "^7.14.6",
"@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.5",
"@babel/plugin-transform-runtime": "^7.14.5",
"@babel/plugin-transform-runtime": "^7.15.0",
"@babel/polyfill": "^7.12.1",
"@babel/preset-env": "^7.14.7",
"@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",
"browserslist": "^4.16.7",
"cfonts": "^2.9.3",
"chalk": "^4.1.1",
"chalk": "^4.1.2",
"changelog-parser": "^2.8.0",
"copy-webpack-plugin": "^9.0.1",
"core-js": "^3.15.2",
"core-js": "^3.16.0",
"cross-env": "^7.0.3",
"css-loader": "^5.2.7",
"css-loader": "^6.2.0",
"css-minimizer-webpack-plugin": "^3.0.2",
"del": "^6.0.0",
"electron": "^13.1.7",
"electron": "^13.1.8",
"electron-builder": "^22.11.7",
"electron-debug": "^3.2.0",
"electron-devtools-installer": "^3.2.0",
"electron-to-chromium": "^1.3.779",
"eslint": "^7.30.0",
"electron-to-chromium": "^1.3.796",
"eslint": "^7.32.0",
"eslint-config-standard": "^14.1.1",
"eslint-formatter-friendly": "^7.0.0",
"eslint-loader": "^4.0.2",
@ -204,9 +203,9 @@
"less": "^4.1.1",
"less-loader": "^10.0.1",
"less-plugin-clean-css": "^1.5.1",
"markdown-it": "^12.1.0",
"mini-css-extract-plugin": "^2.1.0",
"postcss": "^8.3.5",
"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",
@ -219,25 +218,32 @@
"stylus-loader": "^6.1.0",
"terser-webpack-plugin": "^5.1.4",
"url-loader": "^4.1.1",
"vue-loader": "^15.9.7",
"vue-loader": "^15.9.8",
"vue-template-compiler": "^2.6.14",
"webpack": "^5.45.1",
"webpack": "^5.49.0",
"webpack-cli": "^4.7.2",
"webpack-dev-server": "^3.11.2",
"webpack-hot-middleware": "^2.25.0",
"webpack-merge": "^5.8.0"
},
"dependencies": {
"crypto-js": "^4.0.0",
"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",
"http-terminator": "^3.0.0",
"iconv-lite": "^0.6.3",
"image-size": "^1.0.0",
"lrc-file-parser": "^1.1.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",
"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",

View File

@ -1,26 +1,12 @@
### 新增
- 添加 win arm64 架构的安装包构建
- 新增“添加歌曲到列表时的位置”设置,可选项为列表的“顶部”与“底部”
- 新增局域网同步功能实验性首次使用前建议先备份一次列表此功能需要配合PC端使用移动端与PC端处在同一个局域网路由器的网络下时可以多端实时同步歌曲列表使用问题请看"常见问题"。
### 优化
- 优化网络请求,尝试去解决无法连接服务器的问题
- 优化mg源打开歌单的链接兼容
- 添加播放器对系统媒体控制与显示的兼容处理现在在windows下的锁屏界面可以正确显示当前播放的音乐信息及切换歌曲了
### 修复
- 修复mg源搜索失效的问题
### 移除
- 因wy源的歌单列表已没有“最新”排序的选项所以现跟随移除wy源歌单列表按“最新”排序的按钮
### 变更
- 添加歌曲到列表时从原来的底部改为顶部,若你想要将你的列表歌曲顺序反转以适应这一变更,可先按住`shift`键的情况下点击列表的最后一首歌然后再点击列表的第一首歌完成倒序选中最后随便右击列表的任意一首歌在弹出的菜单中选择调整顺序在弹出框输入1后确定即可反转列表。
若你想要恢复原来的行为则可以去更改“添加歌曲到列表时的位置”设置项。
### 其他
- 更新electron到v13.1.7
- 修复导入kg歌单最多只能加载100、500首歌曲的问题。注现在可以加载1000+首歌曲的歌单但出于未知原因会导致部分歌曲无法加载可能是无版权导致的目前酷狗码仍然最多只能加载500首歌
- 修复某些情况下所显示的歌词、封面图片与当前正在播放的歌曲不一致的问题

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.42',
version: '1.0.43',
player: {
togglePlayMethod: 'listLoop',
highQuality: false,
@ -85,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

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

@ -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)
})
// 由于集合安装包中不包含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,6 +18,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")
</template>
<script>
@ -25,7 +27,7 @@ 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
@ -61,6 +63,11 @@ export default {
message: 'initing',
apis: {},
},
sync: {
enable: false,
isShowSyncMode: false,
deviceName: '',
},
},
updateTimeout: null,
envParams: {
@ -103,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: {
@ -193,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',
@ -272,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)
@ -328,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()
})
@ -590,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()

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')
@ -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,6 +483,7 @@ export default {
},
async play() {
this.clearDelayNextTimeout()
this.updateMediaSessionInfo()
const targetSong = this.targetSong
@ -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 !== this.targetSong) return
this.musicInfo.img = targetSong.img
this.updateMediaSessionInfo()
})
}
},
setLrc(targetSong) {
this.getLrc(targetSong).then(({ lyric, tlyric, lxlyric }) => {
if (targetSong !== this.targetSong) 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

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

@ -130,6 +130,14 @@
"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_checking": "Checking for updates...",
"update_current_label": "Current version: ",

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

@ -130,6 +130,14 @@
"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_checking": "检查更新中...",
"update_current_label": "当前版本:",

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

@ -130,6 +130,14 @@
"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_checking": "檢查更新中...",
"update_current_label": "當前版本:",

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,11 +114,20 @@ 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
switch (this.state.setting.list.addMusicLocationType) {
switch (addMusicLocationType) {
case 'top':
targetList.list.unshift(musicInfo)
break
@ -104,11 +137,18 @@ const mutations = {
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) {
switch (this.state.setting.list.addMusicLocationType) {
@ -122,41 +162,79 @@ const mutations = {
}
}
},
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
switch (this.state.setting.list.addMusicLocationType) {
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]
break
}
let map = {}
let ids = []
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 = []
@ -164,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 = {
@ -196,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

@ -141,6 +141,7 @@ const getLyric = function(musicInfo, retryedSource = [], originMusic) {
})
}
let prevPlayIndex
// getters
const getters = {
list: state => state.listInfo.list,
@ -156,13 +157,14 @@ const getters = {
if (listId != '__temp__') {
if (isPlayList) {
playIndex = state.listInfo.list.indexOf(state.playMusicInfo.musicInfo)
playIndex = state.listInfo.list.findIndex(m => m.songmid == state.playMusicInfo.musicInfo.songmid)
if (!isTempPlay) listPlayIndex = playIndex
} else {
let list = window.allList[listId]
if (list) playIndex = list.list.indexOf(state.playMusicInfo.musicInfo)
if (list) playIndex = list.list.findIndex(m => m.songmid == state.playMusicInfo.musicInfo.songmid)
}
}
if (listPlayIndex > -1) prevPlayIndex = listPlayIndex
// console.log({
// listId,
// playIndex,
@ -170,7 +172,7 @@ const getters = {
// listPlayIndex,
// isPlayList,
// isTempPlay,
// musicInfo: state.playMusicInfo.musicInfo,
// // musicInfo: state.playMusicInfo.musicInfo,
// })
return {
listId,
@ -250,9 +252,9 @@ const actions = {
if (state.playedList.length) {
// 从已播放列表移除播放列表已删除的歌曲
let index
for (index = state.playedList.indexOf(state.playMusicInfo) - 1; index > -1; index--) {
for (index = state.playedList.findIndex(m => m.songmid === state.playMusicInfo.musicInfo.songmid) - 1; index > -1; index--) {
const playMusicInfo = state.playedList[index]
if (playMusicInfo.listId == currentListId && !currentList.includes(playMusicInfo.musicInfo)) {
if (playMusicInfo.listId == currentListId && !currentList.some(m => m.songmid === playMusicInfo.musicInfo.songmid)) {
commit('removePlayedList', index)
continue
}
@ -273,7 +275,19 @@ const actions = {
})
if (!filteredList.length) return commit('setPlayMusicInfo', null)
const playInfo = getters.playInfo
let currentIndex = filteredList.indexOf(currentList[playInfo.listPlayIndex])
let currentMusic
if (playInfo.listPlayIndex < 0) {
let index = prevPlayIndex
if (index > currentList.length - 1) index = 0
while (index > -1) {
currentMusic = currentList[index]
if (currentMusic) break
index--
}
} else {
currentMusic = currentList[playInfo.listPlayIndex]
}
let currentIndex = filteredList.findIndex(m => m.songmid == currentMusic.songmid)
if (currentIndex == -1) currentIndex = 0
let nextIndex = currentIndex
if (!playInfo.isTempPlay) {
@ -311,9 +325,9 @@ const actions = {
if (state.playedList.length) {
// 从已播放列表移除播放列表已删除的歌曲
let index
for (index = state.playedList.indexOf(state.playMusicInfo) + 1; index < state.playedList.length; index++) {
for (index = state.playedList.findIndex(m => m.songmid === state.playMusicInfo.musicInfo.songmid) + 1; index < state.playedList.length; index++) {
const playMusicInfo = state.playedList[index]
if (playMusicInfo.listId == currentListId && !currentList.includes(playMusicInfo.musicInfo)) {
if (playMusicInfo.listId == currentListId && !currentList.some(m => m.songmid === playMusicInfo.musicInfo.songmid)) {
commit('removePlayedList', index)
continue
}
@ -334,7 +348,19 @@ const actions = {
if (!filteredList.length) return commit('setPlayMusicInfo', null)
const playInfo = getters.playInfo
const currentIndex = filteredList.indexOf(currentList[playInfo.listPlayIndex])
let currentMusic
if (playInfo.listPlayIndex < 0) {
let index = prevPlayIndex - 1
if (index < 0) index = currentList.length - 1
while (index > -1) {
currentMusic = currentList[index]
if (currentMusic) break
index--
}
} else {
currentMusic = currentList[playInfo.listPlayIndex]
}
let currentIndex = filteredList.findIndex(m => m.songmid == currentMusic.songmid)
let nextIndex = currentIndex
switch (rootState.setting.player.togglePlayMethod) {
case 'listLoop':
@ -433,7 +459,7 @@ 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) playIndex = state.listInfo.list.findIndex(m => m.songmid == playMusicInfo.musicInfo.songmid)
}
state.playMusicInfo = playMusicInfo

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

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

@ -150,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')}}
@ -295,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'
@ -410,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 {
@ -474,6 +504,10 @@ export default {
isToTray: false,
themeId: 0,
},
sync: {
enable: false,
port: '23332',
},
windowSizeId: 1,
langId: 'cns',
themeId: 0,
@ -608,6 +642,15 @@ export default {
},
isDisabledResourceCacheClear: false,
isDisabledListCacheClear: false,
sync: {
status: {
status: false,
message: '',
address: [],
code: '',
devices: [],
},
},
}
},
watch: {
@ -665,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
},
@ -684,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
@ -1151,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>