Merge branch 'lyswhut:dev' into dev

This commit is contained in:
Folltoshe 2023-04-02 00:07:28 +08:00 committed by GitHub
commit 19549209d0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 1308 additions and 1317 deletions

View File

@ -6,6 +6,41 @@ 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/).
## [2.2.0](https://github.com/lyswhut/lx-music-desktop/compare/v2.1.2...v2.2.0) - 2023-03-26
从v2.2.0起,我们发布了一个独立版的[数据同步服务](https://github.com/lyswhut/lx-music-sync-server#readme),如果你有服务器,可以将其部署到服务器上作为私人多端同步服务使用,详情看该项目说明
### 不兼容性变更说明
- 同步功能从这个版本起数据同步功能至少需要移动端v1.0.0的版本才能连接,连接的地址格式也略有改变,详情看[文档说明](https://lyswhut.github.io/lx-music-doc/desktop/faq/sync)
### 新增
- 重构数据同步功能,新增客户端模式
- 新增全屏时自动关闭歌词设置,默认开启,可以去设置-桌面歌词设置更改
- 新增设置-桌面歌词设置-重置窗口设置功能,点击时会重置桌面歌词窗口大小及位置
- 新增设置-其他-列表数据清理功能,点击时会清空已创建的所有列表及所有收藏的歌曲
### 优化
- 支持wy源flac hires歌曲类型的显示
- 快捷键调整音量时每次加减2%音量改为4%#1220
- 音量、播放模式等设置弹出式按钮在鼠标移到按钮上时将自动弹出设置内容,保留点击切换显示/隐藏
- 支持kg源搜索列表、排行榜flac hires歌曲类型的显示#1231, #1238 By @helloplhm-qwq, @Folltoshe
- 播放速率的粒度调整为0.01范围0.6-2.0x
### 修复
- 修复同步连接的处理问题
- 修复记住播放进度的情况下使用Scheme URL打开应用播放的歌曲进度没有被重置的问题
- 修复使用酷狗码无法打开某些类型的歌单的问题
- 修复tx源某些歌单因为歌曲信息缺失导致打开失败的问题
- 修复连续选择时的初始选择歌曲位置被意外改变的问题
### 其他
- 更新 Electron 到v22.3.4
## [2.1.2](https://github.com/lyswhut/lx-music-desktop/compare/v2.1.1...v2.1.2) - 2023-02-18

View File

@ -104,7 +104,7 @@ module.exports = {
],
},
{
test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)$/,
type: 'asset',
parser: {
dataUrlCondition: {

2030
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": "2.2.0-beta.6",
"version": "2.2.1-beta",
"description": "一个免费的音乐查找助手",
"main": "./dist/main.js",
"productName": "lx-music-desktop",
@ -205,8 +205,8 @@
},
"homepage": "https://github.com/lyswhut/lx-music-desktop#readme",
"devDependencies": {
"@babel/core": "^7.21.0",
"@babel/eslint-parser": "^7.19.1",
"@babel/core": "^7.21.3",
"@babel/eslint-parser": "^7.21.3",
"@babel/plugin-proposal-class-properties": "^7.18.6",
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/plugin-transform-modules-umd": "^7.18.6",
@ -216,60 +216,60 @@
"@types/better-sqlite3": "^7.6.3",
"@types/needle": "^3.2.0",
"@types/tunnel": "^0.0.3",
"@typescript-eslint/eslint-plugin": "^5.54.1",
"@typescript-eslint/parser": "^5.54.1",
"@typescript-eslint/eslint-plugin": "^5.57.0",
"@typescript-eslint/parser": "^5.57.0",
"@volar/vue-language-plugin-pug": "^1.2.0",
"babel-loader": "^9.1.2",
"browserslist": "^4.21.5",
"chalk": "^4.1.2",
"changelog-parser": "^3.0.1",
"copy-webpack-plugin": "^11.0.0",
"core-js": "^3.29.0",
"core-js": "^3.29.1",
"cross-env": "^7.0.3",
"css-loader": "^6.7.3",
"css-minimizer-webpack-plugin": "^4.2.2",
"css-minimizer-webpack-plugin": "^5.0.0",
"del": "^6.1.1",
"electron": "^22.3.2",
"electron-builder": "^24.0.0",
"electron": "^22.3.5",
"electron-builder": "^24.1.2",
"electron-debug": "^3.2.0",
"electron-devtools-installer": "^3.2.0",
"electron-to-chromium": "^1.4.328",
"electron-updater": "^6.0.0",
"eslint": "^8.36.0",
"electron-to-chromium": "^1.4.345",
"electron-updater": "^6.0.2",
"eslint": "^8.37.0",
"eslint-config-standard": "^17.0.0",
"eslint-config-standard-with-typescript": "^34.0.0",
"eslint-config-standard-with-typescript": "^34.0.1",
"eslint-formatter-friendly": "github:lyswhut/eslint-friendly-formatter#2170d1320e2fad13615a9dcf229669f0bb473a53",
"eslint-plugin-html": "^7.1.0",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-n": "^15.6.1",
"eslint-plugin-n": "^15.7.0",
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-vue": "^9.9.0",
"eslint-plugin-vue": "^9.10.0",
"eslint-webpack-plugin": "^4.0.0",
"html-webpack-plugin": "^5.5.0",
"less": "^4.1.3",
"less-loader": "^11.1.0",
"mini-css-extract-plugin": "^2.7.3",
"mini-css-extract-plugin": "^2.7.5",
"node-loader": "^2.0.0",
"postcss": "^8.4.21",
"postcss-loader": "^7.0.2",
"postcss-loader": "^7.1.0",
"postcss-pxtorem": "^6.0.0",
"pug": "^3.0.2",
"pug-plain-loader": "^1.1.0",
"rimraf": "^4.4.0",
"rimraf": "^4.4.1",
"spinnies": "github:lyswhut/spinnies#233305c58694aa3b053e3ab9af9049993f918b9d",
"svg-sprite-loader": "^6.0.11",
"svg-transform-loader": "^2.0.13",
"svgo-loader": "^4.0.0",
"terser": "^5.16.6",
"terser": "^5.16.8",
"terser-webpack-plugin": "^5.3.7",
"ts-loader": "^9.4.2",
"typescript": "^4.9.5",
"vue-eslint-parser": "^9.1.0",
"typescript": "^5.0.2",
"vue-eslint-parser": "^9.1.1",
"vue-loader": "^17.0.1",
"vue-template-compiler": "^2.7.14",
"webpack": "^5.76.1",
"webpack": "^5.77.0",
"webpack-cli": "^5.0.1",
"webpack-dev-server": "^4.11.1",
"webpack-dev-server": "^4.13.1",
"webpack-hot-middleware": "github:lyswhut/webpack-hot-middleware#329c4375134b89d39da23a56a94db651247c74a1",
"webpack-merge": "^5.8.0"
},
@ -287,7 +287,7 @@
"jschardet": "^3.0.0",
"koa": "^2.14.1",
"long": "^5.2.1",
"music-metadata": "^8.1.3",
"music-metadata": "^8.1.4",
"needle": "github:lyswhut/needle#93299ac841b7e9a9f82ca7279b88aaaeda404060",
"node-id3": "^0.2.6",
"sortablejs": "^1.15.0",

View File

@ -1,30 +1,12 @@
从v2.2.0起,我们发布了一个独立版的[数据同步服务](https://github.com/lyswhut/lx-music-sync-server#readme),如果你有服务器,可以将其部署到服务器上作为私人多端同步服务使用,详情看该项目说明
### 不兼容性变更说明
- 同步功能从这个版本起数据同步功能至少需要移动端v1.0.0的版本才能连接
### 新增
- 重构数据同步功能,新增客户端模式
- 新增全屏时自动关闭歌词设置,默认开启,可以去设置-桌面歌词设置更改
- 新增设置-桌面歌词设置-重置窗口设置功能,点击时会重置桌面歌词窗口大小及位置
- 新增设置-其他-列表数据清理功能,点击时会清空已创建的所有列表及所有收藏的歌曲
### 优化
- 支持wy源flac hires歌曲类型的显示
- 快捷键调整音量时每次加减2%音量改为4%#1220
- 音量、播放模式等设置弹出式按钮在鼠标移到按钮上时将自动弹出设置内容,保留点击切换显示/隐藏
- 支持kg源搜索列表、排行榜flac hires歌曲类型的显示#1231, #1238 By @helloplhm-qwq, @Folltoshe
- 优化对系统Media Session的支持现在切歌不会再会导致信息丢失的问题了
- 启用桌面歌词时,取消对歌词窗口的聚焦
### 修复
- 修复同步连接的处理问题
- 修复记住播放进度的情况下使用Scheme URL打开应用播放的歌曲进度没有被重置的问题
- 修复使用酷狗码无法打开某些类型的歌单的问题
- 修复tx源某些歌单因为歌曲信息缺失导致打开失败的问题
- 修复启用全局快捷键时与Media Session注册冲突的问题启用全局快捷键时不再注册媒体控制快捷键
### 其他
- 更新 Electron 到v22.3.2
- 更新 electron 到 v22.3.5

File diff suppressed because one or more lines are too long

View File

@ -29,21 +29,21 @@ const local: LX.HotKeyConfig = {
const global: LX.HotKeyConfig = {
enable: false,
keys: {
MediaPlayPause: {
type: HOTKEY_PLAYER.toggle_play.type,
name: '',
action: HOTKEY_PLAYER.toggle_play.action,
},
MediaPreviousTrack: {
type: HOTKEY_PLAYER.prev.type,
name: '',
action: HOTKEY_PLAYER.prev.action,
},
MediaNextTrack: {
type: HOTKEY_PLAYER.next.type,
name: '',
action: HOTKEY_PLAYER.next.action,
},
// MediaPlayPause: {
// type: HOTKEY_PLAYER.toggle_play.type,
// name: '',
// action: HOTKEY_PLAYER.toggle_play.action,
// },
// MediaPreviousTrack: {
// type: HOTKEY_PLAYER.prev.type,
// name: '',
// action: HOTKEY_PLAYER.prev.action,
// },
// MediaNextTrack: {
// type: HOTKEY_PLAYER.next.type,
// name: '',
// action: HOTKEY_PLAYER.next.action,
// },
'mod+alt+f5': {
type: HOTKEY_PLAYER.toggle_play.type,
name: HOTKEY_PLAYER.toggle_play.name,

View File

@ -1,6 +1,8 @@
import { join } from 'path'
import { homedir } from 'os'
const isMac = process.platform == 'darwin'
const defaultSetting: LX.AppSetting = {
version: '2.1.0',
@ -14,7 +16,7 @@ const defaultSetting: LX.AppSetting = {
'common.isShowAnimation': true,
'common.randomAnimate': true,
'common.isAgreePact': false,
'common.controlBtnPosition': process.platform === 'darwin' ? 'left' : 'right',
'common.controlBtnPosition': isMac ? 'left' : 'right',
'common.playBarProgressStyle': 'mini',
'common.tryAutoUpdate': true,
'common.showChangeLog': true,
@ -31,7 +33,7 @@ const defaultSetting: LX.AppSetting = {
'player.isShowLyricTranslation': false,
'player.isShowLyricRoma': false,
'player.isS2t': false,
'player.isPlayLxlrc': false,
'player.isPlayLxlrc': !isMac,
'player.isSavePlayTime': false,
'player.audioVisualization': false,
'player.waitPlayEndStop': true,

View File

@ -310,10 +310,12 @@
"setting__desktop_lyric_always_on_top": "Make the lyrics always above other windows",
"setting__desktop_lyric_always_on_top_loop": "Automatically refresh the top of the lyrics (try to enable this setting when the lyrics are still blocked by some programs)",
"setting__desktop_lyric_audio_visualization": "Audio Visualization (Experimental)",
"setting__desktop_lyric_color": "Lyric font color",
"setting__desktop_lyric_color_reset": "Reset color",
"setting__desktop_lyric_delay_scroll": "Delayed lyrics scroll",
"setting__desktop_lyric_direction_horizontal": "horizontal direction",
"setting__desktop_lyric_direction_vertical": "vertical direction",
"setting__desktop_lyric_direction": "Lyrics Display Direction",
"setting__desktop_lyric_direction_horizontal": "Horizontal direction",
"setting__desktop_lyric_direction_vertical": "Vertical direction",
"setting__desktop_lyric_ellipsis": "Lyrics are not allowed to wrap",
"setting__desktop_lyric_enable": "Display lyrics",
"setting__desktop_lyric_font": "Lyric font",
@ -329,7 +331,7 @@
"setting__desktop_lyric_played_color": "color played",
"setting__desktop_lyric_reset": "Reset",
"setting__desktop_lyric_reset_window": "Reset window settings",
"setting__desktop_lyric_scroll_align": "now playing lyrics scroll position",
"setting__desktop_lyric_scroll_align": "Now playing lyrics scroll position",
"setting__desktop_lyric_scroll_align_center": "Center",
"setting__desktop_lyric_scroll_align_top": "Top",
"setting__desktop_lyric_shadow_color": "Shadow color",
@ -339,6 +341,7 @@
"setting__download_data_embed": "Whether to embed the following content in the audio file",
"setting__download_embed_lyric": "Embedding lyric",
"setting__download_embed_pic": "Embedding cover",
"setting__download_embed_rlyric": "Also embed Roman accent lyrics (if available)",
"setting__download_embed_tlyric": "Also embed translated lyrics (if available)",
"setting__download_enable": "Whether to enable download function",
"setting__download_lyric": "Lyrics download",
@ -462,7 +465,7 @@
"setting__sync": "Data synchronization",
"setting__sync_client_address": "Current device address: {address}",
"setting__sync_client_host": "Synchronization service address",
"setting__sync_client_host_tip": "Please enter the synchronization service address",
"setting__sync_client_host_tip": "http://IP:Port",
"setting__sync_client_mode": "client mode",
"setting__sync_client_status": "Status: {status}",
"setting__sync_code_blocked_ip": "The IP of the current device has been blocked by the server!",

View File

@ -465,7 +465,7 @@
"setting__sync": "数据同步",
"setting__sync_client_address": "当前设备地址:{address}",
"setting__sync_client_host": "同步服务地址",
"setting__sync_client_host_tip": "请输入同步服务地址",
"setting__sync_client_host_tip": "http://IP地址:端口号",
"setting__sync_client_mode": "客户端模式",
"setting__sync_client_status": "状态:{status}",
"setting__sync_code_blocked_ip": "当前设备的IP已被服务端封禁",

View File

@ -310,6 +310,7 @@
"setting__desktop_lyric_always_on_top": "使歌詞總是在其他窗口之上",
"setting__desktop_lyric_always_on_top_loop": "自動刷新歌詞置頂(當歌詞置頂後仍被某些程序遮擋時可嘗試啟用此設置)",
"setting__desktop_lyric_audio_visualization": "音頻可視化(實驗性)",
"setting__desktop_lyric_color": "歌詞字體顏色",
"setting__desktop_lyric_color_reset": "重置顏色",
"setting__desktop_lyric_delay_scroll": "延遲歌詞滾動",
"setting__desktop_lyric_direction": "歌詞顯示方向",
@ -340,6 +341,7 @@
"setting__download_data_embed": "是否將以下內容嵌入到音頻文件中",
"setting__download_embed_lyric": "歌詞嵌入",
"setting__download_embed_pic": "封面嵌入",
"setting__download_embed_rlyric": "同時嵌入羅馬音歌詞(如果有)",
"setting__download_embed_tlyric": "同時嵌入翻譯歌詞(如果有)",
"setting__download_enable": "是否啟用下載功能",
"setting__download_lyric": "歌詞下載",
@ -463,7 +465,7 @@
"setting__sync": "數據同步",
"setting__sync_client_address": "當前設備地址:{address}",
"setting__sync_client_host": "同步服務地址",
"setting__sync_client_host_tip": "請輸入同步服務地址",
"setting__sync_client_host_tip": "http://IP地址:端口號",
"setting__sync_client_mode": "客戶端模式",
"setting__sync_client_status": "狀態:{status}",
"setting__sync_code_blocked_ip": "當前設備的IP已被服務端封禁",

View File

@ -61,6 +61,7 @@ const codeAuth = async(urlInfo: LX.Sync.Client.UrlInfo, serverId: string, authCo
const keyAuth = async(urlInfo: LX.Sync.Client.UrlInfo, keyInfo: LX.Sync.ClientKeyInfo) => {
const msg = aesEncrypt(SYNC_CODE.authMsg + getComputerName(), keyInfo.key)
// eslint-disable-next-line @typescript-eslint/promise-function-async
return request(`${urlInfo.httpProtocol}//${urlInfo.hostPath}/ah`, { headers: { i: keyInfo.clientId, m: msg } }).then(({ text, code }) => {
if (code != 200) throw new Error(SYNC_CODE.authFailed)

View File

@ -7,94 +7,114 @@ import { aesDecrypt, aesEncrypt, getComputerName, rsaEncrypt } from '../utils'
const requestIps = new Map<string, number>()
const getAvailableIP = (req: http.IncomingMessage) => {
let ip = getIP(req)
return ip && (requestIps.get(ip) ?? 0) < 10 ? ip : null
}
const verifyByKey = (encryptMsg: string, userId: string) => {
const keyInfo = getClientKeyInfo(userId)
if (!keyInfo) return null
let text
try {
text = aesDecrypt(encryptMsg, keyInfo.key)
} catch (err) {
return null
}
// console.log(text)
if (text.startsWith(SYNC_CODE.authMsg)) {
const deviceName = text.replace(SYNC_CODE.authMsg, '') || 'Unknown'
if (deviceName != keyInfo.deviceName) {
keyInfo.deviceName = deviceName
saveClientKeyInfo(keyInfo)
}
return aesEncrypt(SYNC_CODE.helloMsg, keyInfo.key)
}
return null
}
const verifyByCode = (encryptMsg: string, password: string) => {
let key = ''.padStart(16, Buffer.from(password).toString('hex'))
// const iv = Buffer.from(key.split('').reverse().join('')).toString('base64')
key = Buffer.from(key).toString('base64')
// console.log(req.headers.m, authCode, key)
let text
try {
text = aesDecrypt(encryptMsg, key)
} catch (err) {
return null
}
// console.log(text)
if (text.startsWith(SYNC_CODE.authMsg)) {
const data = text.split('\n')
const publicKey = `-----BEGIN PUBLIC KEY-----\n${data[1]}\n-----END PUBLIC KEY-----`
const deviceName = data[2] || 'Unknown'
const isMobile = data[3] == 'lx_music_mobile'
const keyInfo = createClientKeyInfo(deviceName, isMobile)
return rsaEncrypt(Buffer.from(JSON.stringify({
clientId: keyInfo.clientId,
key: keyInfo.key,
serverName: getComputerName(),
})), publicKey)
}
return null
}
export const authCode = async(req: http.IncomingMessage, res: http.ServerResponse, password: string) => {
let code = 401
let msg: string = SYNC_CODE.msgAuthFailed
let ip = getIP(req)
// console.log(req.headers)
if (typeof req.headers.m == 'string') {
if (ip && (requestIps.get(ip) ?? 0) < 10) {
if (req.headers.m) {
label:
if (req.headers.i) { // key验证
if (typeof req.headers.i != 'string') break label
const keyInfo = getClientKeyInfo(req.headers.i)
if (!keyInfo) break label
let text
try {
text = aesDecrypt(req.headers.m, keyInfo.key)
} catch (err) {
break label
}
// console.log(text)
if (text.startsWith(SYNC_CODE.authMsg)) {
code = 200
const deviceName = text.replace(SYNC_CODE.authMsg, '') || 'Unknown'
if (deviceName != keyInfo.deviceName) {
keyInfo.deviceName = deviceName
saveClientKeyInfo(keyInfo)
}
msg = aesEncrypt(SYNC_CODE.helloMsg, keyInfo.key)
}
} else { // 连接码验证
let key = ''.padStart(16, Buffer.from(password).toString('hex'))
// const iv = Buffer.from(key.split('').reverse().join('')).toString('base64')
key = Buffer.from(key).toString('base64')
// console.log(req.headers.m, authCode, key)
let text
try {
text = aesDecrypt(req.headers.m, key)
} catch (err) {
break label
}
// console.log(text)
if (text.startsWith(SYNC_CODE.authMsg)) {
code = 200
const data = text.split('\n')
const publicKey = `-----BEGIN PUBLIC KEY-----\n${data[1]}\n-----END PUBLIC KEY-----`
const deviceName = data[2] || 'Unknown'
const isMobile = data[3] == 'lx_music_mobile'
const keyInfo = createClientKeyInfo(deviceName, isMobile)
msg = rsaEncrypt(Buffer.from(JSON.stringify({
clientId: keyInfo.clientId,
key: keyInfo.key,
serverName: getComputerName(),
})), publicKey)
}
}
let ip = getAvailableIP(req)
if (ip) {
if (typeof req.headers.m == 'string' && req.headers.m) {
const userId = req.headers.i
const _msg = typeof userId == 'string' && userId
? verifyByKey(req.headers.m, userId)
: verifyByCode(req.headers.m, password)
if (_msg != null) {
msg = _msg
code = 200
}
} else {
code = 403
msg = SYNC_CODE.msgBlockedIp
}
if (code != 200) {
const num = requestIps.get(ip) ?? 0
// if (num > 20) return
requestIps.set(ip, num + 1)
}
} else {
code = 403
msg = SYNC_CODE.msgBlockedIp
}
res.writeHead(code)
res.end(msg)
if (ip && code != 200) {
const num = requestIps.get(ip) ?? 0
if (num > 20) return
requestIps.set(ip, num + 1)
}
}
const verifyConnection = (encryptMsg: string, userId: string) => {
const keyInfo = getClientKeyInfo(userId)
if (!keyInfo) return false
let text
try {
text = aesDecrypt(encryptMsg, keyInfo.key)
} catch (err) {
return false
}
// console.log(text)
return text == SYNC_CODE.msgConnect
}
export const authConnect = async(req: http.IncomingMessage) => {
const query = querystring.parse((req.url as string).split('?')[1])
const i = query.i
const t = query.t
label:
if (typeof i == 'string' && typeof t == 'string') {
const keyInfo = getClientKeyInfo(i)
if (!keyInfo) break label
let text
try {
text = aesDecrypt(t, keyInfo.key)
} catch (err) {
break label
}
// console.log(text)
if (text == SYNC_CODE.msgConnect) return
let ip = getAvailableIP(req)
if (ip) {
const query = querystring.parse((req.url as string).split('?')[1])
const i = query.i
const t = query.t
if (typeof i == 'string' && typeof t == 'string' && verifyConnection(t, i)) return
const num = requestIps.get(ip) ?? 0
requestIps.set(ip, num + 1)
}
throw new Error('failed')
}

View File

@ -40,16 +40,18 @@ const codeTools: {
},
}
const checkDuplicateClient = (newSocket: LX.Sync.Server.Socket) => {
for (const client of [...wss!.clients]) {
if (client === newSocket || client.keyInfo.clientId != newSocket.keyInfo.clientId) continue
console.log('duplicate client', client.keyInfo.deviceName)
client.isReady = false
client.close(SYNC_CLOSE_CODE.normal)
}
}
const handleConnection = async(socket: LX.Sync.Server.Socket, request: IncomingMessage) => {
const queryData = url.parse(request.url as string, true).query as Record<string, string>
socket.onClose(() => {
// console.log('disconnect', reason)
status.devices.splice(status.devices.findIndex(k => k.clientId == keyInfo?.clientId), 1)
sendServerStatus(status)
})
// // if (typeof socket.handshake.query.i != 'string') return socket.disconnect(true)
const keyInfo = getClientKeyInfo(queryData.i)
if (!keyInfo) {
@ -60,6 +62,8 @@ const handleConnection = async(socket: LX.Sync.Server.Socket, request: IncomingM
saveClientKeyInfo(keyInfo)
// // socket.lx_keyInfo = keyInfo
socket.keyInfo = keyInfo
checkDuplicateClient(socket)
try {
await syncList(wss as LX.Sync.Server.SocketServer, socket)
} catch (err) {
@ -68,6 +72,11 @@ const handleConnection = async(socket: LX.Sync.Server.Socket, request: IncomingM
return
}
status.devices.push(keyInfo)
socket.onClose(() => {
// console.log('disconnect', reason)
status.devices.splice(status.devices.findIndex(k => k.clientId == keyInfo?.clientId), 1)
sendServerStatus(status)
})
// handleConnection(io, socket)
sendServerStatus(status)
@ -216,15 +225,15 @@ const handleStartServer = async(port = 9527, ip = '0.0.0.0') => await new Promis
})
const interval = setInterval(() => {
wss?.clients.forEach(ws => {
if (ws.isAlive == false) {
ws.terminate()
wss?.clients.forEach(socket => {
if (socket.isAlive == false) {
socket.terminate()
return
}
ws.isAlive = false
ws.ping(noop)
if (ws.keyInfo.isMobile) ws.send('ping', noop)
socket.isAlive = false
socket.ping(noop)
if (socket.keyInfo.isMobile) socket.send('ping', noop)
})
}, 30000)

View File

@ -81,6 +81,7 @@ const winEvent = () => {
// browserWindow!.setAlwaysOnTop(global.lx.appSetting['desktopLyric.isAlwaysOnTop'], 'screen-saver')
// }
if (global.lx.appSetting['desktopLyric.isAlwaysOnTop'] && global.lx.appSetting['desktopLyric.isAlwaysOnTopLoop']) alwaysOnTopTools.startLoop()
browserWindow!.blur()
})
}

View File

@ -154,7 +154,15 @@ export const initHotKey = async() => {
let localConfig = electronStore_hotKey.get('local') as LX.HotKeyConfig | null
let globalConfig = electronStore_hotKey.get('global') as LX.HotKeyConfig | null
if (!localConfig) {
if (globalConfig) {
// 移除v2.2.0及之前设置的全局媒体快捷键注册
if (globalConfig.keys.MediaPlayPause) {
delete globalConfig.keys.MediaPlayPause
delete globalConfig.keys.MediaNextTrack
delete globalConfig.keys.MediaPreviousTrack
electronStore_hotKey.set('global', globalConfig)
}
} else {
// migrate hotKey
const config = await migrateHotKey()
if (config) {

View File

@ -1,11 +1,11 @@
<html lang="cn">
<!DOCTYPE html>
<html lang="en" style="background-color: transparent;">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<meta http-equiv="X-UA-Compatible" content="ie=edge"/>
<title>桌面歌词-洛雪音乐助手</title>
</head>
<body>
<body id="body" style="background-color: transparent;">
<div id="root"></div>
<script>
window.dom_style_theme = document.createElement('style')

Binary file not shown.

View File

@ -8,10 +8,10 @@
<template #content>
<div :class="$style.setting">
<div :class="$style.info">
<span>{{ playbackRate }}x</span>
<base-btn min @click="handleUpdatePlaybackRate(1)">{{ $t('player__playback_rate_reset_btn') }}</base-btn>
<span>{{ playbackRate.toFixed(2) }}x</span>
<base-btn min @click="handleUpdatePlaybackRate(100)">{{ $t('player__playback_rate_reset_btn') }}</base-btn>
</div>
<base-slider-bar :value="playbackRate" :min="0.5" :max="2" @change="handleUpdatePlaybackRate" />
<base-slider-bar :class="$style.slider" :value="playbackRate * 100" :min="60" :max="200" @change="handleUpdatePlaybackRate" />
</div>
</template>
</material-popup-btn>
@ -22,7 +22,7 @@
import { playbackRate } from '@renderer/store/player/playbackRate'
const handleUpdatePlaybackRate = (val) => {
window.app_event.setPlaybackRate(Math.round(val * 10) / 10)
window.app_event.setPlaybackRate(Math.round(val) / 100)
}
// const icon = computed(() => {
@ -87,6 +87,7 @@ const handleUpdatePlaybackRate = (val) => {
flex-flow: column nowrap;
padding: 2px 3px;
gap: 8px;
width: 300px;
}
.info {
@ -100,5 +101,9 @@ const handleUpdatePlaybackRate = (val) => {
}
}
.slider {
width: 100%;
}
</style>

View File

@ -16,7 +16,7 @@
@update:model-value="saveVolumeIsMute($event)"
/>
</div>
<base-slider-bar :value="volume" :min="0" :max="1" @change="handleUpdateVolume" />
<base-slider-bar :class="$style.slider" :value="volume" :min="0" :max="1" @change="handleUpdateVolume" />
</div>
</template>
</material-popup-btn>
@ -91,6 +91,7 @@ const icon = computed(() => {
flex-flow: column nowrap;
padding: 2px 3px;
gap: 8px;
width: 140px;
}
.info {
@ -104,5 +105,8 @@ const icon = computed(() => {
}
}
.slider {
width: 100%;
}
</style>

View File

@ -17,7 +17,7 @@
</template>
<script>
import { ref, onMounted } from '@common/utils/vueTools'
import { ref } from '@common/utils/vueTools'
import usePlayProgress from '@renderer/utils/compositions/usePlayProgress'
import { isShowPlayerDetail } from '@renderer/store/player/state'
@ -69,12 +69,12 @@ export default {
visibleProgress.value = false
}
onMounted(() => {
visible.value = true
requestAnimationFrame(() => {
visible.value = false
})
})
// onMounted(() => {
// visible.value = true
// requestAnimationFrame(() => {
// visible.value = false
// })
// })
return {
visible,

View File

@ -74,13 +74,14 @@ export default ({ props }: {
removeAllSelect()
if (lastSelectIndex != clickIndex) {
let isNeedReverse = false
if (clickIndex < lastSelectIndex) {
let temp = lastSelectIndex
lastSelectIndex = clickIndex
let _lastSelectIndex = lastSelectIndex
if (clickIndex < _lastSelectIndex) {
let temp = _lastSelectIndex
_lastSelectIndex = clickIndex
clickIndex = temp
isNeedReverse = true
}
selectedList.value = props.list.slice(lastSelectIndex, clickIndex + 1)
selectedList.value = props.list.slice(_lastSelectIndex, clickIndex + 1)
if (isNeedReverse) selectedList.value.reverse()
}
} else {

View File

@ -358,6 +358,7 @@ export const getOnlineOtherSourceLyricInfo = async({ musicInfos, onToggleSource,
reqPromise = Promise.reject(err)
}
retryedSource.includes(musicInfo.source)
// eslint-disable-next-line @typescript-eslint/promise-function-async
return reqPromise.then((lyricInfo: LX.Music.LyricInfo) => {
return existTimeExp.test(lyricInfo.lyric) ? {
lyricInfo,
@ -392,6 +393,7 @@ export const handleGetOnlineLyricInfo = async({ musicInfo, onToggleSource, isRef
} catch (err) {
reqPromise = Promise.reject(err)
}
// eslint-disable-next-line @typescript-eslint/promise-function-async
return reqPromise.then((lyricInfo: LX.Music.LyricInfo) => {
return existTimeExp.test(lyricInfo.lyric) ? {
musicInfo,

View File

@ -72,6 +72,7 @@ const getMusicPlayUrl = async(musicInfo: LX.Music.MusicInfo | LX.Download.ListIt
if (window.lx.isPlayedStop || diffCurrentMusicInfo(musicInfo)) return null
return url
// eslint-disable-next-line @typescript-eslint/promise-function-async
}).catch(err => {
// console.log('err', err.message)
if (window.lx.isPlayedStop ||
@ -195,6 +196,7 @@ const handlePlay = () => {
*/
export const playList = (listId: string, index: number) => {
setPlayListId(listId)
pause()
setPlayMusicInfo(listId, getList(listId)[index])
clearPlayedList()
clearTempPlayeList()
@ -217,6 +219,7 @@ export const playNext = async(isAutoToggle = false): Promise<void> => {
if (tempPlayList.length) { // 如果稍后播放列表存在歌曲则直接播放改列表的歌曲
const playMusicInfo = tempPlayList[0]
removeTempPlayList(0)
pause()
setPlayMusicInfo(playMusicInfo.listId, playMusicInfo.musicInfo, playMusicInfo.isTempPlay)
handlePlay()
return
@ -257,6 +260,7 @@ export const playNext = async(isAutoToggle = false): Promise<void> => {
if (index < playedList.length) {
const playMusicInfo = playedList[index]
pause()
setPlayMusicInfo(playMusicInfo.listId, playMusicInfo.musicInfo, playMusicInfo.isTempPlay)
handlePlay()
return
@ -311,6 +315,7 @@ export const playNext = async(isAutoToggle = false): Promise<void> => {
isTempPlay: false,
}
pause()
setPlayMusicInfo(nextPlayMusicInfo.listId, nextPlayMusicInfo.musicInfo)
handlePlay()
}
@ -353,6 +358,7 @@ export const playPrev = async(isAutoToggle = false): Promise<void> => {
if (index > -1) {
const playMusicInfo = playedList[index]
pause()
setPlayMusicInfo(playMusicInfo.listId, playMusicInfo.musicInfo, playMusicInfo.isTempPlay)
handlePlay()
return
@ -407,6 +413,7 @@ export const playPrev = async(isAutoToggle = false): Promise<void> => {
isTempPlay: false,
}
pause()
setPlayMusicInfo(nextPlayMusicInfo.listId, nextPlayMusicInfo.musicInfo)
handlePlay()
}

View File

@ -42,6 +42,7 @@ export default () => {
musicInfo: songInfo,
},
},
// eslint-disable-next-line @typescript-eslint/promise-function-async
}).then(res => {
// console.log(res)
if (!/^https?:/.test(res.data.url)) return Promise.reject(new Error('Get url failed'))

View File

@ -1,19 +1,44 @@
import { onBeforeUnmount } from '@common/utils/vueTools'
import { getDuration, getPlaybackRate, getCurrentTime } from '@renderer/plugins/player'
import { musicInfo } from '@renderer/store/player/state'
import { isPlay, musicInfo, playMusicInfo } from '@renderer/store/player/state'
import { playProgress } from '@renderer/store/player/playProgress'
import { playNext, playPrev, stop } from '@renderer/core/player'
// import { } from ''
import { pause, play, playNext, playPrev, stop } from '@renderer/core/player'
export default () => {
// 创建一个空白音频以保持对 Media Session 的注册
const emptyAudio = new Audio()
emptyAudio.autoplay = false
emptyAudio.src = require('@renderer/assets/medias/Silence02s.mp3')
emptyAudio.controls = false
emptyAudio.preload = 'auto'
emptyAudio.onplaying = () => {
emptyAudio.pause()
}
void emptyAudio.play()
let prevPicUrl = ''
const updateMediaSessionInfo = () => {
if (musicInfo.id == null) {
navigator.mediaSession.metadata = null
return
}
const mediaMetadata: MediaMetadata = {
title: musicInfo.name,
artist: musicInfo.singer,
album: musicInfo.album,
artwork: [],
}
if (musicInfo.pic) mediaMetadata.artwork = [{ src: musicInfo.pic }]
if (musicInfo.pic) {
const pic = new Image()
pic.src = prevPicUrl = musicInfo.pic
pic.onload = () => {
if (prevPicUrl == pic.src) {
mediaMetadata.artwork = [{ src: pic.src }]
// @ts-expect-error
navigator.mediaSession.metadata = new window.MediaMetadata(mediaMetadata)
}
}
} else prevPicUrl = ''
// @ts-expect-error
navigator.mediaSession.metadata = new window.MediaMetadata(mediaMetadata)
@ -48,25 +73,27 @@ export default () => {
navigator.mediaSession.playbackState = 'none'
}
const handleSetPlayInfo = () => {
updateMediaSessionInfo()
updatePositionState({
position: playProgress.nowPlayTime,
duration: playProgress.maxPlayTime,
emptyAudio.play().finally(() => {
updateMediaSessionInfo()
updatePositionState({
position: playProgress.nowPlayTime,
duration: playProgress.maxPlayTime,
})
handlePause()
})
handlePause()
}
// const 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('play', () => {
if (isPlay.value || !playMusicInfo) return
console.log('play')
play()
})
navigator.mediaSession.setActionHandler('pause', () => {
if (!isPlay.value || !playMusicInfo) return
console.log('pause')
pause()
})
navigator.mediaSession.setActionHandler('stop', () => {
console.log('stop')
setStop()
@ -107,6 +134,8 @@ export default () => {
window.app_event.on('pause', handlePause)
window.app_event.on('stop', handleStop)
window.app_event.on('error', handlePause)
window.app_event.on('playerEmptied', handleSetPlayInfo)
// window.app_event.on('playerLoadstart', handleSetPlayInfo)
window.app_event.on('musicToggled', handleSetPlayInfo)
window.app_event.on('picUpdated', updateMediaSessionInfo)
@ -117,6 +146,8 @@ export default () => {
window.app_event.off('pause', handlePause)
window.app_event.off('stop', handleStop)
window.app_event.off('error', handlePause)
window.app_event.off('playerEmptied', handleSetPlayInfo)
// window.app_event.off('playerLoadstart', handleSetPlayInfo)
window.app_event.off('musicToggled', handleSetPlayInfo)
window.app_event.off('picUpdated', updateMediaSessionInfo)
})

View File

@ -95,6 +95,7 @@ export default () => {
versionInfoPromise = handleGetVersionInfo()
}
} else versionInfoPromise = handleGetVersionInfo()
// eslint-disable-next-line @typescript-eslint/promise-function-async
void versionInfoPromise.then((result) => {
versionInfo.reCheck = false

View File

@ -14,12 +14,13 @@ export const setListDetail = (result: ListDetailInfo, id: string, page: number)
listDetailInfo.list = markRaw([...result.list])
listDetailInfo.id = id
listDetailInfo.source = result.source
listDetailInfo.total = result.total
if (page == 1 || (result.total && result.list.length)) listDetailInfo.total = result.total
else listDetailInfo.total = result.limit * page
listDetailInfo.limit = result.limit
listDetailInfo.page = page
if (result.list.length) listDetailInfo.noItemLabel = ''
else listDetailInfo.noItemLabel = window.i18n.t('no_item')
else if (page == 1) listDetailInfo.noItemLabel = window.i18n.t('no_item')
}
export const clearListDetail = () => {
listDetailInfo.list = []
@ -80,6 +81,7 @@ export const getListDetailAll = async(id: string, isRefresh = false): Promise<LX
return result
}) ?? Promise.reject(new Error('source not found' + source))
}
// eslint-disable-next-line @typescript-eslint/promise-function-async
return await loadData(bangId, 1).then((result: ListDetailInfo) => {
if (result.total <= result.limit) return result.list

View File

@ -20,7 +20,7 @@ const saveSearchHistoryListThrottle = throttle((list: LX.List.SearchHistoryList)
export const getHistoryList = async() => {
if (isInitedSearchHistory) return
if (isInitedSearchHistory || historyList.length) return
historyList.push(...(await getSearchHistoryList() ?? []))
isInitedSearchHistory = true
}
@ -28,6 +28,7 @@ export const addHistoryWord = async(word: string) => {
if (!appSetting['search.isShowHistorySearch']) return
if (!isInitedSearchHistory) await getHistoryList()
let index = historyList.indexOf(word)
if (index == 0) return
if (index > -1) historyList.splice(index, 1)
if (historyList.length >= 15) historyList.splice(14, historyList.length - 14)
historyList.unshift(word)

View File

@ -34,25 +34,27 @@ const handleSortList = (list: LX.Music.MusicInfo[], keyword: string) => {
const setLists = (results: SearchResult[], page: number, text: string): LX.Music.MusicInfo[] => {
let pages = []
let total = 0
// let limit = 0
let totals = []
let limit = 0
let list = []
for (const source of results) {
maxPages[source.source] = source.allPage
limit = Math.max(source.limit, limit)
if (source.allPage < page) continue
list.push(...source.list)
pages.push(source.allPage)
total += source.total
// limit = Math.max(source.limit, limit)
totals.push(source.total)
}
list = deduplicationList(list.map(s => markRaw(toNewMusicInfo(s))))
let listInfo = listInfos.all
listInfo.maxPage = Math.max(...pages)
listInfo.total = total
listInfo.maxPage = Math.max(0, ...pages)
const total = Math.max(0, ...totals)
if (page == 1 || (total && list.length)) listInfo.total = total
else listInfo.total = limit * page
// listInfo.limit = limit
listInfo.page = page
listInfo.list = handleSortList(list, text)
if (text && !list.length) listInfo.noItemLabel = window.i18n.t('no_item')
if (text && !list.length && page == 1) listInfo.noItemLabel = window.i18n.t('no_item')
else listInfo.noItemLabel = ''
return listInfo.list
}
@ -61,11 +63,12 @@ const setList = (datas: SearchResult, page: number, text: string): LX.Music.Musi
// console.log(datas.source, datas.list)
let listInfo = listInfos[datas.source] as ListInfo
listInfo.list = deduplicationList(datas.list.map(s => markRaw(toNewMusicInfo(s))))
listInfo.total = datas.total
if (page == 1 || (datas.total && datas.list.length)) listInfo.total = datas.total
else listInfo.total = datas.limit * page
listInfo.maxPage = datas.allPage
listInfo.page = page
listInfo.limit = datas.limit
if (text && !datas.list.length) listInfo.noItemLabel = window.i18n.t('no_item')
if (text && !datas.list.length && page == 1) listInfo.noItemLabel = window.i18n.t('no_item')
else listInfo.noItemLabel = ''
return listInfo.list
}

View File

@ -36,22 +36,24 @@ let maxTotals: Partial<Record<LX.OnlineSource, number>> = {
}
const setLists = (results: SearchResult[], page: number, text: string): ListInfoItem[] => {
let totals = []
// let limit = 0
let limit = 0
let list = []
for (const source of results) {
list.push(...source.list)
totals.push(source.total)
maxTotals[source.source] = source.total
maxPages[source.source] = Math.ceil(source.total / source.limit)
// limit = Math.max(source.limit, limit)
limit = Math.max(source.limit, limit)
}
markRawList(list)
let listInfo = listInfos.all
listInfo.total = Math.max(...totals)
const total = Math.max(0, ...totals)
if (page == 1 || (total && list.length)) listInfo.total = total
else listInfo.total = limit * page
listInfo.page = page
listInfo.list = handleSortList(list, text)
if (text && !list.length) listInfo.noItemLabel = window.i18n.t('no_item')
if (text && !list.length && page == 1) listInfo.noItemLabel = window.i18n.t('no_item')
else listInfo.noItemLabel = ''
return listInfo.list
}
@ -60,10 +62,11 @@ const setList = (datas: SearchResult, page: number, text: string): ListInfoItem[
// console.log(datas.source, datas.list)
let listInfo = listInfos[datas.source] as SearchListInfo
listInfo.list = markRawList(datas.list)
listInfo.total = datas.total
if (page == 1 || (datas.total && datas.list.length)) listInfo.total = datas.total
else listInfo.total = datas.limit * page
listInfo.page = page
listInfo.limit = datas.limit
if (text && !datas.list.length) listInfo.noItemLabel = window.i18n.t('no_item')
if (text && !datas.list.length && page == 1) listInfo.noItemLabel = window.i18n.t('no_item')
else listInfo.noItemLabel = ''
return listInfo.list
}

View File

@ -33,25 +33,27 @@ export const clearList = () => {
export const setList = (result: ListInfo, tagId: string, sortId: string, page: number) => {
listInfo.list = markRaw([...result.list])
listInfo.total = result.total
if (page == 1 || (result.total && result.list.length)) listInfo.total = result.total
else listInfo.total = result.limit * page
listInfo.limit = result.limit
listInfo.page = page
listInfo.source = result.source
listInfo.tagId = tagId
listInfo.sortId = sortId
if (result.list.length) listInfo.noItemLabel = ''
else listInfo.noItemLabel = window.i18n.t('no_item')
else if (page == 1) listInfo.noItemLabel = window.i18n.t('no_item')
}
export const setListDetail = (result: ListDetailInfo, id: string, page: number) => {
listDetailInfo.list = markRaw([...result.list])
listDetailInfo.id = id
listDetailInfo.source = result.source
listDetailInfo.total = result.total
if (page == 1 || (result.total && result.list.length)) listDetailInfo.total = result.total
else listDetailInfo.total = result.limit * page
listDetailInfo.limit = result.limit
listDetailInfo.page = page
listDetailInfo.info = markRaw({ ...result.info })
if (result.list.length) listDetailInfo.noItemLabel = ''
else listDetailInfo.noItemLabel = window.i18n.t('no_item')
else if (page == 1) listDetailInfo.noItemLabel = window.i18n.t('no_item')
}
export const setSelectListInfo = (info: ListInfoItem) => {
@ -158,6 +160,7 @@ export const getListDetailAll = async(id: string, source: LX.OnlineSource, isRef
return result
}) ?? Promise.reject(new Error('source not found' + source))
}
// eslint-disable-next-line @typescript-eslint/promise-function-async
return await loadData(id, 1).then((result: ListDetailInfo) => {
if (result.total <= result.limit) return result.list

View File

@ -120,7 +120,7 @@ export default {
if (item.filesize_high !== 0) {
let size = sizeFormate(item.filesize_high)
types.push({ type: 'flac24bit', size, hash: item.hash_high })
_types.flac = {
_types.flac24bit = {
size,
hash: item.hash_high,
}
@ -190,6 +190,7 @@ export default {
let total = body.data.total
let limit = 100
let listData = this.filterData(body.data.info)
// console.log(listData)
return {
total,
list: listData,

View File

@ -53,7 +53,7 @@ export default {
singer: decodeName(rawData.SingerName),
name: decodeName(rawData.SongName),
albumName: decodeName(rawData.AlbumName),
albumId: rawData.Albumid,
albumId: rawData.AlbumID,
songmid: rawData.Audioid,
source: 'kg',
interval: formatPlayTime(rawData.Duration),

View File

@ -305,7 +305,8 @@ export default {
return `https://y.qq.com/n/ryqq/playlist/${id}`
},
search(text, page, limit = 20) {
search(text, page, limit = 20, retryNum = 0) {
if (retryNum > 5) throw new Error('max retry')
return httpFetch(`http://c.y.qq.com/soso/fcgi-bin/client_music_search_songlist?page_no=${page - 1}&num_per_page=${limit}&format=json&query=${encodeURIComponent(text)}&remoteplace=txt.yqq.playlist&inCharset=utf8&outCharset=utf-8`, {
headers: {
'User-Agent': 'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0)',
@ -313,7 +314,7 @@ export default {
},
})
.promise.then(({ body }) => {
if (body.code != 0) throw new Error('filed')
if (body.code != 0) return this.search(text, page, limit, ++retryNum)
// console.log(body.data.list)
return {
list: body.data.list.map(item => {

View File

@ -68,13 +68,14 @@ export default ({ list, listAll }) => {
removeAllSelect()
if (lastSelectIndex != clickIndex) {
let isNeedReverse = false
if (clickIndex < lastSelectIndex) {
let temp = lastSelectIndex
lastSelectIndex = clickIndex
let _lastSelectIndex = lastSelectIndex
if (clickIndex < _lastSelectIndex) {
let temp = _lastSelectIndex
_lastSelectIndex = clickIndex
clickIndex = temp
isNeedReverse = true
}
selectedList.value = list.value.slice(lastSelectIndex, clickIndex + 1)
selectedList.value = list.value.slice(_lastSelectIndex, clickIndex + 1)
if (isNeedReverse) selectedList.value.reverse()
}
} else {

View File

@ -67,14 +67,15 @@ export default ({ list }) => {
if (selectedList.value.length) {
removeAllSelect()
if (lastSelectIndex != clickIndex) {
let _lastSelectIndex = lastSelectIndex
let isNeedReverse = false
if (clickIndex < lastSelectIndex) {
let temp = lastSelectIndex
lastSelectIndex = clickIndex
if (clickIndex < _lastSelectIndex) {
let temp = _lastSelectIndex
_lastSelectIndex = clickIndex
clickIndex = temp
isNeedReverse = true
}
selectedList.value = list.value.slice(lastSelectIndex, clickIndex + 1)
selectedList.value = list.value.slice(_lastSelectIndex, clickIndex + 1)
if (isNeedReverse) selectedList.value.reverse()
}
} else {