重构数据同步功能,新增客户端模式

pull/1229/head
lyswhut 2023-02-28 18:01:56 +08:00
parent b42092884e
commit 3b70acc3ca
43 changed files with 2232 additions and 1249 deletions

692
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -282,7 +282,6 @@
"electron-log": "^4.4.8",
"electron-store": "^8.1.0",
"font-list": "^1.4.5",
"http-terminator": "^3.2.0",
"iconv-lite": "^0.6.3",
"image-size": "^1.0.2",
"jschardet": "^3.0.0",
@ -291,12 +290,12 @@
"music-metadata": "^8.1.3",
"needle": "github:lyswhut/needle#93299ac841b7e9a9f82ca7279b88aaaeda404060",
"node-id3": "^0.2.5",
"socket.io": "^4.6.0",
"sortablejs": "^1.15.0",
"tunnel": "^0.0.6",
"utf-8-validate": "^6.0.2",
"vue": "^3.2.47",
"vue-router": "^4.1.6"
"vue-router": "^4.1.6",
"ws": "^8.12.1"
},
"overrides": {
"got": "^11",

View File

@ -1,3 +1,6 @@
### 新增
- 重构数据同步功能,新增客户端模式
### 优化

View File

@ -75,3 +75,24 @@ export const DOWNLOAD_STATUS = {
} as const
export const QUALITYS = ['flac24bit', 'flac', 'wav', 'ape', '320k', '192k', '128k'] as const
export const SYNC_CODE = {
helloMsg: 'Hello~::^-^::~v3~',
idPrefix: 'OjppZDo6',
authMsg: 'lx-music auth::',
authFailed: 'Auth failed',
missingAuthCode: 'Missing auth code',
getServiceIdFailed: 'Get service id failed',
connectServiceFailed: 'Connect service failed',
connecting: 'Connecting...',
unknownServiceAddress: 'Unknown service address',
msgBlockedIp: 'Blocked IP',
msgConnect: 'lx-music connect',
msgAuthFailed: 'Auth failed',
} as const
export const SYNC_CLOSE_CODE = {
normal: 1000,
failed: 4100,
} as const

View File

@ -2,7 +2,7 @@ import { join } from 'path'
import { homedir } from 'os'
const defaultSetting: LX.AppSetting = {
version: '2.0.0',
version: '2.1.0',
'common.windowSizeId': 3,
'common.fontSize': 16,
@ -108,8 +108,11 @@ const defaultSetting: LX.AppSetting = {
// 'tray.isToTray': false,
'tray.themeId': 0,
'sync.mode': 'server',
'sync.enable': false,
'sync.port': '23332',
'sync.server.port': '23332',
'sync.server.maxSsnapshotNum': 5,
'sync.client.host': '',
'theme.id': 'blue_plus',
// 'theme.id': 'green',

View File

@ -499,6 +499,11 @@ declare global {
*/
'tray.themeId': number
/**
*
*/
'sync.mode': 'server' | 'client'
/**
*
*/
@ -507,7 +512,17 @@ declare global {
/**
*
*/
'sync.port': '23332' | string
'sync.server.port': '23332' | string
/**
*
*/
'sync.server.maxSsnapshotNum': number
/**
*
*/
'sync.client.host': string
/**
*

View File

@ -1,10 +1,15 @@
declare namespace LX {
namespace Sync {
interface Enable {
interface EnableServer {
enable: boolean
port: string
}
interface EnableClient {
enable: boolean
host: string
authCode?: string
}
interface SyncActionBase <A> {
action: A
@ -14,14 +19,17 @@ declare namespace LX {
}
type SyncAction<A, D = undefined> = D extends undefined ? SyncActionBase<A> : SyncActionData<A, D>
type SyncMainWindowActions = SyncAction<'select_mode', KeyInfo>
type SyncMainWindowActions = SyncAction<'select_mode', string>
| SyncAction<'close_select_mode'>
| SyncAction<'status', Status>
| SyncAction<'client_status', ClientStatus>
| SyncAction<'server_status', ServerStatus>
type SyncServiceActions = SyncAction<'select_mode', Mode>
| SyncAction<'get_status'>
| SyncAction<'get_server_status'>
| SyncAction<'get_client_status'>
| SyncAction<'generate_code'>
| SyncAction<'enable', Enable>
| SyncAction<'enable_server', EnableServer>
| SyncAction<'enable_client', EnableClient>
type ActionList = SyncAction<'list_data_overwrite', LX.List.ListActionDataOverwrite>
| SyncAction<'list_create', LX.List.ListActionAdd>
@ -36,25 +44,55 @@ declare namespace LX {
| SyncAction<'list_music_overwrite', LX.List.ListActionMusicOverwrite>
| SyncAction<'list_music_clear', LX.List.ListActionMusicClear>
type ActionSync = SyncAction<'list:sync:list_sync_get_md5', string>
| SyncAction<'list:sync:list_sync_get_list_data', ListData>
| SyncAction<'list:sync:list_sync_get_sync_mode', Mode>
| SyncAction<'list:sync:action', ActionList>
// | SyncAction<'finished'>
type ActionSyncType = Actions<ActionSync>
type ActionSyncSend = SyncAction<'list:sync:list_sync_get_md5'>
| SyncAction<'list:sync:list_sync_get_list_data'>
| SyncAction<'list:sync:list_sync_get_sync_mode'>
| SyncAction<'list:sync:list_sync_set_data', LX.Sync.ListData>
| SyncAction<'list:sync:action', ActionList>
| SyncAction<'list:sync:finished'>
type ActionSyncSendType = Actions<ActionSyncSend>
interface List {
action: string
data: any
}
interface Status {
interface ServerStatus {
status: boolean
message: string
address: string[]
code: string
devices: KeyInfo[]
devices: ServerKeyInfo[]
}
interface KeyInfo {
interface ClientStatus {
status: boolean
message: string
address: string[]
}
interface ClientKeyInfo {
clientId: string
key: string
serverName: string
}
interface ServerKeyInfo {
clientId: string
key: string
deviceName: string
connectionTime?: number
lastSyncDate?: number
snapshotKey: string
isMobile: boolean
}
type ListData = Omit<LX.List.ListDataFull, 'tempList'>
@ -65,7 +103,7 @@ declare namespace LX {
| 'overwrite_remote_local'
| 'overwrite_local_remote_full'
| 'overwrite_remote_local_full'
| 'none'
// | 'none'
| 'cancel'
}

View File

@ -5,3 +5,8 @@ type DeepPartial<T> = {
}
type Modify<T, R> = Omit<T, keyof R> & R
// type UndefinedOrNever = undefined
type Actions<T extends { action: string, data?: any }> = {
[U in T as U['action']]: 'data' extends keyof U ? U['data'] : undefined
}

View File

@ -20,7 +20,7 @@ const oldThemeMap = {
export default (setting: any): Partial<LX.AppSetting> => {
setting = { ...setting }
// 迁移 v2 之前的配置
// 迁移 v2.0.0 之前的配置
if (compareVer(setting.version, '2.0.0') < 0) {
// 迁移列表滚动位置设置 ~0.18.3
if (setting.list?.scroll) {
@ -133,7 +133,16 @@ export default (setting: any): Partial<LX.AppSetting> => {
setting['odc.isAutoClearSearchInput'] = setting.odc?.isAutoClearSearchInput
setting['odc.isAutoClearSearchList'] = setting.odc?.isAutoClearSearchList
setting.version = '2.0.0'
}
// 迁移 v2.2.0 之前的设置数据
if (compareVer(setting.version, '2.1.0') < 0) {
setting['sync.erver.port'] = setting['sync.port']
setting.version = '2.1.0'
}
return setting
}

View File

@ -454,13 +454,24 @@
"setting__setting__desktop_lyric_font_weight_font": "verbatim lyrics",
"setting__setting__desktop_lyric_font_weight_line": "progressive lyrics",
"setting__sync": "Data synchronization",
"setting__sync_address": "Synchronization service address: {address}",
"setting__sync_auth_code": "Connection code: {code}",
"setting__sync_device": "Connected devices: {devices}",
"setting__sync_enable": "Enable the synchronization function (because the data is transmitted in clear text, please use it under a trusted network)",
"setting__sync_port": "Sync port settings",
"setting__sync_port_tip": "Please enter the synchronization service port number",
"setting__sync_refresh_code": "Refresh the connection code",
"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_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!",
"setting__sync_code_fail": "Invalid connection code",
"setting__sync_enable": "Enable sync",
"setting__sync_mode": "Synchronous mode",
"setting__sync_mode_client": "client mode",
"setting__sync_mode_server": "server mode",
"setting__sync_server_address": "Synchronization service address: {address}",
"setting__sync_server_auth_code": "Connection code: {code}",
"setting__sync_server_device": "Connected devices: {devices}",
"setting__sync_server_mode": "Server mode (since the data is transmitted in clear text, please use it under a trusted network)",
"setting__sync_server_port": "Sync port settings",
"setting__sync_server_port_tip": "Please enter the synchronization service port number",
"setting__sync_server_refresh_code": "Refresh the connection code",
"setting__sync_tip": "For how to use it, please see the \"Sync function\" section of the FAQ",
"setting__update": "Update",
"setting__update_checking": "Checking for updates...",
@ -475,6 +486,7 @@
"setting__update_show_change_log": "Show changelog on first boot after version update",
"setting__update_try_auto_update": "Attempt to download updates automatically when a new version is found",
"setting__update_unknown": "Unknown",
"setting_sync_status_enabled": "connected",
"song_list": "Playlists",
"songlist__import_input_btn_confirm": "Open",
"songlist__import_input_show_btn": "Open Playlist",
@ -502,6 +514,8 @@
"source_tx": "Tencent",
"source_wy": "Netease",
"source_xm": "Xiami",
"sync__auth_code_input_tip": "Please enter the connection code",
"sync__auth_code_title": "Need to enter the connection code",
"sync__merge_btn_local_remote": "Local list merge remote list",
"sync__merge_btn_remote_local": "Remote list merge local list",
"sync__merge_label": "Merge",
@ -519,6 +533,7 @@
"sync__overwrite_tip": "Cover: ",
"sync__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.",
"sync__title": "Choose how to synchronize the list with {name}",
"sync_status_disabled": "not connected",
"tag__high_quality": "HQ",
"tag__lossless": "SQ",
"tag__lossless_24bit": "24bit",

View File

@ -457,13 +457,24 @@
"setting__setting__desktop_lyric_font_weight_font": "逐字歌词",
"setting__setting__desktop_lyric_font_weight_line": "逐行歌词",
"setting__sync": "数据同步",
"setting__sync_address": "同步服务地址:{address}",
"setting__sync_auth_code": "连接码:{code}",
"setting__sync_device": "已连接的设备:{devices}",
"setting__sync_enable": "启用同步功能(由于数据是明文传输,请在受信任的网络下使用)",
"setting__sync_port": "同步端口设置",
"setting__sync_port_tip": "请输入同步服务端口号",
"setting__sync_refresh_code": "刷新连接码",
"setting__sync_client_address": "当前设备地址:{address}",
"setting__sync_client_host": "同步服务地址",
"setting__sync_client_host_tip": "请输入同步服务地址",
"setting__sync_client_mode": "客户端模式",
"setting__sync_client_status": "状态:{status}",
"setting__sync_code_blocked_ip": "当前设备的IP已被服务端封禁",
"setting__sync_code_fail": "连接码无效",
"setting__sync_enable": "启用同步功能",
"setting__sync_mode": "同步模式",
"setting__sync_mode_client": "客户端模式",
"setting__sync_mode_server": "服务端模式",
"setting__sync_server_address": "同步服务地址:{address}",
"setting__sync_server_auth_code": "连接码:{code}",
"setting__sync_server_device": "已连接的设备:{devices}",
"setting__sync_server_mode": "服务端模式(由于数据是明文传输,请在受信任的网络下使用)",
"setting__sync_server_port": "同步端口设置",
"setting__sync_server_port_tip": "请输入同步服务端口号",
"setting__sync_server_refresh_code": "刷新连接码",
"setting__sync_tip": "使用方式请看常见问题“同步功能”部分",
"setting__update": "软件更新",
"setting__update_checking": "检查更新中...",
@ -478,6 +489,7 @@
"setting__update_show_change_log": "更新版本后的首次启动时显示更新日志",
"setting__update_try_auto_update": "发现新版本时尝试自动下载更新",
"setting__update_unknown": "未知",
"setting_sync_status_enabled": "已连接",
"song_list": "歌单",
"songlist__import_input_btn_confirm": "打开",
"songlist__import_input_show_btn": "打开歌单",
@ -505,6 +517,8 @@
"source_tx": "企鹅音乐",
"source_wy": "网易音乐",
"source_xm": "虾米音乐",
"sync__auth_code_input_tip": "请输入连接码",
"sync__auth_code_title": "需要输入连接码",
"sync__merge_btn_local_remote": "本机列表 合并 远程列表",
"sync__merge_btn_remote_local": "远程列表 合并 本机列表",
"sync__merge_label": "合并",
@ -522,6 +536,7 @@
"sync__overwrite_tip": "覆盖:",
"sync__overwrite_tip_desc": "被覆盖者与覆盖者列表ID相同的列表将被删除后替换成覆盖者的列表列表ID不同的列表将被合并到一起若勾选完全覆盖则被覆盖者的所有列表将被移除然后替换成覆盖者的列表。",
"sync__title": "选择与 {name} 的列表同步方式",
"sync_status_disabled": "未连接",
"tag__high_quality": "HQ",
"tag__lossless": "SQ",
"tag__lossless_24bit": "24bit",

View File

@ -455,13 +455,24 @@
"setting__setting__desktop_lyric_font_weight_font": "逐字歌詞",
"setting__setting__desktop_lyric_font_weight_line": "逐行歌詞",
"setting__sync": "數據同步",
"setting__sync_address": "同步服務地址:{address}",
"setting__sync_auth_code": "連接碼:{code}",
"setting__sync_device": "已連接的設備:{devices}",
"setting__sync_enable": "啟用同步功能(由於數據是明文傳輸,請在受信任的網絡下使用)",
"setting__sync_port": "同步端口設置",
"setting__sync_port_tip": "請輸入同步服務端口號",
"setting__sync_refresh_code": "刷新連接碼",
"setting__sync_client_address": "當前設備地址:{address}",
"setting__sync_client_host": "同步服務地址",
"setting__sync_client_host_tip": "請輸入同步服務地址",
"setting__sync_client_mode": "客戶端模式",
"setting__sync_client_status": "狀態:{status}",
"setting__sync_code_blocked_ip": "當前設備的IP已被服務端封禁",
"setting__sync_code_fail": "連接碼無效",
"setting__sync_enable": "啟用同步功能",
"setting__sync_mode": "同步模式",
"setting__sync_mode_client": "客戶端模式",
"setting__sync_mode_server": "服務端模式",
"setting__sync_server_address": "同步服務地址:{address}",
"setting__sync_server_auth_code": "連接碼:{code}",
"setting__sync_server_device": "已連接的設備:{devices}",
"setting__sync_server_mode": "服務端模式(由於數據是明文傳輸,請在受信任的網絡下使用)",
"setting__sync_server_port": "同步端口設置",
"setting__sync_server_port_tip": "請輸入同步服務端口號",
"setting__sync_server_refresh_code": "刷新連接碼",
"setting__sync_tip": "使用方式請看常見問題“同步功能”部分",
"setting__update": "軟件更新",
"setting__update_checking": "檢查更新中...",
@ -476,6 +487,7 @@
"setting__update_show_change_log": "更新版本後的首次啟動時顯示更新日誌",
"setting__update_try_auto_update": "發現新版本時嘗試自動下載更新",
"setting__update_unknown": "未知",
"setting_sync_status_enabled": "已連接",
"song_list": "歌單",
"songlist__import_input_show_btn": "打開歌單",
"songlist__import_input_tip": "輸入歌單鏈接或歌單ID",
@ -502,6 +514,8 @@
"source_tx": "企鵝音樂",
"source_wy": "網易音樂",
"source_xm": "蝦米音樂",
"sync__auth_code_input_tip": "請輸入連接碼",
"sync__auth_code_title": "需要輸入連接碼",
"sync__merge_btn_local_remote": "本機列表 合併 遠程列表",
"sync__merge_btn_remote_local": "遠程列表 合併 本機列表",
"sync__merge_label": "合併",
@ -519,6 +533,7 @@
"sync__overwrite_tip": "覆蓋:",
"sync__overwrite_tip_desc": "被覆蓋者與覆蓋者列表ID相同的列表將被刪除後替換成覆蓋者的列表列表ID不同的列表將被合併到一起若勾選完全覆蓋則被覆蓋者的所有列表將被移除然後替換成覆蓋者的列表。",
"sync__title": "選擇與 {name} 的列表同步方式",
"sync_status_disabled": "未連接",
"tag__high_quality": "HQ",
"tag__lossless": "SQ",
"tag__lossless_24bit": "24bit",

View File

@ -0,0 +1,92 @@
import { request, generateRsaKey } from './utils'
import { getSyncAuthKey, setSyncAuthKey } from '../data'
import { SYNC_CODE } from '@common/constants'
import log from '../log'
import { aesDecrypt, aesEncrypt, getComputerName, rsaDecrypt } from '../utils'
const hello = async(urlInfo: LX.Sync.Client.UrlInfo) => request(`${urlInfo.httpProtocol}//${urlInfo.hostPath}/hello`)
.then(({ text }) => text == SYNC_CODE.helloMsg)
.catch((err: any) => {
log.error('[auth] hello', err.message)
console.log(err)
return false
})
const getServerId = async(urlInfo: LX.Sync.Client.UrlInfo) => request(`${urlInfo.httpProtocol}//${urlInfo.hostPath}/id`)
.then(({ text }) => {
if (!text.startsWith(SYNC_CODE.idPrefix)) return ''
return text.replace(SYNC_CODE.idPrefix, '')
})
.catch((err: any) => {
log.error('[auth] getServerId', err.message)
console.log(err)
throw err
})
const codeAuth = async(urlInfo: LX.Sync.Client.UrlInfo, serverId: string, authCode: string) => {
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')
let { publicKey, privateKey } = await generateRsaKey()
publicKey = publicKey.replace(/\n/g, '')
.replace('-----BEGIN PUBLIC KEY-----', '')
.replace('-----END PUBLIC KEY-----', '')
const msg = aesEncrypt(`${SYNC_CODE.authMsg}\n${publicKey}\n${getComputerName()}\nlx_music_desktop`, key)
// console.log(msg, key)
return request(`${urlInfo.httpProtocol}//${urlInfo.hostPath}/ah`, { headers: { m: msg } }).then(async({ text, code }) => {
// console.log(text)
switch (text) {
case SYNC_CODE.msgBlockedIp:
throw new Error(SYNC_CODE.msgBlockedIp)
case SYNC_CODE.authFailed:
throw new Error(SYNC_CODE.authFailed)
default:
if (code != 200) throw new Error(SYNC_CODE.authFailed)
}
let msg
try {
msg = rsaDecrypt(Buffer.from(text, 'base64'), privateKey).toString()
} catch (err: any) {
log.error('[auth] codeAuth decryptMsg error', err.message)
throw new Error(SYNC_CODE.authFailed)
}
// console.log(msg)
if (!msg) return Promise.reject(new Error(SYNC_CODE.authFailed))
const info = JSON.parse(msg) as LX.Sync.ClientKeyInfo
void setSyncAuthKey(serverId, info)
return info
})
}
const keyAuth = async(urlInfo: LX.Sync.Client.UrlInfo, keyInfo: LX.Sync.ClientKeyInfo) => {
const msg = aesEncrypt(SYNC_CODE.authMsg + getComputerName(), keyInfo.key)
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)
let msg
try {
msg = aesDecrypt(text, keyInfo.key)
} catch (err: any) {
log.error('[auth] keyAuth decryptMsg error', err.message)
throw new Error(SYNC_CODE.authFailed)
}
if (msg != SYNC_CODE.helloMsg) return Promise.reject(new Error(SYNC_CODE.authFailed))
})
}
const auth = async(urlInfo: LX.Sync.Client.UrlInfo, serverId: string, authCode?: string) => {
if (authCode) return codeAuth(urlInfo, serverId, authCode)
const keyInfo = await getSyncAuthKey(serverId)
if (!keyInfo) throw new Error(SYNC_CODE.missingAuthCode)
await keyAuth(urlInfo, keyInfo)
return keyInfo
}
export default async(urlInfo: LX.Sync.Client.UrlInfo, authCode?: string) => {
console.log('connect: ', urlInfo.href, authCode)
if (!await hello(urlInfo)) throw new Error(SYNC_CODE.connectServiceFailed)
const serverId = await getServerId(urlInfo)
if (!serverId) throw new Error(SYNC_CODE.getServiceIdFailed)
return auth(urlInfo, serverId, authCode)
}

View File

@ -0,0 +1,207 @@
import WebSocket from 'ws'
import { encryptMsg, decryptMsg } from './utils'
import * as modules from './modules'
// import { action as commonAction } from '@/store/modules/common'
// import { getStore } from '@/store'
import registerSyncListHandler from './syncList'
import log from '../log'
import { SYNC_CLOSE_CODE, SYNC_CODE } from '@common/constants'
import { dateFormat } from '@common/utils/common'
import { aesEncrypt, getAddress } from '../utils'
import { sendClientStatus } from '@main/modules/winMain'
let status: LX.Sync.ClientStatus = {
status: false,
message: '',
address: [],
}
export const sendSyncStatus = (newStatus: Omit<LX.Sync.ClientStatus, 'address'>) => {
status.status = newStatus.status
status.message = newStatus.message
if (status.status) {
status.address = getAddress()
}
sendClientStatus(status)
}
export const sendSyncMessage = (message: string) => {
status.message = message
sendClientStatus(status)
}
const handleConnection = (socket: LX.Sync.Client.Socket) => {
for (const moduleInit of Object.values(modules)) {
moduleInit(socket)
}
}
const heartbeatTools = {
failedNum: 0,
pingTimeout: null as NodeJS.Timeout | null,
delayRetryTimeout: null as NodeJS.Timeout | null,
handleOpen() {
console.log('open')
this.failedNum = 0
this.heartbeat()
},
heartbeat() {
if (this.pingTimeout) clearTimeout(this.pingTimeout)
// Use `WebSocket#terminate()`, which immediately destroys the connection,
// instead of `WebSocket#close()`, which waits for the close timer.
// Delay should be equal to the interval at which your server
// sends out pings plus a conservative assumption of the latency.
this.pingTimeout = setTimeout(() => {
client?.terminate()
}, 30000 + 1000)
},
reConnnect() {
if (this.pingTimeout) {
clearTimeout(this.pingTimeout)
this.pingTimeout = null
}
// client = null
if (!client) return
if (this.failedNum > 3) throw new Error('connect error')
this.delayRetryTimeout = setTimeout(() => {
this.delayRetryTimeout = null
if (!client) return
console.log(dateFormat(new Date()), 'reconnnect...')
connect(client.data.urlInfo, client.data.keyInfo)
}, 2000)
this.failedNum++
},
clearTimeout() {
if (this.delayRetryTimeout) {
clearTimeout(this.delayRetryTimeout)
this.delayRetryTimeout = null
}
if (this.pingTimeout) {
clearTimeout(this.pingTimeout)
this.pingTimeout = null
}
},
connect(socket: LX.Sync.Client.Socket) {
console.log('heartbeatTools connect')
socket.on('open', () => {
this.handleOpen()
})
socket.on('ping', () => {
this.heartbeat()
})
socket.on('close', (code) => {
console.log(code)
switch (code) {
case SYNC_CLOSE_CODE.normal:
case SYNC_CLOSE_CODE.failed:
return
}
this.reConnnect()
})
},
}
let client: LX.Sync.Client.Socket | null
// let listSyncPromise: Promise<void>
export const connect = (urlInfo: LX.Sync.Client.UrlInfo, keyInfo: LX.Sync.ClientKeyInfo) => {
client = new WebSocket(`${urlInfo.wsProtocol}//${urlInfo.hostPath}?i=${encodeURIComponent(keyInfo.clientId)}&t=${encodeURIComponent(aesEncrypt(SYNC_CODE.msgConnect, keyInfo.key))}`, {
}) as LX.Sync.Client.Socket
client.data = {
keyInfo,
urlInfo,
}
heartbeatTools.connect(client)
// listSyncPromise = registerSyncListHandler(socket)
let events: Partial<{ [K in keyof LX.Sync.ActionSyncSendType]: Array<(data: LX.Sync.ActionSyncSendType[K]) => (void | Promise<void>)> }> = {}
let closeEvents: Array<(err: Error) => (void | Promise<void>)> = []
client.addEventListener('message', ({ data }) => {
if (data == 'ping') return
if (typeof data === 'string') {
let syncData: LX.Sync.ActionSync
try {
syncData = JSON.parse(decryptMsg(keyInfo, data))
} catch {
return
}
const handlers = events[syncData.action]
if (handlers) {
// @ts-expect-error
for (const handler of handlers) void handler(syncData.data)
}
}
})
client.onRemoteEvent = function(eventName, handler) {
let eventArr = events[eventName]
if (!eventArr) events[eventName] = eventArr = []
// let eventArr = events.get(eventName)
// if (!eventArr) events.set(eventName, eventArr = [])
eventArr.push(handler)
return () => {
eventArr!.splice(eventArr!.indexOf(handler), 1)
}
}
client.sendData = function(eventName, data, callback) {
client?.send(encryptMsg(keyInfo, JSON.stringify({ action: eventName, data })), callback)
}
client.onClose = function(handler: typeof closeEvents[number]) {
closeEvents.push(handler)
return () => {
closeEvents.splice(closeEvents.indexOf(handler), 1)
}
}
client.addEventListener('open', () => {
log.info('connect')
// const store = getStore()
// global.lx.syncKeyInfo = keyInfo
client!.isReady = false
sendSyncStatus({
status: false,
message: 'Wait syncing...',
})
void registerSyncListHandler(client as LX.Sync.Client.Socket).then(() => {
log.info('sync list success')
handleConnection(client as LX.Sync.Client.Socket)
log.info('register list sync service success')
client!.isReady = true
sendSyncStatus({
status: true,
message: '',
})
}).catch(err => {
console.log(err)
log.r_error(err.stack)
sendSyncStatus({
status: false,
message: err.message,
})
})
})
client.addEventListener('close', () => {
sendSyncStatus({
status: false,
message: '',
})
const err = new Error('closed')
for (const handler of closeEvents) void handler(err)
closeEvents = []
events = {}
})
}
export const disconnect = async() => {
if (!client) return
log.info('disconnecting...')
client.close(SYNC_CLOSE_CODE.normal)
client = null
heartbeatTools.clearTimeout()
}
export const getStatus = (): LX.Sync.ClientStatus => status

View File

@ -0,0 +1,69 @@
import handleAuth from './auth'
import { connect as socketConnect, disconnect as socketDisconnect, sendSyncStatus, sendSyncMessage } from './client'
// import { getSyncHost } from '@/utils/data'
import { SYNC_CODE } from '@common/constants'
import log from '../log'
import { parseUrl } from './utils'
const handleConnect = async(host: string, authCode?: string) => {
// const hostInfo = await getSyncHost()
// console.log(hostInfo)
// if (!hostInfo || !hostInfo.host || !hostInfo.port) throw new Error(SYNC_CODE.unknownServiceAddress)
const urlInfo = parseUrl(host)
await disconnectServer(false)
const keyInfo = await handleAuth(urlInfo, authCode)
socketConnect(urlInfo, keyInfo)
}
const handleDisconnect = async() => {
await socketDisconnect()
}
const connectServer = async(host: string, authCode?: string) => {
sendSyncStatus({
status: false,
message: SYNC_CODE.connecting,
})
return handleConnect(host, authCode).then(() => {
sendSyncStatus({
status: true,
message: '',
})
}).catch(async err => {
sendSyncStatus({
status: false,
message: err.message,
})
switch (err.message) {
case SYNC_CODE.connectServiceFailed:
case SYNC_CODE.missingAuthCode:
break
default:
log.r_warn(err.message)
break
}
return Promise.reject(err)
})
}
const disconnectServer = async(isResetStatus = true) => handleDisconnect().then(() => {
log.info('disconnect...')
if (isResetStatus) {
sendSyncStatus({
status: false,
message: '',
})
}
}).catch((err: any) => {
log.error(`disconnect error: ${err.message as string}`)
sendSyncMessage(err.message)
})
export {
connectServer,
disconnectServer,
}
export {
getStatus,
} from './client'

View File

@ -0,0 +1 @@
export { default as list } from './list'

View File

@ -0,0 +1,7 @@
import initOn from './on'
import initSend from './send'
export default (socket: LX.Sync.Client.Socket) => {
initOn(socket)
initSend(socket)
}

View File

@ -0,0 +1,9 @@
import { handleRemoteListAction } from '@main/modules/sync/utils'
export default (socket: LX.Sync.Client.Socket) => {
socket.onRemoteEvent('list:sync:action', (action) => {
if (!socket.isReady) return
void handleRemoteListAction(action)
})
}

View File

@ -0,0 +1,21 @@
import { registerListActionEvent } from '@main/modules/sync/utils'
let socket: LX.Sync.Client.Socket | null
let unregisterLocalListAction: (() => void) | null
const sendListAction = (action: LX.Sync.ActionList) => {
// console.log('sendListAction', action.action)
if (!socket?.isReady) return
socket.sendData('list:sync:action', action)
}
export default (_socket: LX.Sync.Client.Socket) => {
socket = _socket
socket.onClose(() => {
socket = null
unregisterLocalListAction?.()
unregisterLocalListAction = null
})
unregisterLocalListAction = registerListActionEvent(sendListAction)
}

View File

@ -0,0 +1,73 @@
import log from '../log'
import { getLocalListData, setLocalListData } from '../utils'
import { toMD5 } from '@common/utils/nodejs'
import { removeSelectModeListener, sendCloseSelectMode, sendSelectMode } from '@main/modules/winMain'
import { SYNC_CLOSE_CODE } from '@common/constants'
const logInfo = (eventName: keyof LX.Sync.ActionSyncSendType, success = false) => {
log.info(`[${eventName as string}]${eventName.replace('list:sync:list_sync_', '').replaceAll('_', ' ')}${success ? ' success' : ''}`)
}
const logError = (eventName: keyof LX.Sync.ActionSyncSendType, err: Error) => {
log.error(`[${eventName as string}]${eventName.replace('list:sync:list_sync_', '').replaceAll('_', ' ')} error: ${err.message}`)
}
export default async(socket: LX.Sync.Client.Socket) => new Promise<void>((resolve, reject) => {
let listenEvents: Array<() => void> = []
const unregisterEvents = () => {
while (listenEvents.length) listenEvents.shift()?.()
}
socket.onClose(() => {
unregisterEvents()
sendCloseSelectMode()
removeSelectModeListener()
reject(new Error('closed'))
})
listenEvents.push(socket.onRemoteEvent('list:sync:list_sync_get_md5', async() => {
logInfo('list:sync:list_sync_get_md5')
const md5 = toMD5(JSON.stringify(await getLocalListData()))
socket?.sendData('list:sync:list_sync_get_md5', md5, (err) => {
if (err) {
logError('list:sync:list_sync_get_md5', err)
socket.close(SYNC_CLOSE_CODE.failed)
return
}
logInfo('list:sync:list_sync_get_md5', true)
})
}))
listenEvents.push(socket.onRemoteEvent('list:sync:list_sync_get_list_data', async() => {
logInfo('list:sync:list_sync_get_list_data')
socket?.sendData('list:sync:list_sync_get_list_data', await getLocalListData(), (err) => {
if (err) {
logError('list:sync:list_sync_get_list_data', err)
socket.close(SYNC_CLOSE_CODE.failed)
return
}
logInfo('list:sync:list_sync_get_list_data', true)
})
}))
listenEvents.push(socket.onRemoteEvent('list:sync:list_sync_get_sync_mode', async() => {
logInfo('list:sync:list_sync_get_sync_mode')
sendSelectMode(socket.data.keyInfo.serverName, (mode) => {
removeSelectModeListener()
socket?.sendData('list:sync:list_sync_get_sync_mode', mode, (err) => {
if (err) {
logError('list:sync:list_sync_get_sync_mode', err)
socket.close(SYNC_CLOSE_CODE.failed)
return
}
logInfo('list:sync:list_sync_get_sync_mode', true)
})
})
}))
listenEvents.push(socket.onRemoteEvent('list:sync:list_sync_set_data', async(data) => {
logInfo('list:sync:list_sync_set_data')
await setLocalListData(data)
logInfo('list:sync:list_sync_set_data', true)
}))
listenEvents.push(socket.onRemoteEvent('list:sync:finished', async() => {
unregisterEvents()
resolve()
logInfo('list:sync:finished', true)
}))
})

View File

@ -0,0 +1,115 @@
import { generateKeyPair } from 'node:crypto'
import { httpFetch, type RequestOptions } from '@main/utils/request'
export const request = async(url: string, options: RequestOptions = { }) => {
return httpFetch(url, {
...options,
timeout: options.timeout ?? 10000,
}).then(response => {
return {
text: response.body,
code: response.statusCode,
}
})
// const controller = new AbortController()
// let id: number | null = setTimeout(() => {
// id = null
// controller.abort()
// }, timeout)
// return fetch(url, {
// ...options,
// signal: controller.signal,
// // eslint-disable-next-line @typescript-eslint/promise-function-async
// }).then(async(response) => {
// const text = await response.text()
// return {
// text,
// code: response.status,
// }
// }).catch(err => {
// // console.log(err, err.code, err.message)
// throw err
// }).finally(() => {
// if (id == null) return
// clearTimeout(id)
// })
}
// export const aesEncrypt = (text: string, key: string, iv: string) => {
// const cipher = createCipheriv('aes-128-cbc', Buffer.from(key, 'base64'), Buffer.from(iv, 'base64'))
// return Buffer.concat([cipher.update(Buffer.from(text)), cipher.final()]).toString('base64')
// }
// export const aesDecrypt = (text: string, key: string, iv: string) => {
// 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()
// }
export const generateRsaKey = async() => new Promise<{ publicKey: string, privateKey: string }>((resolve, reject) => {
generateKeyPair(
'rsa',
{
modulusLength: 2048, // It holds a number. It is the key size in bits and is applicable for RSA, and DSA algorithm only.
publicKeyEncoding: {
type: 'spki', // Note the type is pkcs1 not spki
format: 'pem',
},
privateKeyEncoding: {
type: 'pkcs8', // Note again the type is set to pkcs1
format: 'pem',
// cipher: "aes-256-cbc", //Optional
// passphrase: "", //Optional
},
},
(err, publicKey, privateKey) => {
if (err) {
reject(err)
return
}
resolve({
publicKey,
privateKey,
})
},
)
})
export const encryptMsg = (keyInfo: LX.Sync.ClientKeyInfo, msg: string): string => {
return msg
// if (!keyInfo) return ''
// return aesEncrypt(msg, keyInfo.key, keyInfo.iv)
}
export const decryptMsg = (keyInfo: LX.Sync.ClientKeyInfo, enMsg: string): string => {
return enMsg
// if (!keyInfo) return ''
// let msg = ''
// try {
// msg = aesDecrypt(enMsg, keyInfo.key, keyInfo.iv)
// } catch (err) {
// console.log(err)
// }
// return msg
}
export const parseUrl = (host: string): LX.Sync.Client.UrlInfo => {
const url = new URL(host)
let hostPath = url.host + url.pathname
let href = url.href
if (hostPath.endsWith('/')) hostPath = hostPath.replace(/\/$/, '')
if (href.endsWith('/')) href = href.replace(/\/$/, '')
return {
wsProtocol: url.protocol == 'https:' ? 'wss:' : 'ws:',
httpProtocol: url.protocol,
hostPath,
href,
}
}
export const sendStatus = (status: LX.Sync.ClientStatus) => {
// syncLog.log(JSON.stringify(status))
}

View File

@ -0,0 +1,180 @@
import { randomBytes } from 'node:crypto'
import { STORE_NAMES } from '@common/constants'
import getStore from '@main/utils/store'
import { throttle } from '@common/utils/common'
import path from 'node:path'
import fs from 'node:fs'
import log from './log'
export const getSyncAuthKey = async(serverId: string) => {
const store = getStore(STORE_NAMES.SYNC)
const keys = store.get('syncAuthKey') as Record<string, LX.Sync.ClientKeyInfo> | null
if (!keys) return null
return keys[serverId] ?? null
}
export const setSyncAuthKey = async(serverId: string, info: LX.Sync.ClientKeyInfo) => {
const store = getStore(STORE_NAMES.SYNC)
let keys: Record<string, LX.Sync.ClientKeyInfo> = (store.get('syncAuthKey') as Record<string, LX.Sync.ClientKeyInfo> | null) ?? {}
keys[serverId] = info
store.set('syncAuthKey', keys)
}
let syncHost: string
export const getSyncHost = async() => {
if (syncHost === undefined) {
const store = getStore(STORE_NAMES.SYNC)
syncHost = (store.get('syncHost') as typeof syncHost | null) ?? ''
}
return syncHost
}
export const setSyncHost = async(host: string) => {
// let hostInfo = await getData(syncHostPrefix) || {}
// hostInfo.host = host
// hostInfo.port = port
syncHost = host
const store = getStore(STORE_NAMES.SYNC)
store.set('syncHost', syncHost)
}
let syncHostHistory: string[]
export const getSyncHostHistory = async() => {
if (syncHostHistory === undefined) {
const store = getStore(STORE_NAMES.SYNC)
syncHostHistory = (store.get('syncHostHistory') as string[]) ?? []
}
return syncHostHistory
}
export const addSyncHostHistory = async(host: string) => {
let syncHostHistory = await getSyncHostHistory()
if (syncHostHistory.some(h => h == host)) return
syncHostHistory.unshift(host)
if (syncHostHistory.length > 20) syncHostHistory = syncHostHistory.slice(0, 20) // 最多存储20个
const store = getStore(STORE_NAMES.SYNC)
store.set('syncHostHistory', syncHostHistory)
}
export const removeSyncHostHistory = async(index: number) => {
syncHostHistory.splice(index, 1)
const store = getStore(STORE_NAMES.SYNC)
store.set('syncHostHistory', syncHostHistory)
}
export interface SnapshotInfo {
latest: string | null
time: number
list: string[]
}
interface DevicesInfo {
serverId: string
clients: Record<string, LX.Sync.ServerKeyInfo>
snapshotInfo: SnapshotInfo
}
// const devicesFilePath = path.join(global.lx.dataPath, 'devices.json')
const devicesInfo: DevicesInfo = { serverId: '', clients: {}, snapshotInfo: { latest: null, time: 0, list: [] } }
let deviceKeys: string[] = []
const saveDevicesInfoThrottle = throttle(() => {
const store = getStore(STORE_NAMES.SYNC)
store.set('keys', devicesInfo.clients)
})
const initDeviceInfo = () => {
const store = getStore(STORE_NAMES.SYNC)
const serverId = store.get('serverId') as string | undefined
if (serverId) devicesInfo.serverId = serverId
else {
devicesInfo.serverId = randomBytes(4 * 4).toString('base64')
saveDevicesInfoThrottle()
}
const devices = store.get('clients') as DevicesInfo['clients'] | undefined
if (devices) devicesInfo.clients = devices
deviceKeys = Object.values(devicesInfo.clients).map(device => device.key).filter(k => k)
const snapshotInfo = store.get('snapshotInfo') as DevicesInfo['snapshotInfo'] | undefined
if (snapshotInfo) devicesInfo.snapshotInfo = snapshotInfo
}
export const createClientKeyInfo = (deviceName: string, isMobile: boolean): LX.Sync.ServerKeyInfo => {
const keyInfo: LX.Sync.ServerKeyInfo = {
clientId: randomBytes(4 * 4).toString('base64'),
key: randomBytes(16).toString('base64'),
deviceName,
isMobile,
snapshotKey: '',
lastSyncDate: 0,
}
saveClientKeyInfo(keyInfo)
return keyInfo
}
export const saveClientKeyInfo = (keyInfo: LX.Sync.ServerKeyInfo) => {
if (devicesInfo.clients[keyInfo.clientId] == null && Object.keys(devicesInfo.clients).length > 101) throw new Error('max keys')
devicesInfo.clients[keyInfo.clientId] = keyInfo
saveDevicesInfoThrottle()
}
export const getClientKeyInfo = (clientId?: string): LX.Sync.ServerKeyInfo | null => {
if (!clientId) return null
return devicesInfo.clients[clientId] ?? null
}
export const getServerId = (): string => {
if (!devicesInfo.serverId) initDeviceInfo()
return devicesInfo.serverId
}
export const isIncluedsDevice = (name: string) => {
return deviceKeys.includes(name)
}
export const clearOldSnapshot = async() => {
if (!devicesInfo.snapshotInfo) return
const snapshotList = devicesInfo.snapshotInfo.list.filter(name => !isIncluedsDevice(name))
let requiredSave = snapshotList.length > global.lx.appSetting['sync.server.maxSsnapshotNum']
while (snapshotList.length > global.lx.appSetting['sync.server.maxSsnapshotNum']) {
const name = snapshotList.pop()
if (name) {
await removeSnapshot(name)
devicesInfo.snapshotInfo.list.splice(devicesInfo.snapshotInfo.list.indexOf(name), 1)
} else break
}
if (requiredSave) saveSnapshotInfo(devicesInfo.snapshotInfo)
}
export const updateDeviceSnapshotKey = (keyInfo: LX.Sync.ServerKeyInfo, key: string) => {
if (keyInfo.snapshotKey) deviceKeys.splice(deviceKeys.indexOf(keyInfo.snapshotKey), 1)
keyInfo.snapshotKey = key
keyInfo.lastSyncDate = Date.now()
saveClientKeyInfo(keyInfo)
deviceKeys.push(key)
saveDevicesInfoThrottle()
void clearOldSnapshot()
}
const saveSnapshotInfoThrottle = throttle(() => {
const store = getStore(STORE_NAMES.SYNC)
store.set('snapshotInfo', devicesInfo.snapshotInfo)
})
export const getSnapshotInfo = (): SnapshotInfo => {
return devicesInfo.snapshotInfo
}
export const saveSnapshotInfo = (info: SnapshotInfo) => {
devicesInfo.snapshotInfo = info
saveSnapshotInfoThrottle()
}
export const getSnapshot = async(name: string) => {
const filePath = path.join(global.lxDataPath, `snapshot_${name}`)
let listData: LX.Sync.ListData
try {
listData = JSON.parse((await fs.promises.readFile(filePath)).toString('utf-8'))
} catch (err) {
log.warn(err)
return null
}
return listData
}
export const saveSnapshot = async(name: string, data: string) => {
const filePath = path.join(global.lxDataPath, `snapshot_${name}`)
return fs.promises.writeFile(filePath, data).catch((err) => {
log.error(err)
throw err
})
}
export const removeSnapshot = async(name: string) => {
const filePath = path.join(global.lxDataPath, `snapshot_${name}`)
return fs.promises.unlink(filePath).catch((err) => {
log.error(err)
})
}

View File

@ -1,21 +1,28 @@
// import Event from './event/event'
import { disconnectServer } from './client'
import { stopServer } from './server'
// import eventNames from './event/name'
import * as modules from './modules'
import { startServer, stopServer, getStatus, generateCode } from './server/server'
export {
startServer,
stopServer,
getStatus,
getStatus as getServerStatus,
generateCode,
// Event,
// eventNames,
modules,
}
} from './server'
export {
connectServer,
disconnectServer,
getStatus as getClientStatus,
} from './client'
export default () => {
global.lx.event_app.on('main_window_close', () => {
void stopServer()
if (global.lx.appSetting['sync.mode'] == 'server') {
void stopServer()
} else {
void disconnectServer()
}
})
}

View File

@ -0,0 +1,25 @@
import { log as writeLog } from '@common/utils'
export default {
r_info(...params: any[]) {
writeLog.info(...params)
},
r_warn(...params: any[]) {
writeLog.warn(...params)
},
r_error(...params: any[]) {
writeLog.error(...params)
},
info(...params: any[]) {
// if (global.lx.isEnableSyncLog) writeLog.info(...params)
console.log(...params)
},
warn(...params: any[]) {
// if (global.lx.isEnableSyncLog) writeLog.warn(...params)
console.warn(...params)
},
error(...params: any[]) {
// if (global.lx.isEnableSyncLog) writeLog.error(...params)
console.warn(...params)
},
}

View File

@ -1,22 +1,17 @@
import type http from 'http'
import { SYNC_CODE } from './config'
import {
aesEncrypt,
aesDecrypt,
createClientKeyInfo,
getClientKeyInfo,
setClientKeyInfo,
rsaEncrypt,
} from './utils'
import { SYNC_CODE } from '@common/constants'
import querystring from 'node:querystring'
import { getIP } from './utils'
import { createClientKeyInfo, getClientKeyInfo, saveClientKeyInfo } from '../data'
import { aesDecrypt, aesEncrypt, getComputerName, rsaEncrypt } from '../utils'
const requestIps = new Map<string, number>()
export const authCode = async(req: http.IncomingMessage, res: http.ServerResponse, authCode: string) => {
export const authCode = async(req: http.IncomingMessage, res: http.ServerResponse, password: string) => {
let code = 401
let msg: string = SYNC_CODE.msgAuthFailed
let ip = req.socket.remoteAddress
let ip = getIP(req)
// console.log(req.headers)
if (typeof req.headers.m == 'string') {
if (ip && (requestIps.get(ip) ?? 0) < 10) {
@ -38,12 +33,12 @@ export const authCode = async(req: http.IncomingMessage, res: http.ServerRespons
const deviceName = text.replace(SYNC_CODE.authMsg, '') || 'Unknown'
if (deviceName != keyInfo.deviceName) {
keyInfo.deviceName = deviceName
setClientKeyInfo(keyInfo)
saveClientKeyInfo(keyInfo)
}
msg = aesEncrypt(SYNC_CODE.helloMsg, keyInfo.key)
}
} else { // 连接码验证
let key = ''.padStart(16, Buffer.from(authCode).toString('hex'))
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)
@ -59,7 +54,13 @@ export const authCode = async(req: http.IncomingMessage, res: http.ServerRespons
const data = text.split('\n')
const publicKey = `-----BEGIN PUBLIC KEY-----\n${data[1]}\n-----END PUBLIC KEY-----`
const deviceName = data[2] || 'Unknown'
msg = rsaEncrypt(Buffer.from(JSON.stringify(createClientKeyInfo(deviceName))), publicKey)
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)
}
}
}

View File

@ -1,8 +0,0 @@
export const SYNC_CODE = {
helloMsg: 'Hello~::^-^::~v2~',
idPrefix: 'OjppZDo6',
authMsg: 'lx-music auth::',
msgAuthFailed: 'Auth failed',
msgBlockedIp: 'Blocked IP',
msgConnect: 'lx-music connect',
} as const

View File

@ -1,7 +1,16 @@
import { startServer, stopServer, getStatus } from './server'
import * as modules from './modules'
import {
startServer,
stopServer,
getStatus,
generateCode,
} from './server'
export {
startServer,
stopServer,
getStatus,
generateCode,
modules,
}

View File

@ -0,0 +1,68 @@
// import { throttle } from '@common/utils/common'
// import { sendSyncActionList } from '@main/modules/winMain'
import { SYNC_CLOSE_CODE } from '@common/constants'
import { updateDeviceSnapshotKey } from '../../data'
import { handleRemoteListAction } from '../../utils'
import { createSnapshot, encryptMsg } from '../utils'
let wss: LX.Sync.Server.SocketServer | null
let removeListener: (() => void) | null
// type listAction = 'list:action'
// const addMusic = (orderId, callback) => {
// // ...
// }
const broadcast = async(key: string, data: any, excludeIds: string[] = []) => {
if (!wss) return
const dataStr = JSON.stringify({ action: 'list:sync:action', data })
for (const socket of wss.clients) {
if (excludeIds.includes(socket.keyInfo.clientId) || !socket.isReady) continue
socket.send(encryptMsg(socket.keyInfo, dataStr), (err) => {
if (err) {
socket.close(SYNC_CLOSE_CODE.failed)
return
}
updateDeviceSnapshotKey(socket.keyInfo, key)
})
}
}
export const sendListAction = async(action: LX.Sync.ActionList) => {
console.log('sendListAction', action.action)
// io.sockets
await broadcast('list:sync:action', action)
}
export const registerListHandler = (_wss: LX.Sync.Server.SocketServer, socket: LX.Sync.Server.Socket) => {
if (!wss) {
wss = _wss
// removeListener = registerListActionEvent()
}
socket.onRemoteEvent('list:sync:action', (action) => {
if (!socket.isReady) return
// console.log(msg)
void handleRemoteListAction(action).then(updated => {
if (!updated) return
void createSnapshot().then(key => {
if (!key) return
updateDeviceSnapshotKey(socket.keyInfo, key)
void broadcast(key, action, [socket.keyInfo.clientId])
})
})
// socket.broadcast.emit('list:action', { action: 'list_remove', data: { id: 'default', index: 0 } })
})
// socket.on('list:add', addMusic)
}
export const unregisterListHandler = () => {
wss = null
if (removeListener) {
removeListener()
removeListener = null
}
}

View File

@ -1,16 +1,18 @@
import http from 'http'
import { Server, type Socket } from 'socket.io'
import { createHttpTerminator, type HttpTerminator } from 'http-terminator'
import * as modules from '../modules'
import http, { type IncomingMessage } from 'node:http'
import url from 'node:url'
import { WebSocketServer } from 'ws'
import * as modules from './modules'
import { authCode, authConnect } from './auth'
import { getAddress, getServerId, generateCode as handleGenerateCode, getClientKeyInfo, setClientKeyInfo } from './utils'
import { syncList, removeSnapshot } from './syncList'
import { log } from '@common/utils'
import { sendStatus } from '@main/modules/winMain'
import { SYNC_CODE } from './config'
import syncList from './syncList'
import log from '../log'
import { SYNC_CLOSE_CODE, SYNC_CODE } from '@common/constants'
import { decryptMsg, encryptMsg, generateCode as handleGenerateCode } from './utils'
import { getAddress } from '../utils'
import { sendServerStatus } from '@main/modules/winMain/index'
import { getClientKeyInfo, getServerId, saveClientKeyInfo } from '../data'
let status: LX.Sync.Status = {
let status: LX.Sync.ServerStatus = {
status: false,
message: '',
address: [],
@ -27,7 +29,7 @@ const codeTools: {
start() {
this.stop()
this.timeout = setInterval(() => {
void generateCode()
void handleGenerateCode()
}, 60 * 3 * 1000)
},
stop() {
@ -37,12 +39,45 @@ const codeTools: {
},
}
const handleConnection = (io: Server, socket: LX.Sync.Socket) => {
console.log('connection')
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) {
socket.close(SYNC_CLOSE_CODE.failed)
return
}
keyInfo.lastSyncDate = Date.now()
saveClientKeyInfo(keyInfo)
// // socket.lx_keyInfo = keyInfo
socket.keyInfo = keyInfo
try {
await syncList(wss as LX.Sync.Server.SocketServer, socket)
} catch (err) {
// console.log(err)
log.warn(err)
return
}
status.devices.push(keyInfo)
// handleConnection(io, socket)
sendServerStatus(status)
// console.log('connection', keyInfo.deviceName)
log.info('connection', keyInfo.deviceName)
// console.log(socket.handshake.query)
for (const module of Object.values(modules)) {
module.registerListHandler(io, socket)
module.registerListHandler(wss as LX.Sync.Server.SocketServer, socket)
}
socket.isReady = true
}
const handleUnconnection = () => {
@ -64,11 +99,16 @@ const authConnection = (req: http.IncomingMessage, callback: (err: string | null
})
}
let httpTerminator: HttpTerminator | null = null
let io: Server | null = null
let wss: LX.Sync.Server.SocketServer | null
let httpServer: http.Server
const handleStartServer = async(port = 9527) => await new Promise((resolve, reject) => {
const httpServer = http.createServer((req, res) => {
function noop() {}
function onSocketError(err: Error) {
console.error(err)
}
const handleStartServer = async(port = 9527, ip = '0.0.0.0') => await new Promise((resolve, reject) => {
httpServer = http.createServer((req, res) => {
// console.log(req.url)
let code
let msg
@ -93,46 +133,96 @@ const handleStartServer = async(port = 9527) => await new Promise((resolve, reje
res.writeHead(code)
res.end(msg)
})
httpTerminator = createHttpTerminator({
server: httpServer,
})
io = new Server(httpServer, {
path: '/sync',
serveClient: false,
connectTimeout: 10000,
pingTimeout: 30000,
maxHttpBufferSize: 1e9, // 1G
allowRequest: authConnection,
transports: ['websocket'],
wss = new WebSocketServer({
noServer: true,
})
io.on('connection', async(_socket: Socket) => {
const socket = _socket as LX.Sync.Socket
socket.on('disconnect', reason => {
console.log('disconnect', reason)
status.devices.splice(status.devices.findIndex(k => k.clientId == keyInfo?.clientId), 1)
sendStatus(status)
if (!status.devices.length) handleUnconnection()
wss.on('connection', function(socket, request) {
socket.isReady = false
socket.on('pong', () => {
socket.isAlive = true
})
if (typeof socket.handshake.query.i != 'string') return socket.disconnect(true)
const keyInfo = getClientKeyInfo(socket.handshake.query.i)
if (!keyInfo || !io) return socket.disconnect(true)
keyInfo.connectionTime = Date.now()
setClientKeyInfo(keyInfo)
// socket.lx_keyInfo = keyInfo
socket.data.keyInfo = keyInfo
socket.data.isReady = false
try {
await syncList(io, socket)
} catch (err) {
// console.log(err)
log.warn(err)
return
// const events = new Map<keyof ActionsType, Array<(err: Error | null, data: LX.Sync.ActionSyncType[keyof LX.Sync.ActionSyncType]) => void>>()
// const events = new Map<keyof LX.Sync.ActionSyncType, Array<(err: Error | null, data: LX.Sync.ActionSyncType[keyof LX.Sync.ActionSyncType]) => void>>()
let events: Partial<{ [K in keyof LX.Sync.ActionSyncType]: Array<(data: LX.Sync.ActionSyncType[K]) => void> }> = {}
socket.addEventListener('message', ({ data }) => {
if (typeof data === 'string') {
let syncData: LX.Sync.ActionSync
try {
syncData = JSON.parse(decryptMsg(socket.keyInfo, data))
} catch (err) {
log.error('parse message error:', err)
socket.close(SYNC_CLOSE_CODE.failed)
return
}
const handlers = events[syncData.action]
if (handlers) {
// @ts-expect-error
for (const handler of handlers) handler(syncData.data)
}
}
})
socket.addEventListener('close', () => {
const err = new Error('closed')
for (const handler of Object.values(events).flat()) {
// @ts-expect-error
handler(err, null)
}
events = {}
if (!status.devices.length) handleUnconnection()
log.info('deconnection', socket.keyInfo.deviceName)
})
socket.onRemoteEvent = function(eventName, handler) {
let eventArr = events[eventName]
if (!eventArr) events[eventName] = eventArr = []
// let eventArr = events.get(eventName)
// if (!eventArr) events.set(eventName, eventArr = [])
eventArr.push(handler)
return () => {
eventArr!.splice(eventArr!.indexOf(handler), 1)
}
}
status.devices.push(keyInfo)
handleConnection(io, socket)
socket.data.isReady = true
sendStatus(status)
socket.sendData = function(eventName, data, callback) {
socket.send(encryptMsg(socket.keyInfo, JSON.stringify({ action: eventName, data })), callback)
}
void handleConnection(socket, request)
})
httpServer.on('upgrade', function upgrade(request, socket, head) {
socket.on('error', onSocketError)
// This function is not defined on purpose. Implement it with your own logic.
authConnection(request, err => {
if (err) {
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n')
socket.destroy()
return
}
socket.removeListener('error', onSocketError)
wss?.handleUpgrade(request, socket, head, function done(ws) {
wss?.emit('connection', ws, request)
})
})
})
const interval = setInterval(() => {
wss?.clients.forEach(ws => {
if (ws.isAlive == false) {
ws.terminate()
return
}
ws.isAlive = false
ws.ping(noop)
if (ws.keyInfo.isMobile) ws.send('ping', noop)
})
}, 30000)
wss.on('close', function close() {
clearInterval(interval)
})
httpServer.on('error', error => {
@ -142,35 +232,41 @@ const handleStartServer = async(port = 9527) => await new Promise((resolve, reje
httpServer.on('listening', () => {
const addr = httpServer.address()
// console.log(addr)
if (!addr) {
reject(new Error('address is null'))
return
}
const bind = typeof addr == 'string' ? `pipe ${addr}` : `port ${addr.port}`
console.info(`Listening on ${bind}`)
log.info(`Listening on ${ip} ${bind}`)
resolve(null)
})
httpServer.listen(port)
httpServer.listen(port, ip)
})
const handleStopServer = async() => {
if (!httpTerminator) return
if (!io) return
io.close()
await httpTerminator.terminate().catch(() => {})
io = null
httpTerminator = null
}
const handleStopServer = async() => new Promise<void>((resolve, reject) => {
if (!wss) return
wss.close()
wss = null
httpServer.close((err) => {
if (err) {
reject(err)
return
}
resolve()
})
})
export const stopServer = async() => {
console.log('stop')
codeTools.stop()
if (!status.status) {
status.status = false
status.message = ''
status.address = []
status.code = ''
sendStatus(status)
sendServerStatus(status)
return
}
console.log('stoping sync server...')
@ -184,14 +280,15 @@ export const stopServer = async() => {
console.log(err)
status.message = err.message
}).finally(() => {
sendStatus(status)
sendServerStatus(status)
})
}
export const startServer = async(port: number) => {
console.log('status.status', status.status)
if (status.status) await handleStopServer()
console.log('starting sync server...')
log.info('starting sync server')
await handleStartServer(port).then(() => {
console.log('sync server started')
status.status = true
@ -207,18 +304,14 @@ export const startServer = async(port: number) => {
status.address = []
status.code = ''
}).finally(() => {
sendStatus(status)
sendServerStatus(status)
})
}
export const getStatus = (): LX.Sync.Status => status
export const getStatus = (): LX.Sync.ServerStatus => status
export const generateCode = async() => {
status.code = handleGenerateCode()
sendStatus(status)
sendServerStatus(status)
return status.code
}
export {
removeSnapshot,
}

View File

@ -1,13 +1,18 @@
import { promises as fsPromises } from 'fs'
import { encryptMsg, decryptMsg, getSnapshotFilePath } from './utils'
import { throttle } from '@common/utils'
import { type Server } from 'socket.io'
import { sendCloseSelectMode, sendSelectMode } from '@main/modules/winMain'
import { LIST_IDS } from '@common/constants'
import { SYNC_CLOSE_CODE } from '@common/constants'
import { removeSelectModeListener, sendCloseSelectMode, sendSelectMode } from '@main/modules/winMain'
import { getSnapshot, updateDeviceSnapshotKey } from '../data'
import { getLocalListData, setLocalListData } from '../utils'
import { createSnapshot, encryptMsg, getCurrentListInfoKey } from './utils'
const handleSetLocalListData = async(listData: LX.Sync.ListData) => {
await setLocalListData(listData)
return createSnapshot()
}
// type ListInfoType = LX.List.UserListInfoFull | LX.List.MyDefaultListInfoFull | LX.List.MyLoveListInfoFull
let io: Server | null
let wss: LX.Sync.Server.SocketServer | null
let syncingId: string | null = null
const wait = async(time = 1000) => await new Promise((resolve, reject) => setTimeout(resolve, time))
@ -19,107 +24,93 @@ const patchListData = (listData: Partial<LX.Sync.ListData>): LX.Sync.ListData =>
}, listData)
}
const getRemoteListData = async(socket: LX.Sync.Socket): Promise<LX.Sync.ListData> => await new Promise((resolve, reject) => {
const getRemoteListData = async(socket: LX.Sync.Server.Socket): Promise<LX.Sync.ListData> => await new Promise((resolve, reject) => {
console.log('getRemoteListData')
const handleError = (reason: string) => {
socket.removeListener('list:sync', handleSuccess)
socket.removeListener('disconnect', handleError)
reject(new Error(reason))
}
const handleSuccess = (enData: string) => {
socket.removeListener('disconnect', handleError)
socket.removeListener('list:sync', handleSuccess)
console.log('getRemoteListData', 'handleSuccess')
const data: LX.Sync.Data | null = JSON.parse(decryptMsg(socket.data.keyInfo, enData))
if (!data) {
reject(new Error('Get remote list data failed'))
return
}
if (data.action != 'getData') return
resolve(patchListData(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' })))
let removeEventClose = socket.onClose(reject)
let removeEvent = socket.onRemoteEvent('list:sync:list_sync_get_list_data', (listData) => {
resolve(patchListData(listData))
removeEventClose()
removeEvent()
})
socket.sendData('list:sync:list_sync_get_list_data', undefined, (err) => {
if (!err) return
reject(err)
removeEventClose()
removeEvent()
})
})
// const getAllLists = async() => {
// const lists = []
// lists.push(await getListMusics(defaultList.id).then(musics => ({ ...defaultList, list: toRaw(musics).map(m => toOldMusicInfo(m)) })))
// lists.push(await getListMusics(loveList.id).then(musics => ({ ...loveList, list: toRaw(musics).map(m => toOldMusicInfo(m)) })))
const getRemoteListMD5 = async(socket: LX.Sync.Server.Socket): Promise<string> => await new Promise((resolve, reject) => {
let removeEventClose = socket.onClose(reject)
let removeEvent = socket.onRemoteEvent('list:sync:list_sync_get_md5', (md5) => {
resolve(md5)
removeEventClose()
removeEvent()
})
socket.sendData('list:sync:list_sync_get_md5', undefined, (err) => {
if (!err) return
reject(err)
removeEventClose()
removeEvent()
})
})
// for await (const list of userLists) {
// lists.push(await getListMusics(list.id).then(musics => ({ ...toRaw(list), list: toRaw(musics).map(m => toOldMusicInfo(m)) })))
// }
// return lists
// }
const getLocalListData = async(): Promise<LX.Sync.ListData> => {
const lists: LX.Sync.ListData = {
defaultList: await global.lx.worker.dbService.getListMusics(LIST_IDS.DEFAULT),
loveList: await global.lx.worker.dbService.getListMusics(LIST_IDS.LOVE),
userList: [],
}
const userListInfos = await global.lx.worker.dbService.getAllUserList()
for await (const list of userListInfos) {
lists.userList.push(await global.lx.worker.dbService.getListMusics(list.id)
.then(musics => ({ ...list, list: musics })))
}
return lists
}
const getSyncMode = async(socket: LX.Sync.Socket): Promise<LX.Sync.Mode> => await new Promise((resolve, reject) => {
const handleDisconnect = () => {
const getSyncMode = async(socket: LX.Sync.Server.Socket): Promise<LX.Sync.Mode> => new Promise((resolve, reject) => {
const handleDisconnect = (err: Error) => {
sendCloseSelectMode()
reject(new Error('disconnect'))
removeSelectModeListener()
reject(err)
}
socket.on('disconnect', handleDisconnect)
let removeListener = sendSelectMode(socket.data.keyInfo, (mode) => {
removeListener()
sendSelectMode(socket.keyInfo.deviceName, (mode) => {
removeSelectModeListener()
removeEventClose()
resolve(mode)
})
let removeEventClose = socket.onClose(handleDisconnect)
})
const finishedSync = (socket: LX.Sync.Socket) => {
return socket.emit('list:sync', encryptMsg(socket.data.keyInfo, JSON.stringify({
action: 'finished',
})))
}
const finishedSync = async(socket: LX.Sync.Server.Socket) => new Promise<void>((resolve, reject) => {
socket.sendData('list:sync:finished', undefined, (err) => {
if (err) reject(err)
else resolve()
})
})
const setLocalList = (listData: LX.Sync.ListData) => {
void global.lx.event_list.list_data_overwrite(listData, true)
}
const setRemotelList = async(socket: LX.Sync.Socket, listData: LX.Sync.ListData) => {
if (!io) return
const sockets: LX.Sync.RemoteSocket[] = 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 })))
const sendDataPromise = async(socket: LX.Sync.Server.Socket, dataStr: string, key: string) => new Promise<void>((resolve, reject) => {
socket.send(encryptMsg(socket.keyInfo, dataStr), (err) => {
if (err) {
socket.close(SYNC_CLOSE_CODE.failed)
resolve()
return
}
updateDeviceSnapshotKey(socket.keyInfo, key)
resolve()
})
})
const overwriteRemoteListData = async(listData: LX.Sync.ListData, key: string, excludeIds: string[] = []) => {
if (!wss) return
const dataStr = JSON.stringify({ action: 'list:sync:action', data: { action: 'list_data_overwrite', data: listData } })
const tasks: Array<Promise<void>> = []
for (const socket of wss.clients) {
if (excludeIds.includes(socket.keyInfo.clientId) || !socket.isReady) continue
tasks.push(sendDataPromise(socket, dataStr, key))
}
if (!tasks.length) return
await Promise.all(tasks)
}
const writeFilePromises = new Map<string, Promise<void>>()
const updateSnapshot = async(path: string, data: string) => {
console.log('updateSnapshot', path)
let writeFilePromise = writeFilePromises.get(path) ?? Promise.resolve()
writeFilePromise = writeFilePromise.then(async() => {
if (writeFilePromise !== writeFilePromises.get(path)) return
await fsPromises.writeFile(path, data)
const setRemotelList = async(socket: LX.Sync.Server.Socket, listData: LX.Sync.ListData, key: string): Promise<void> => new Promise((resolve, reject) => {
socket.sendData('list:sync:list_sync_set_data', listData, (err) => {
if (err) {
reject(err)
return
}
updateDeviceSnapshotKey(socket.keyInfo, key)
resolve()
})
writeFilePromises.set(path, writeFilePromise)
await writeFilePromise.finally(() => {
if (writeFilePromise !== writeFilePromises.get(path)) return
writeFilePromises.delete(path)
})
}
})
type UserDataObj = Record<string, LX.List.UserListInfoFull>
const createUserListDataObj = (listData: LX.Sync.ListData): UserDataObj => {
const userListDataObj: UserDataObj = {}
for (const list of listData.userList) userListDataObj[list.id] = list
@ -225,12 +216,14 @@ const overwriteList = (sourceListData: LX.Sync.ListData, targetListData: LX.Sync
return newListData
}
const handleMergeListData = async(socket: LX.Sync.Socket): Promise<LX.Sync.ListData | null> => {
const handleMergeListData = async(socket: LX.Sync.Server.Socket): Promise<[LX.Sync.ListData, boolean, boolean]> => {
const mode: LX.Sync.Mode = await getSyncMode(socket)
const [remoteListData, localListData] = await Promise.all([getRemoteListData(socket), getLocalListData()])
console.log('handleMergeListData', 'remoteListData, localListData')
let listData: LX.Sync.ListData
let requiredUpdateLocalListData = true
let requiredUpdateRemoteListData = true
switch (mode) {
case 'merge_local_remote':
listData = mergeList(localListData, remoteListData)
@ -246,63 +239,47 @@ const handleMergeListData = async(socket: LX.Sync.Socket): Promise<LX.Sync.ListD
break
case 'overwrite_local_remote_full':
listData = localListData
requiredUpdateLocalListData = false
break
case 'overwrite_remote_local_full':
listData = remoteListData
requiredUpdateRemoteListData = false
break
case 'none': return null
// case 'none': return null
case 'cancel':
socket.disconnect(true)
default:
socket.close(SYNC_CLOSE_CODE.normal)
throw new Error('cancel')
}
return listData
return [listData, requiredUpdateLocalListData, requiredUpdateRemoteListData]
}
const handleSyncList = async(socket: LX.Sync.Socket): Promise<LX.Sync.ListData | null> => {
const handleSyncList = async(socket: LX.Sync.Server.Socket) => {
const [remoteListData, localListData] = await Promise.all([getRemoteListData(socket), getLocalListData()])
console.log('handleSyncList', 'remoteListData, localListData')
const listData: LX.Sync.ListData = {
defaultList: [],
loveList: [],
userList: [],
}
if (localListData.defaultList.length || localListData.loveList.length || localListData.userList.length) {
if (remoteListData.defaultList.length || remoteListData.loveList.length || remoteListData.userList.length) {
const mergedList = await handleMergeListData(socket)
const [mergedList, requiredUpdateLocalListData, requiredUpdateRemoteListData] = await handleMergeListData(socket)
console.log('handleMergeListData', 'mergedList')
// console.log(mergedList)
if (!mergedList) return null
listData.defaultList = mergedList.defaultList
listData.loveList = mergedList.loveList
listData.userList = mergedList.userList
setLocalList(mergedList)
void setRemotelList(socket, mergedList)
let key
if (requiredUpdateLocalListData) {
key = await handleSetLocalListData(mergedList)
await overwriteRemoteListData(mergedList, key, [socket.keyInfo.clientId])
}
if (requiredUpdateRemoteListData) {
if (!key) key = await getCurrentListInfoKey()
await setRemotelList(socket, mergedList, key)
}
} else {
void setRemotelList(socket, localListData)
listData.defaultList = localListData.defaultList
listData.loveList = localListData.loveList
listData.userList = localListData.userList
await setRemotelList(socket, localListData, await getCurrentListInfoKey())
}
} else {
if (remoteListData.defaultList.length || remoteListData.loveList.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
const key = await handleSetLocalListData(remoteListData)
updateDeviceSnapshotKey(socket.keyInfo, key)
await overwriteRemoteListData(remoteListData, key, [socket.keyInfo.clientId])
}
}
return await updateSnapshot(socket.data.snapshotFilePath, JSON.stringify({
defaultList: listData.defaultList,
loveList: listData.loveList,
userList: listData.userList,
})).then(() => {
socket.data.isCreatedSnapshot = true
return listData
})
}
const mergeListDataFromSnapshot = (
@ -347,7 +324,16 @@ const mergeListDataFromSnapshot = (
}
return ids.map(id => map.get(id)) as LX.Music.MusicInfo[]
}
const handleMergeListDataFromSnapshot = async(socket: LX.Sync.Socket, snapshot: LX.Sync.ListData): Promise<LX.Sync.ListData> => {
const checkListLatest = async(socket: LX.Sync.Server.Socket) => {
const remoteListMD5 = await getRemoteListMD5(socket)
const currentListInfoKey = await getCurrentListInfoKey()
const latest = remoteListMD5 == currentListInfoKey
if (latest && socket.keyInfo.snapshotKey != currentListInfoKey) updateDeviceSnapshotKey(socket.keyInfo, currentListInfoKey)
return latest
}
const handleMergeListDataFromSnapshot = async(socket: LX.Sync.Server.Socket, snapshot: LX.Sync.ListData) => {
if (await checkListLatest(socket)) return
const addMusicLocationType = global.lx.appSetting['list.addMusicLocationType']
const [remoteListData, localListData] = await Promise.all([getRemoteListData(socket), getLocalListData()])
const newListData: LX.Sync.ListData = {
@ -404,79 +390,37 @@ const handleMergeListDataFromSnapshot = async(socket: LX.Sync.Socket, snapshot:
})
newListData.userList = newUserList
setLocalList(newListData)
void setRemotelList(socket, newListData)
return await updateSnapshot(socket.data.snapshotFilePath, JSON.stringify({
defaultList: newListData.defaultList,
loveList: newListData.loveList,
userList: newListData.userList,
})).then(() => {
socket.data.isCreatedSnapshot = true
return newListData
})
const key = await handleSetLocalListData(newListData)
await setRemotelList(socket, newListData, key)
await overwriteRemoteListData(newListData, key, [socket.keyInfo.clientId])
}
const registerUpdateSnapshotTask = (socket: LX.Sync.Socket, snapshot: LX.Sync.ListData) => {
if (!socket.data.isCreatedSnapshot) return
const handleUpdateSnapshot = throttle(() => {
// TODO: 同步成功后再保存快照
void getLocalListData().then(({ defaultList, loveList, userList }) => {
if (defaultList != null) snapshot.defaultList = defaultList
if (loveList != null) snapshot.loveList = loveList
if (userList != null) snapshot.userList = userList
void updateSnapshot(socket.data.snapshotFilePath, JSON.stringify(snapshot))
})
}, 2000)
global.lx.event_list.on('list_changed', handleUpdateSnapshot)
socket.on('disconnect', () => {
global.lx.event_list.off('list_changed', handleUpdateSnapshot)
})
}
const syncList = async(socket: LX.Sync.Socket): Promise<LX.Sync.ListData | null> => {
socket.data.snapshotFilePath = getSnapshotFilePath(socket.data.keyInfo)
let fileData: any
let isSyncRequired = false
try {
fileData = (await fsPromises.readFile(socket.data.snapshotFilePath)).toString()
fileData = JSON.parse(fileData)
} catch (error) {
const err = error as NodeJS.ErrnoException
if (err.code != 'ENOENT') throw err
isSyncRequired = true
const syncList = async(socket: LX.Sync.Server.Socket) => {
// socket.data.snapshotFilePath = getSnapshotFilePath(socket.keyInfo)
if (socket.keyInfo.snapshotKey) {
const listData = await getSnapshot(socket.keyInfo.snapshotKey)
if (listData) {
await handleMergeListDataFromSnapshot(socket, listData)
return
}
}
console.log('isSyncRequired', isSyncRequired)
if (isSyncRequired) return await handleSyncList(socket)
return await handleMergeListDataFromSnapshot(socket, patchListData(fileData))
await handleSyncList(socket)
}
// const checkSyncQueue = async(): Promise<void> => {
// if (!syncingId) return
// console.log('sync queue...')
// await wait()
// await checkSyncQueue()
// }
// export {
// syncList = async(_io: Server, socket: LX.Sync.Socket) => {
// io = _io
// await checkSyncQueue()
// syncingId = socket.data.keyInfo.clientId
// return await syncList(socket).then(newListData => {
// registerUpdateSnapshotTask(socket, { ...newListData })
// return finishedSync(socket)
// }).finally(() => {
// syncingId = null
// })
// }
// }
export default async(_wss: LX.Sync.Server.SocketServer, socket: LX.Sync.Server.Socket) => {
if (!wss) {
wss = _wss
_wss.addListener('close', () => {
wss = null
})
}
const _syncList = async(_io: Server, socket: LX.Sync.Socket) => {
io = _io
let disconnected = false
socket.on('disconnect', () => {
socket.onClose(() => {
disconnected = true
if (syncingId == socket.data.keyInfo.clientId) syncingId = null
if (syncingId == socket.keyInfo.clientId) syncingId = null
})
while (true) {
@ -485,22 +429,16 @@ const _syncList = async(_io: Server, socket: LX.Sync.Socket) => {
await wait()
}
syncingId = socket.data.keyInfo.clientId
return await syncList(socket).then(newListData => {
if (newListData) registerUpdateSnapshotTask(socket, { ...newListData })
syncingId = socket.keyInfo.clientId
await syncList(socket).then(async() => {
// if (newListData) registerUpdateSnapshotTask(socket, { ...newListData })
return finishedSync(socket)
}).finally(() => {
syncingId = null
})
}
const removeSnapshot = async(keyInfo: LX.Sync.KeyInfo) => {
const filePath = getSnapshotFilePath(keyInfo)
await fsPromises.unlink(filePath)
}
export {
_syncList as syncList,
removeSnapshot,
}
// const removeSnapshot = async(keyInfo: LX.Sync.KeyInfo) => {
// const filePath = getSnapshotFilePath(keyInfo)
// await fsPromises.unlink(filePath)
// }

View File

@ -1,97 +1,28 @@
import { networkInterfaces } from 'os'
import { randomBytes, createCipheriv, createDecipheriv, publicEncrypt, privateDecrypt, constants } from 'crypto'
import { join } from 'path'
import getStore from '@main/utils/store'
const STORE_NAME = 'sync'
type KeyInfos = Record<string, LX.Sync.KeyInfo>
export const getAddress = (): string[] => {
const nets = networkInterfaces()
const results: string[] = []
// console.log(nets)
for (const interfaceInfos of Object.values(nets)) {
if (!interfaceInfos) continue
// Skip over non-IPv4 and internal (i.e. 127.0.0.1) addresses
for (const interfaceInfo of interfaceInfos) {
if (interfaceInfo.family === 'IPv4' && !interfaceInfo.internal) {
results.push(interfaceInfo.address)
}
}
}
return results
}
let serverId: string | undefined
export const getServerId = (): string => {
if (serverId) return serverId
const store = getStore(STORE_NAME)
serverId = store.get('serverId') as string | undefined
if (!serverId) {
serverId = randomBytes(4 * 4).toString('base64')
store.set('serverId', serverId)
}
return serverId
}
let keyInfos: KeyInfos
export const createClientKeyInfo = (deviceName: string): LX.Sync.KeyInfo => {
const keyInfo: LX.Sync.KeyInfo = {
clientId: randomBytes(4 * 4).toString('base64'),
key: randomBytes(16).toString('base64'),
deviceName,
}
const store = getStore(STORE_NAME)
keyInfos ??= store.get('keys') as KeyInfos || {}
if (Object.keys(keyInfos).length > 101) throw new Error('max keys')
keyInfos[keyInfo.clientId] = keyInfo
store.set('keys', keyInfos)
return keyInfo
}
export const setClientKeyInfo = (keyInfo: LX.Sync.KeyInfo) => {
keyInfos[keyInfo.clientId] = keyInfo
const store = getStore(STORE_NAME)
store.set('keys', keyInfos)
}
export const getClientKeyInfo = (clientId: string): LX.Sync.KeyInfo | null => {
if (!keyInfos) {
const store = getStore(STORE_NAME)
keyInfos = store.get('keys') as KeyInfos || {}
}
return keyInfos[clientId] ?? null
}
import { toMD5 } from '@common/utils/nodejs'
import type http from 'node:http'
import {
getSnapshotInfo,
saveSnapshot,
saveSnapshotInfo,
type SnapshotInfo,
} from '../data'
import { getLocalListData } from '../utils'
export const generateCode = (): string => {
return Math.random().toString().substring(2, 8)
}
export const aesEncrypt = (buffer: string | Buffer, key: string): string => {
const cipher = createCipheriv('aes-128-ecb', Buffer.from(key, 'base64'), '')
return Buffer.concat([cipher.update(buffer), cipher.final()]).toString('base64')
export const getIP = (request: http.IncomingMessage) => {
return request.socket.remoteAddress
}
export const aesDecrypt = (text: string, key: string): string => {
const decipher = createDecipheriv('aes-128-ecb', Buffer.from(key, 'base64'), '')
return Buffer.concat([decipher.update(Buffer.from(text, 'base64')), decipher.final()]).toString()
}
export const rsaEncrypt = (buffer: Buffer, key: string): string => {
return publicEncrypt({ key, padding: constants.RSA_PKCS1_OAEP_PADDING }, buffer).toString('base64')
}
export const rsaDecrypt = (buffer: Buffer, key: string): Buffer => {
return privateDecrypt({ key, padding: constants.RSA_PKCS1_OAEP_PADDING }, buffer)
}
export const encryptMsg = (keyInfo: LX.Sync.KeyInfo, msg: string): string => {
export const encryptMsg = (keyInfo: LX.Sync.ServerKeyInfo, msg: string): string => {
return msg
// if (!keyInfo) return ''
// return aesEncrypt(msg, keyInfo.key, keyInfo.iv)
}
export const decryptMsg = (keyInfo: LX.Sync.KeyInfo, enMsg: string): string => {
export const decryptMsg = (keyInfo: LX.Sync.ServerKeyInfo, enMsg: string): string => {
return enMsg
// if (!keyInfo) return ''
// let msg = ''
@ -103,6 +34,29 @@ export const decryptMsg = (keyInfo: LX.Sync.KeyInfo, enMsg: string): string => {
// return msg
}
export const getSnapshotFilePath = (keyInfo: LX.Sync.KeyInfo): string => {
return join(global.lxDataPath, `snapshot-${Buffer.from(keyInfo.clientId).toString('hex').substring(0, 10)}.json`)
let snapshotInfo: SnapshotInfo
export const createSnapshot = async() => {
if (!snapshotInfo) snapshotInfo = getSnapshotInfo()
const listData = JSON.stringify(await getLocalListData())
const md5 = toMD5(listData)
if (snapshotInfo.latest == md5) return md5
if (snapshotInfo.list.includes(md5)) {
snapshotInfo.list.splice(snapshotInfo.list.indexOf(md5), 1)
} else await saveSnapshot(md5, listData)
if (snapshotInfo.latest) snapshotInfo.list.unshift(snapshotInfo.latest)
snapshotInfo.latest = md5
snapshotInfo.time = Date.now()
saveSnapshotInfo(snapshotInfo)
return md5
}
export const getCurrentListInfoKey = async() => {
if (!snapshotInfo) snapshotInfo = getSnapshotInfo()
if (snapshotInfo.latest) {
return snapshotInfo.latest
}
snapshotInfo.latest = toMD5(JSON.stringify(await getLocalListData()))
saveSnapshotInfo(snapshotInfo)
return snapshotInfo.latest
}

View File

@ -1,59 +1,84 @@
// import { throttle } from '@common/utils/common'
import { type Server } from 'socket.io'
// import { sendSyncActionList } from '@main/modules/winMain'
import { encryptMsg, decryptMsg } from '../server/utils'
import { createCipheriv, createDecipheriv, publicEncrypt, privateDecrypt, constants } from 'node:crypto'
import os, { networkInterfaces } from 'node:os'
import cp from 'node:child_process'
import { LIST_IDS } from '@common/constants'
let io: Server | null
let removeListener: (() => void) | null
type listAction = 'list:action'
export const getAddress = (): string[] => {
const nets = networkInterfaces()
const results: string[] = []
// console.log(nets)
const handleListAction = ({ action, data }: LX.Sync.ActionList) => {
// console.log('handleListAction', action)
switch (action) {
case 'list_data_overwrite':
void global.lx.event_list.list_data_overwrite(data, true)
break
case 'list_create':
void global.lx.event_list.list_create(data.position, data.listInfos, true)
break
case 'list_remove':
void global.lx.event_list.list_remove(data, true)
break
case 'list_update':
void global.lx.event_list.list_update(data, true)
break
case 'list_update_position':
void global.lx.event_list.list_update_position(data.position, data.ids, true)
break
case 'list_music_add':
void global.lx.event_list.list_music_add(data.id, data.musicInfos, data.addMusicLocationType, true)
break
case 'list_music_move':
void global.lx.event_list.list_music_move(data.fromId, data.toId, data.musicInfos, data.addMusicLocationType, true)
break
case 'list_music_remove':
void global.lx.event_list.list_music_remove(data.listId, data.ids, true)
break
case 'list_music_update':
void global.lx.event_list.list_music_update(data, true)
break
case 'list_music_update_position':
void global.lx.event_list.list_music_update_position(data.listId, data.position, data.ids, true)
break
case 'list_music_overwrite':
void global.lx.event_list.list_music_overwrite(data.listId, data.musicInfos, true)
break
case 'list_music_clear':
void global.lx.event_list.list_music_clear(data, true)
break
default:
break
for (const interfaceInfos of Object.values(nets)) {
if (!interfaceInfos) continue
// Skip over non-IPv4 and internal (i.e. 127.0.0.1) addresses
for (const interfaceInfo of interfaceInfos) {
if (interfaceInfo.family === 'IPv4' && !interfaceInfo.internal) {
results.push(interfaceInfo.address)
}
}
}
return results
}
const registerListActionEvent = () => {
// https://stackoverflow.com/a/75309339
export const getComputerName = () => {
let name: string | undefined
switch (process.platform) {
case 'win32':
name = process.env.COMPUTERNAME
break
case 'darwin':
name = cp.execSync('scutil --get ComputerName').toString().trim()
break
case 'linux':
name = cp.execSync('hostnamectl --pretty').toString().trim()
break
}
if (!name) name = os.hostname()
return name
}
export const aesEncrypt = (text: string, key: string) => {
const cipher = createCipheriv('aes-128-ecb', Buffer.from(key, 'base64'), '')
return Buffer.concat([cipher.update(Buffer.from(text)), cipher.final()]).toString('base64')
}
export const aesDecrypt = (text: string, key: string) => {
const decipher = createDecipheriv('aes-128-ecb', Buffer.from(key, 'base64'), '')
return Buffer.concat([decipher.update(Buffer.from(text, 'base64')), decipher.final()]).toString()
}
export const rsaEncrypt = (buffer: Buffer, key: string): string => {
return publicEncrypt({ key, padding: constants.RSA_PKCS1_OAEP_PADDING }, buffer).toString('base64')
}
export const rsaDecrypt = (buffer: Buffer, key: string): Buffer => {
return privateDecrypt({ key, padding: constants.RSA_PKCS1_OAEP_PADDING }, buffer)
}
export const getLocalListData = async(): Promise<LX.Sync.ListData> => {
const lists: LX.Sync.ListData = {
defaultList: await global.lx.worker.dbService.getListMusics(LIST_IDS.DEFAULT),
loveList: await global.lx.worker.dbService.getListMusics(LIST_IDS.LOVE),
userList: [],
}
const userListInfos = await global.lx.worker.dbService.getAllUserList()
for await (const list of userListInfos) {
lists.userList.push(await global.lx.worker.dbService.getListMusics(list.id)
.then(musics => ({ ...list, list: musics })))
}
return lists
}
export const setLocalListData = async(listData: LX.Sync.ListData) => {
await global.lx.event_list.list_data_overwrite(listData, true)
}
export const registerListActionEvent = (sendListAction: (action: LX.Sync.ActionList) => (void | Promise<void>)) => {
const list_data_overwrite = async(listData: MakeOptional<LX.List.ListDataFull, 'tempList'>, isRemote: boolean = false) => {
if (isRemote) return
await sendListAction({ action: 'list_data_overwrite', data: listData })
@ -130,48 +155,48 @@ const registerListActionEvent = () => {
}
}
// const addMusic = (orderId, callback) => {
// // ...
// }
export const handleRemoteListAction = async({ action, data }: LX.Sync.ActionList) => {
// console.log('handleRemoteListAction', action)
const broadcast = async(action: listAction, data: any, excludeIds: string[] = []) => {
if (!io) return
const sockets: LX.Sync.RemoteSocket[] = await io.fetchSockets()
for (const socket of sockets) {
if (excludeIds.includes(socket.data.keyInfo.clientId) || !socket.data.isReady) continue
socket.emit(action, encryptMsg(socket.data.keyInfo, data))
}
}
export const sendListAction = async(action: LX.Sync.ActionList) => {
console.log('sendListAction', action.action)
// io.sockets
await broadcast('list:action', JSON.stringify(action))
}
export const registerListHandler = (_io: Server, socket: LX.Sync.Socket) => {
if (!io) {
io = _io
removeListener = registerListActionEvent()
}
socket.on('list:action', msg => {
if (!socket.data.isReady) return
// console.log(msg)
msg = decryptMsg(socket.data.keyInfo, msg)
if (!msg) return
handleListAction(JSON.parse(msg))
void 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)
}
export const unregisterListHandler = () => {
io = null
if (removeListener) {
removeListener()
removeListener = null
switch (action) {
case 'list_data_overwrite':
void global.lx.event_list.list_data_overwrite(data, true)
break
case 'list_create':
void global.lx.event_list.list_create(data.position, data.listInfos, true)
break
case 'list_remove':
void global.lx.event_list.list_remove(data, true)
break
case 'list_update':
void global.lx.event_list.list_update(data, true)
break
case 'list_update_position':
void global.lx.event_list.list_update_position(data.position, data.ids, true)
break
case 'list_music_add':
void global.lx.event_list.list_music_add(data.id, data.musicInfos, data.addMusicLocationType, true)
break
case 'list_music_move':
void global.lx.event_list.list_music_move(data.fromId, data.toId, data.musicInfos, data.addMusicLocationType, true)
break
case 'list_music_remove':
void global.lx.event_list.list_music_remove(data.listId, data.ids, true)
break
case 'list_music_update':
void global.lx.event_list.list_music_update(data, true)
break
case 'list_music_update_position':
void global.lx.event_list.list_music_update_position(data.listId, data.position, data.ids, true)
break
case 'list_music_overwrite':
void global.lx.event_list.list_music_overwrite(data.listId, data.musicInfos, true)
break
case 'list_music_clear':
void global.lx.event_list.list_music_clear(data, true)
break
default:
return false
}
return true
}

View File

@ -1,6 +1,6 @@
import { mainHandle } from '@common/mainIpc'
import { WIN_MAIN_RENDERER_EVENT_NAME } from '@common/ipcNames'
import { startServer, stopServer, getStatus, generateCode } from '@main/modules/sync'
import { startServer, stopServer, getServerStatus, generateCode, connectServer, disconnectServer, getClientStatus } from '@main/modules/sync'
import { sendEvent } from '../main'
let selectModeListenr: ((mode: LX.Sync.Mode) => void) | null = null
@ -8,13 +8,15 @@ let selectModeListenr: ((mode: LX.Sync.Mode) => void) | null = null
export default () => {
mainHandle<LX.Sync.SyncServiceActions, any>(WIN_MAIN_RENDERER_EVENT_NAME.sync_action, async({ params: data }) => {
switch (data.action) {
case 'enable':
case 'enable_server':
data.data.enable ? await startServer(parseInt(data.data.port)) : await stopServer()
return
case 'get_status':
return getStatus()
case 'generate_code':
return await generateCode()
case 'enable_client':
data.data.enable ? await connectServer(data.data.host, data.data.authCode) : await disconnectServer()
return
case 'get_server_status': return getServerStatus()
case 'get_client_status': return getClientStatus()
case 'generate_code': return generateCode()
case 'select_mode':
if (selectModeListenr) selectModeListenr(data.data)
break
@ -29,18 +31,24 @@ export const sendSyncAction = (data: LX.Sync.SyncMainWindowActions) => {
sendEvent(WIN_MAIN_RENDERER_EVENT_NAME.sync_action, data)
}
export const sendStatus = (status: LX.Sync.Status) => {
export const sendClientStatus = (status: LX.Sync.ClientStatus) => {
sendSyncAction({
action: 'status',
action: 'client_status',
data: status,
})
}
export const sendSelectMode = (keyInfo: LX.Sync.KeyInfo, listener: (mode: LX.Sync.Mode) => void) => {
export const sendServerStatus = (status: LX.Sync.ServerStatus) => {
sendSyncAction({
action: 'server_status',
data: status,
})
}
export const sendSelectMode = (deviceName: string, listener: (mode: LX.Sync.Mode) => void) => {
selectModeListenr = listener
sendSyncAction({ action: 'select_mode', data: keyInfo })
return () => {
selectModeListenr = null
}
sendSyncAction({ action: 'select_mode', data: deviceName })
}
export const removeSelectModeListener = () => {
selectModeListenr = null
}
export const sendCloseSelectMode = () => {
sendSyncAction({ action: 'close_select_mode' })

View File

@ -1,4 +1,4 @@
import { Socket as _Socket, RemoteSocket as _RemoteSocket } from 'socket.io'
import type WS from 'ws'
type DefaultEventsMap = Record<string, (...args: any[]) => void>
@ -6,24 +6,57 @@ type DefaultEventsMap = Record<string, (...args: any[]) => void>
declare global {
namespace LX {
namespace Sync {
class Socket extends _Socket {
data: SocketData
namespace Client {
interface Socket extends WS.WebSocket {
isReady: boolean
data: {
keyInfo: ClientKeyInfo
urlInfo: UrlInfo
}
onRemoteEvent: <T extends keyof LX.Sync.ActionSyncSendType>(
eventName: T,
handler: (data: LX.Sync.ActionSyncSendType[T]) => (void | Promise<void>)
) => () => void
onClose: (handler: (err: Error) => (void | Promise<void>)) => () => void
sendData: <T extends keyof LX.Sync.ActionSyncType>(
eventName: T,
data?: LX.Sync.ActionSyncType[T],
callback?: (err?: Error) => void
) => void
}
interface UrlInfo {
wsProtocol: string
httpProtocol: string
hostPath: string
href: string
}
}
class RemoteSocket extends _RemoteSocket<DefaultEventsMap, any> {
readonly data: SocketData
namespace Server {
interface Socket extends WS.WebSocket {
isAlive?: boolean
isMobile: boolean
isReady: boolean
keyInfo: LX.Sync.ServerKeyInfo
onRemoteEvent: <T extends keyof LX.Sync.ActionSyncType>(
eventName: T,
handler: (data: LX.Sync.ActionSyncType[T]) => void
) => () => void
onClose: (handler: (err: Error) => (void | Promise<void>)) => () => void
sendData: <T extends keyof LX.Sync.ActionSyncSendType>(
eventName: T,
data?: LX.Sync.ActionSyncSendType[T],
callback?: (err?: Error) => void
) => void
}
type SocketServer = WS.Server<Socket>
}
interface Data {
action: string
data: any
}
interface SocketData {
snapshotFilePath: string
isCreatedSnapshot: boolean
keyInfo: KeyInfo
isReady: boolean
}
type Action = 'list:sync'
type ListAction = 'getData' | 'finished'
}
}

221
src/main/utils/request.ts Normal file
View File

@ -0,0 +1,221 @@
import needle, { type NeedleHttpVerbs, type NeedleOptions, type BodyData, type NeedleCallback, type NeedleResponse } from 'needle'
// import progress from 'request-progress'
import { httpOverHttp, httpsOverHttp } from 'tunnel'
import { type ClientRequest } from 'node:http'
// import fs from 'fs'
export const requestMsg = {
fail: '请求异常😮,可以多试几次,若还是不行就换一首吧。。。',
unachievable: '哦No😱...接口无法访问了!',
timeout: '请求超时',
// unachievable: '哦No😱...接口无法访问了!已帮你切换到临时接口,重试下看能不能播放吧~',
notConnectNetwork: '无法连接到服务器',
cancelRequest: '取消http请求',
} as const
const httpsRxp = /^https:/
let envProxy: null | { host: string, port: string } = null
const getRequestAgent = (url: string) => {
if (envProxy == null) {
if (global.envParams.cmdParams['proxy-server'] && typeof global.envParams.cmdParams['proxy-server'] == 'string') {
const [host, port = ''] = global.envParams.cmdParams['proxy-server'].split(':')
envProxy = {
host,
port,
}
}
}
const proxy = {
enable: global.lx.appSetting['network.proxy.enable'],
host: global.lx.appSetting['network.proxy.host'],
port: global.lx.appSetting['network.proxy.port'],
envProxy,
}
let options
if (global.lx.appSetting['network.proxy.enable'] && proxy.host) {
options = {
proxy: {
host: proxy.host,
port: parseInt(proxy.port || '80'),
},
}
} else if (proxy.envProxy) {
options = {
proxy: {
host: proxy.envProxy.host,
port: parseInt(proxy.envProxy.port || '80'),
},
}
}
return options ? (httpsRxp.test(url) ? httpsOverHttp : httpOverHttp)(options) : undefined
}
export interface RequestOptions extends NeedleOptions {
method?: NeedleHttpVerbs
body?: BodyData
form?: BodyData
formData?: BodyData
}
export type RequestCallback = NeedleCallback
type RequestResponse = NeedleResponse
const request = (url: string, options: RequestOptions, callback: RequestCallback): ClientRequest => {
let data: BodyData = null
if (options.body) {
data = options.body
} else if (options.form) {
data = options.form
// data.content_type = 'application/x-www-form-urlencoded'
options.json = false
} else if (options.formData) {
data = options.formData
// data.content_type = 'multipart/form-data'
options.json = false
}
options.response_timeout = options.timeout
return needle.request(options.method ?? 'get', url, data, options, (err, resp, body) => {
if (!err) {
body = resp.body = resp.raw.toString()
try {
resp.body = JSON.parse(resp.body)
} catch (_) {}
body = resp.body
}
callback(err, resp, body)
// @ts-expect-error
}).request
}
const defaultHeaders = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36',
}
// var proxyUrl = "http://" + user + ":" + password + "@" + host + ":" + port;
// var proxiedRequest = request.defaults({'proxy': proxyUrl});
// interface RequestPromise extends Promise<RequestResponse> {
// abort: () => void
// }
/**
* promise
* @param {*} url
* @param {*} options
*/
const buildHttpPromose = async(url: string, options: RequestOptions): Promise<RequestResponse> => {
return new Promise((resolve, reject) => {
void fetchData(url, options.method, options, (err, resp, body) => {
// options.isShowProgress && window.api.hideProgress()
// debugRequest && console.log(`\n---response------${url}------------`)
// debugRequest && console.log(body)
// obj.requestObj = null
// obj.cancelFn = null
if (err) {
reject(err)
return
}
resolve(resp)
})
// .then(request => {
// // obj.requestObj = ro
// // if (obj.isCancelled) obj.cancelHttp()
// promise.abort = () => {
// request.destroy(new Error('cancelled'))
// }
// })
})
// let obj = {
// isCancelled: false,
// cancelHttp: () => {
// if (!obj.requestObj) return obj.isCancelled = true
// cancelHttp(obj.requestObj)
// obj.requestObj = null
// obj.promise = obj.cancelHttp = null
// obj.cancelFn(new Error(requestMsg.cancelRequest))
// obj.cancelFn = null
// },
// }
// obj.promise = new Promise((resolve, reject) => {
// obj.cancelFn = reject
// debugRequest && console.log(`\n---send request------${url}------------`)
// fetchData(url, options.method, options, (err, resp, body) => {
// // options.isShowProgress && window.api.hideProgress()
// debugRequest && console.log(`\n---response------${url}------------`)
// debugRequest && console.log(body)
// obj.requestObj = null
// obj.cancelFn = null
// if (err) { reject(err); return }
// resolve(resp)
// }).then(ro => {
// obj.requestObj = ro
// if (obj.isCancelled) obj.cancelHttp()
// })
// })
// return obj
}
/**
*
* @param {*} url
* @param {*} options
*/
export const httpFetch = async(url: string, options: RequestOptions = { method: 'get' }) => {
return buildHttpPromose(url, options).catch(async(err: any) => {
// console.log('出错', err)
if (err.message === 'socket hang up') {
// window.globalObj.apiSource = 'temp'
throw new Error(requestMsg.unachievable)
}
switch (err.code) {
case 'ETIMEDOUT':
case 'ESOCKETTIMEDOUT':
throw new Error(requestMsg.timeout)
case 'ENOTFOUND':
throw new Error(requestMsg.notConnectNetwork)
default:
throw err
}
})
// requestObj.promise = requestObj.promise.catch(async err => {
// // console.log('出错', err)
// if (err.message === 'socket hang up') {
// // window.globalObj.apiSource = 'temp'
// return Promise.reject(new Error(requestMsg.unachievable))
// }
// switch (err.code) {
// case 'ETIMEDOUT':
// case 'ESOCKETTIMEDOUT':
// return Promise.reject(new Error(requestMsg.timeout))
// case 'ENOTFOUND':
// return Promise.reject(new Error(requestMsg.notConnectNetwork))
// default:
// return Promise.reject(err)
// }
// })
// return requestPromise
}
const fetchData = async(url: string, method: RequestOptions['method'], {
headers = {},
format = 'json',
timeout = 15000,
...options
}, callback: RequestCallback) => {
// console.log(url, options)
console.log('---start---', url)
headers = Object.assign({}, headers)
return request(url, {
...options,
method,
headers: Object.assign({}, defaultHeaders, headers),
timeout,
agent: getRequestAgent(url),
json: format === 'json',
}, (err, resp, body) => {
callback(err, resp, body)
})
}

View File

@ -11,6 +11,7 @@
<layout-update-modal />
<layout-pact-modal />
<layout-sync-mode-modal />
<layout-sync-auth-code-modal />
<layout-play-detail />
</div>
</template>

View File

@ -0,0 +1,106 @@
<template>
<material-modal :show="sync.isShowAuthCodeModal" :bg-close="false" @close="handleClose" @after-enter="$refs.input.focus()">
<main :class="$style.main">
<h2>{{ $t('sync__auth_code_title') }}</h2>
<base-input
ref="input"
v-model="authCode"
:class="$style.input"
:placeholder="$t('sync__auth_code_input_tip')"
@submit="handleSubmit" @blur="verify"
/>
<div :class="$style.footer">
<base-btn :class="$style.btn" @click="handleSubmit">{{ $t('btn_confirm') }}</base-btn>
</div>
</main>
</material-modal>
</template>
<script>
import { ref } from '@common/utils/vueTools'
import { sync } from '@renderer/store'
import { appSetting } from '@renderer/store/setting'
import { sendSyncAction } from '@renderer/utils/ipc'
export default {
setup() {
const authCode = ref('')
const handleClose = () => {
sync.isShowAuthCodeModal = false
}
const verify = () => {
if (authCode.value.length > 100) authCode.value = authCode.value.substring(0, 100)
return authCode.value
}
const handleSubmit = () => {
let code = verify()
if (code == '') return
handleClose()
sendSyncAction({
action: 'enable_client',
data: {
enable: appSetting['sync.enable'],
host: appSetting['sync.client.host'],
authCode: code,
},
})
}
return {
sync,
authCode,
handleClose,
verify,
handleSubmit,
}
},
}
</script>
<style lang="less" module>
@import '@renderer/assets/styles/layout.less';
.main {
padding: 0 15px;
max-width: 530px;
min-width: 280px;
display: flex;
flex-flow: column nowrap;
min-height: 0;
// max-height: 100%;
// overflow: hidden;
h2 {
font-size: 13px;
color: var(--color-font);
line-height: 1.3;
word-break: break-all;
// text-align: center;
padding: 15px 0 8px;
}
}
.input {
// width: 100%;
// height: 26px;
padding: 8px 8px;
}
.footer {
margin: 20px 0 15px auto;
}
.btn {
// box-sizing: border-box;
// margin-left: 15px;
// margin-bottom: 15px;
// height: 36px;
// line-height: 36px;
// padding: 0 10px !important;
min-width: 70px;
// .mixin-ellipsis-1;
+.btn {
margin-left: 10px;
}
}
</style>

View File

@ -23,7 +23,7 @@
<dl :class="$style.btnGroup">
<dt :class="$style.label">{{ $t('sync__other_label') }}</dt>
<dd :class="$style.btns">
<base-btn :class="$style.btn" @click="handleSelectMode('none')">{{ $t('sync__overwrite_btn_none') }}</base-btn>
<!-- <base-btn :class="$style.btn" @click="handleSelectMode('none')">{{ $t('sync__overwrite_btn_none') }}</base-btn> -->
<base-btn :class="$style.btn" @click="handleSelectMode('cancel')">{{ $t('sync__overwrite_btn_cancel') }}</base-btn>
</dd>
</dl>

View File

@ -32,25 +32,62 @@ export default () => {
immediate: true,
})
watch(() => appSetting['sync.mode'], (mode) => {
sync.mode = mode
})
watch(() => appSetting['sync.enable'], enable => {
void sendSyncAction({
action: 'enable',
data: {
enable,
port: appSetting['sync.port'],
},
})
switch (appSetting['sync.mode']) {
case 'server':
if (appSetting['sync.server.port']) {
void sendSyncAction({
action: 'enable_server',
data: {
enable: appSetting['sync.enable'],
port: appSetting['sync.server.port'],
},
})
}
break
case 'client':
if (appSetting['sync.client.host']) {
void sendSyncAction({
action: 'enable_client',
data: {
enable: appSetting['sync.enable'],
host: appSetting['sync.client.host'],
},
})
}
break
default:
break
}
sync.enable = enable
})
watch(() => appSetting['sync.port'], port => {
void sendSyncAction({
action: 'enable',
data: {
enable: appSetting['sync.enable'],
port: appSetting['sync.port'],
},
})
sync.port = port
watch(() => appSetting['sync.server.port'], port => {
if (appSetting['sync.mode'] == 'server') {
void sendSyncAction({
action: 'enable_server',
data: {
enable: appSetting['sync.enable'],
port: appSetting['sync.server.port'],
},
})
}
sync.server.port = port
})
watch(() => appSetting['sync.client.host'], host => {
if (appSetting['sync.mode'] == 'client') {
void sendSyncAction({
action: 'enable_client',
data: {
enable: appSetting['sync.enable'],
host: appSetting['sync.client.host'],
},
})
}
sync.client.host = host
})
watch(() => appSetting['network.proxy.enable'], enable => {

View File

@ -2,23 +2,33 @@ import { markRaw, onBeforeUnmount } from '@common/utils/vueTools'
import { onSyncAction, sendSyncAction } from '@renderer/utils/ipc'
import { sync } from '@renderer/store'
import { appSetting } from '@renderer/store/setting'
import { SYNC_CODE } from '@common/constants'
export default () => {
const handleSyncList = (event: LX.Sync.SyncMainWindowActions) => {
console.log(event)
switch (event.action) {
case 'select_mode':
sync.deviceName = event.data.deviceName
sync.deviceName = event.data
sync.isShowSyncMode = true
break
case 'close_select_mode':
sync.isShowSyncMode = false
break
case 'status':
sync.status.status = event.data.status
sync.status.message = event.data.message
sync.status.address = markRaw(event.data.address)
sync.status.code = event.data.code
sync.status.devices = markRaw(event.data.devices)
case 'server_status':
sync.server.status.status = event.data.status
sync.server.status.message = event.data.message
sync.server.status.address = markRaw(event.data.address)
sync.server.status.code = event.data.code
sync.server.status.devices = markRaw(event.data.devices)
break
case 'client_status':
sync.client.status.status = event.data.status
sync.client.status.message = event.data.message
sync.client.status.address = markRaw(event.data.address)
if (event.data.message == SYNC_CODE.missingAuthCode || event.data.message == SYNC_CODE.authFailed) {
if (!sync.isShowAuthCodeModal) sync.isShowAuthCodeModal = true
} else if (sync.isShowAuthCodeModal) sync.isShowAuthCodeModal = false
break
}
}
@ -33,15 +43,36 @@ export default () => {
return async() => {
sync.enable = appSetting['sync.enable']
sync.port = appSetting['sync.port']
if (appSetting['sync.enable'] && appSetting['sync.port']) {
void sendSyncAction({
action: 'enable',
data: {
enable: appSetting['sync.enable'],
port: appSetting['sync.port'],
},
})
sync.mode = appSetting['sync.mode']
sync.server.port = appSetting['sync.server.port']
sync.client.host = appSetting['sync.client.host']
if (appSetting['sync.enable']) {
switch (appSetting['sync.mode']) {
case 'server':
if (appSetting['sync.server.port']) {
void sendSyncAction({
action: 'enable_server',
data: {
enable: appSetting['sync.enable'],
port: appSetting['sync.server.port'],
},
})
}
break
case 'client':
if (appSetting['sync.client.host']) {
void sendSyncAction({
action: 'enable_client',
data: {
enable: appSetting['sync.enable'],
host: appSetting['sync.client.host'],
},
})
}
break
default:
break
}
}
}
}

View File

@ -27,27 +27,51 @@ export const proxy: {
}
export const sync: {
enable: boolean
port: string
mode: LX.AppSetting['sync.mode']
isShowSyncMode: boolean
isShowAuthCodeModal: boolean
deviceName: string
status: {
status: boolean
message: string
address: string[]
code: string
devices: LX.Sync.KeyInfo[]
server: {
port: string
status: {
status: boolean
message: string
address: string[]
code: string
devices: LX.Sync.ServerKeyInfo[]
}
}
client: {
host: string
status: {
status: boolean
message: string
address: string[]
}
}
} = reactive({
enable: false,
port: '',
mode: 'server',
isShowSyncMode: false,
isShowAuthCodeModal: false,
deviceName: '',
status: {
status: false,
message: '',
address: [],
code: '',
devices: [],
server: {
port: '',
status: {
status: false,
message: '',
address: [],
code: '',
devices: [],
},
},
client: {
host: '',
status: {
status: false,
message: '',
address: [],
},
},
})

View File

@ -4,18 +4,37 @@ dt#sync
button(class="help-btn" @click="openUrl('https://lyswhut.github.io/lx-music-doc/desktop/faq/sync')" :aria-label="$t('setting__sync_tip')")
svg-icon(name="help-circle-outline")
dd
base-checkbox(id="setting_sync_enable" :modelValue="appSetting['sync.enable']" @update:modelValue="updateSetting({ 'sync.enable': $event })" :label="syncEnableTitle")
div
p.small {{$t('setting__sync_auth_code', { code: sync.status.code || '' })}}
p.small {{$t('setting__sync_address', { address: sync.status.address.join(', ') || '' })}}
p.small {{$t('setting__sync_device', { devices: syncDevices })}}
p
base-btn.btn(min :disabled="!sync.status.status" @click="refreshSyncCode") {{$t('setting__sync_refresh_code')}}
base-checkbox(id="setting_sync_enable" :modelValue="appSetting['sync.enable']" @update:modelValue="updateSetting({ 'sync.enable': $event })" :label="$t('setting__sync_enable')")
dd
h3#sync_port {{$t('setting__sync_port')}}
h3#sync_mode {{$t('setting__sync_mode')}}
div
base-checkbox.gap-left(id="setting_sync_mode_server" :disabled="sync.enable" :modelValue="appSetting['sync.mode']" @update:modelValue="updateSetting({ 'sync.mode': $event })" need value="server" :label="$t('setting__sync_mode_server')")
base-checkbox.gap-left(id="setting_sync_mode_client" :disabled="sync.enable" :modelValue="appSetting['sync.mode']" @update:modelValue="updateSetting({ 'sync.mode': $event })" need value="client" :label="$t('setting__sync_mode_client')")
dd(v-if="sync.mode == 'client'")
h3 {{$t('setting__sync_client_mode')}}
div
p.small {{$t('setting__sync_client_status', { status: clientStatus })}}
p.small {{$t('setting__sync_client_address', { address: sync.client.status.address.join(', ') || '' })}}
p
base-input.gap-left(:modelValue="appSetting['sync.port']" type="number" @update:modelValue="setSyncPort" :placeholder="$t('setting__sync_port_tip')")
p.small {{$t('setting__sync_client_host')}}
div
base-input.gap-left(:class="$style.hostInput" :modelValue="appSetting['sync.client.host']" :disabled="sync.enable" @update:modelValue="setSyncClientHost" :placeholder="$t('setting__sync_client_host_tip')")
dd(v-else)
h3 {{syncEnableServerTitle}}
div
p.small {{$t('setting__sync_server_auth_code', { code: sync.server.status.code || '' })}}
p.small {{$t('setting__sync_server_address', { address: sync.server.status.address.join(', ') || '' })}}
p.small {{$t('setting__sync_server_device', { devices: syncDevices })}}
p
base-btn.btn(min :disabled="!sync.server.status.status" @click="refreshSyncCode") {{$t('setting__sync_server_refresh_code')}}
p
p.small {{$t('setting__sync_server_port')}}
div
base-input.gap-left(:class="$style.portInput" :modelValue="appSetting['sync.server.port']" :disabled="sync.enable" type="number" @update:modelValue="setSyncServerPort" :placeholder="$t('setting__sync_server_port_tip')")
</template>
<script>
@ -26,6 +45,7 @@ import { openUrl } from '@common/utils/electron'
import { useI18n } from '@renderer/plugins/i18n'
import { appSetting, updateSetting } from '@renderer/store/setting'
import { debounce } from '@common/utils/common'
import { SYNC_CODE } from '@common/constants'
export default {
name: 'SettingSync',
@ -33,19 +53,39 @@ export default {
const t = useI18n()
const syncEnableTitle = computed(() => {
let title = t('setting__sync_enable')
if (sync.status.message) {
title += ` [${sync.status.message}]`
const syncEnableServerTitle = computed(() => {
let title = t('setting__sync_server_mode')
if (sync.server.status.message) {
title += ` [${sync.server.status.message}]`
}
// else if (this.sync.status.address.length) {
// // title += ` [${this.sync.status.address.join(', ')}]`
// else if (this.sync.server.status.address.length) {
// // title += ` [${this.sync.server.status.address.join(', ')}]`
// }
return title
})
const clientStatus = computed(() => {
let status
switch (sync.client.status.message) {
case SYNC_CODE.msgBlockedIp:
status = t('setting__sync_code_blocked_ip')
break
case SYNC_CODE.authFailed:
status = t('setting__sync_code_fail')
break
default:
status = sync.client.status.message
? sync.client.status.message
: sync.client.status.status
? t('setting_sync_status_enabled')
: t('sync_status_disabled')
break
}
return status
})
const syncDevices = computed(() => {
return sync.status.devices.length
? sync.status.devices.map(d => `${d.deviceName} (${d.clientId.substring(0, 5)})`).join(', ')
return sync.server.status.devices.length
? sync.server.status.devices.map(d => `${d.deviceName} (${d.clientId.substring(0, 5)})`).join(', ')
: ''
})
@ -53,8 +93,12 @@ export default {
sendSyncAction({ action: 'generate_code' })
}
const setSyncPort = debounce(port => {
updateSetting({ 'sync.port': port.trim() })
const setSyncServerPort = debounce(port => {
updateSetting({ 'sync.server.port': port.trim() })
}, 500)
const setSyncClientHost = debounce(host => {
updateSetting({ 'sync.client.host': host.trim() })
}, 500)
@ -62,10 +106,12 @@ export default {
appSetting,
updateSetting,
sync,
syncEnableTitle,
setSyncPort,
syncEnableServerTitle,
setSyncServerPort,
setSyncClientHost,
syncDevices,
refreshSyncCode,
clientStatus,
openUrl,
}
},
@ -76,4 +122,12 @@ export default {
.save-path {
font-size: 12px;
}
.portInput[disabled], .hostInput[disabled] {
opacity: .8 !important;
}
.hostInput {
min-width: 380px;
}
</style>