重构数据同步功能,新增客户端模式
parent
b42092884e
commit
3b70acc3ca
File diff suppressed because it is too large
Load Diff
|
@ -282,7 +282,6 @@
|
||||||
"electron-log": "^4.4.8",
|
"electron-log": "^4.4.8",
|
||||||
"electron-store": "^8.1.0",
|
"electron-store": "^8.1.0",
|
||||||
"font-list": "^1.4.5",
|
"font-list": "^1.4.5",
|
||||||
"http-terminator": "^3.2.0",
|
|
||||||
"iconv-lite": "^0.6.3",
|
"iconv-lite": "^0.6.3",
|
||||||
"image-size": "^1.0.2",
|
"image-size": "^1.0.2",
|
||||||
"jschardet": "^3.0.0",
|
"jschardet": "^3.0.0",
|
||||||
|
@ -291,12 +290,12 @@
|
||||||
"music-metadata": "^8.1.3",
|
"music-metadata": "^8.1.3",
|
||||||
"needle": "github:lyswhut/needle#93299ac841b7e9a9f82ca7279b88aaaeda404060",
|
"needle": "github:lyswhut/needle#93299ac841b7e9a9f82ca7279b88aaaeda404060",
|
||||||
"node-id3": "^0.2.5",
|
"node-id3": "^0.2.5",
|
||||||
"socket.io": "^4.6.0",
|
|
||||||
"sortablejs": "^1.15.0",
|
"sortablejs": "^1.15.0",
|
||||||
"tunnel": "^0.0.6",
|
"tunnel": "^0.0.6",
|
||||||
"utf-8-validate": "^6.0.2",
|
"utf-8-validate": "^6.0.2",
|
||||||
"vue": "^3.2.47",
|
"vue": "^3.2.47",
|
||||||
"vue-router": "^4.1.6"
|
"vue-router": "^4.1.6",
|
||||||
|
"ws": "^8.12.1"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"got": "^11",
|
"got": "^11",
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
|
### 新增
|
||||||
|
|
||||||
|
- 重构数据同步功能,新增客户端模式
|
||||||
|
|
||||||
### 优化
|
### 优化
|
||||||
|
|
||||||
|
|
|
@ -75,3 +75,24 @@ export const DOWNLOAD_STATUS = {
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export const QUALITYS = ['flac24bit', 'flac', 'wav', 'ape', '320k', '192k', '128k'] 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
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { join } from 'path'
|
||||||
import { homedir } from 'os'
|
import { homedir } from 'os'
|
||||||
|
|
||||||
const defaultSetting: LX.AppSetting = {
|
const defaultSetting: LX.AppSetting = {
|
||||||
version: '2.0.0',
|
version: '2.1.0',
|
||||||
|
|
||||||
'common.windowSizeId': 3,
|
'common.windowSizeId': 3,
|
||||||
'common.fontSize': 16,
|
'common.fontSize': 16,
|
||||||
|
@ -108,8 +108,11 @@ const defaultSetting: LX.AppSetting = {
|
||||||
// 'tray.isToTray': false,
|
// 'tray.isToTray': false,
|
||||||
'tray.themeId': 0,
|
'tray.themeId': 0,
|
||||||
|
|
||||||
|
'sync.mode': 'server',
|
||||||
'sync.enable': false,
|
'sync.enable': false,
|
||||||
'sync.port': '23332',
|
'sync.server.port': '23332',
|
||||||
|
'sync.server.maxSsnapshotNum': 5,
|
||||||
|
'sync.client.host': '',
|
||||||
|
|
||||||
'theme.id': 'blue_plus',
|
'theme.id': 'blue_plus',
|
||||||
// 'theme.id': 'green',
|
// 'theme.id': 'green',
|
||||||
|
|
|
@ -499,6 +499,11 @@ declare global {
|
||||||
*/
|
*/
|
||||||
'tray.themeId': number
|
'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
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 是否在离开搜索界面时自动清空搜索框
|
* 是否在离开搜索界面时自动清空搜索框
|
||||||
|
|
|
@ -1,10 +1,15 @@
|
||||||
declare namespace LX {
|
declare namespace LX {
|
||||||
namespace Sync {
|
namespace Sync {
|
||||||
|
|
||||||
interface Enable {
|
interface EnableServer {
|
||||||
enable: boolean
|
enable: boolean
|
||||||
port: string
|
port: string
|
||||||
}
|
}
|
||||||
|
interface EnableClient {
|
||||||
|
enable: boolean
|
||||||
|
host: string
|
||||||
|
authCode?: string
|
||||||
|
}
|
||||||
|
|
||||||
interface SyncActionBase <A> {
|
interface SyncActionBase <A> {
|
||||||
action: A
|
action: A
|
||||||
|
@ -14,14 +19,17 @@ declare namespace LX {
|
||||||
}
|
}
|
||||||
type SyncAction<A, D = undefined> = D extends undefined ? SyncActionBase<A> : SyncActionData<A, D>
|
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<'close_select_mode'>
|
||||||
| SyncAction<'status', Status>
|
| SyncAction<'client_status', ClientStatus>
|
||||||
|
| SyncAction<'server_status', ServerStatus>
|
||||||
|
|
||||||
type SyncServiceActions = SyncAction<'select_mode', Mode>
|
type SyncServiceActions = SyncAction<'select_mode', Mode>
|
||||||
| SyncAction<'get_status'>
|
| SyncAction<'get_server_status'>
|
||||||
|
| SyncAction<'get_client_status'>
|
||||||
| SyncAction<'generate_code'>
|
| SyncAction<'generate_code'>
|
||||||
| SyncAction<'enable', Enable>
|
| SyncAction<'enable_server', EnableServer>
|
||||||
|
| SyncAction<'enable_client', EnableClient>
|
||||||
|
|
||||||
type ActionList = SyncAction<'list_data_overwrite', LX.List.ListActionDataOverwrite>
|
type ActionList = SyncAction<'list_data_overwrite', LX.List.ListActionDataOverwrite>
|
||||||
| SyncAction<'list_create', LX.List.ListActionAdd>
|
| SyncAction<'list_create', LX.List.ListActionAdd>
|
||||||
|
@ -36,25 +44,55 @@ declare namespace LX {
|
||||||
| SyncAction<'list_music_overwrite', LX.List.ListActionMusicOverwrite>
|
| SyncAction<'list_music_overwrite', LX.List.ListActionMusicOverwrite>
|
||||||
| SyncAction<'list_music_clear', LX.List.ListActionMusicClear>
|
| 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 {
|
interface List {
|
||||||
action: string
|
action: string
|
||||||
data: any
|
data: any
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Status {
|
interface ServerStatus {
|
||||||
status: boolean
|
status: boolean
|
||||||
message: string
|
message: string
|
||||||
address: string[]
|
address: string[]
|
||||||
code: 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
|
clientId: string
|
||||||
key: string
|
key: string
|
||||||
deviceName: string
|
deviceName: string
|
||||||
connectionTime?: number
|
lastSyncDate?: number
|
||||||
|
snapshotKey: string
|
||||||
|
isMobile: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
type ListData = Omit<LX.List.ListDataFull, 'tempList'>
|
type ListData = Omit<LX.List.ListDataFull, 'tempList'>
|
||||||
|
@ -65,7 +103,7 @@ declare namespace LX {
|
||||||
| 'overwrite_remote_local'
|
| 'overwrite_remote_local'
|
||||||
| 'overwrite_local_remote_full'
|
| 'overwrite_local_remote_full'
|
||||||
| 'overwrite_remote_local_full'
|
| 'overwrite_remote_local_full'
|
||||||
| 'none'
|
// | 'none'
|
||||||
| 'cancel'
|
| 'cancel'
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,3 +5,8 @@ type DeepPartial<T> = {
|
||||||
}
|
}
|
||||||
|
|
||||||
type Modify<T, R> = Omit<T, keyof R> & R
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -20,7 +20,7 @@ const oldThemeMap = {
|
||||||
export default (setting: any): Partial<LX.AppSetting> => {
|
export default (setting: any): Partial<LX.AppSetting> => {
|
||||||
setting = { ...setting }
|
setting = { ...setting }
|
||||||
|
|
||||||
// 迁移 v2 之前的配置
|
// 迁移 v2.0.0 之前的配置
|
||||||
if (compareVer(setting.version, '2.0.0') < 0) {
|
if (compareVer(setting.version, '2.0.0') < 0) {
|
||||||
// 迁移列表滚动位置设置 ~0.18.3
|
// 迁移列表滚动位置设置 ~0.18.3
|
||||||
if (setting.list?.scroll) {
|
if (setting.list?.scroll) {
|
||||||
|
@ -133,7 +133,16 @@ export default (setting: any): Partial<LX.AppSetting> => {
|
||||||
|
|
||||||
setting['odc.isAutoClearSearchInput'] = setting.odc?.isAutoClearSearchInput
|
setting['odc.isAutoClearSearchInput'] = setting.odc?.isAutoClearSearchInput
|
||||||
setting['odc.isAutoClearSearchList'] = setting.odc?.isAutoClearSearchList
|
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
|
return setting
|
||||||
}
|
}
|
||||||
|
|
|
@ -454,13 +454,24 @@
|
||||||
"setting__setting__desktop_lyric_font_weight_font": "verbatim lyrics",
|
"setting__setting__desktop_lyric_font_weight_font": "verbatim lyrics",
|
||||||
"setting__setting__desktop_lyric_font_weight_line": "progressive lyrics",
|
"setting__setting__desktop_lyric_font_weight_line": "progressive lyrics",
|
||||||
"setting__sync": "Data synchronization",
|
"setting__sync": "Data synchronization",
|
||||||
"setting__sync_address": "Synchronization service address: {address}",
|
"setting__sync_client_address": "Current device address: {address}",
|
||||||
"setting__sync_auth_code": "Connection code: {code}",
|
"setting__sync_client_host": "Synchronization service address",
|
||||||
"setting__sync_device": "Connected devices: {devices}",
|
"setting__sync_client_host_tip": "Please enter the synchronization service address",
|
||||||
"setting__sync_enable": "Enable the synchronization function (because the data is transmitted in clear text, please use it under a trusted network)",
|
"setting__sync_client_mode": "client mode",
|
||||||
"setting__sync_port": "Sync port settings",
|
"setting__sync_client_status": "Status: {status}",
|
||||||
"setting__sync_port_tip": "Please enter the synchronization service port number",
|
"setting__sync_code_blocked_ip": "The IP of the current device has been blocked by the server!",
|
||||||
"setting__sync_refresh_code": "Refresh the connection code",
|
"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__sync_tip": "For how to use it, please see the \"Sync function\" section of the FAQ",
|
||||||
"setting__update": "Update",
|
"setting__update": "Update",
|
||||||
"setting__update_checking": "Checking for updates...",
|
"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_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_try_auto_update": "Attempt to download updates automatically when a new version is found",
|
||||||
"setting__update_unknown": "Unknown",
|
"setting__update_unknown": "Unknown",
|
||||||
|
"setting_sync_status_enabled": "connected",
|
||||||
"song_list": "Playlists",
|
"song_list": "Playlists",
|
||||||
"songlist__import_input_btn_confirm": "Open",
|
"songlist__import_input_btn_confirm": "Open",
|
||||||
"songlist__import_input_show_btn": "Open Playlist",
|
"songlist__import_input_show_btn": "Open Playlist",
|
||||||
|
@ -502,6 +514,8 @@
|
||||||
"source_tx": "Tencent",
|
"source_tx": "Tencent",
|
||||||
"source_wy": "Netease",
|
"source_wy": "Netease",
|
||||||
"source_xm": "Xiami",
|
"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_local_remote": "Local list merge remote list",
|
||||||
"sync__merge_btn_remote_local": "Remote list merge local list",
|
"sync__merge_btn_remote_local": "Remote list merge local list",
|
||||||
"sync__merge_label": "Merge",
|
"sync__merge_label": "Merge",
|
||||||
|
@ -519,6 +533,7 @@
|
||||||
"sync__overwrite_tip": "Cover: ",
|
"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__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__title": "Choose how to synchronize the list with {name}",
|
||||||
|
"sync_status_disabled": "not connected",
|
||||||
"tag__high_quality": "HQ",
|
"tag__high_quality": "HQ",
|
||||||
"tag__lossless": "SQ",
|
"tag__lossless": "SQ",
|
||||||
"tag__lossless_24bit": "24bit",
|
"tag__lossless_24bit": "24bit",
|
||||||
|
|
|
@ -457,13 +457,24 @@
|
||||||
"setting__setting__desktop_lyric_font_weight_font": "逐字歌词",
|
"setting__setting__desktop_lyric_font_weight_font": "逐字歌词",
|
||||||
"setting__setting__desktop_lyric_font_weight_line": "逐行歌词",
|
"setting__setting__desktop_lyric_font_weight_line": "逐行歌词",
|
||||||
"setting__sync": "数据同步",
|
"setting__sync": "数据同步",
|
||||||
"setting__sync_address": "同步服务地址:{address}",
|
"setting__sync_client_address": "当前设备地址:{address}",
|
||||||
"setting__sync_auth_code": "连接码:{code}",
|
"setting__sync_client_host": "同步服务地址",
|
||||||
"setting__sync_device": "已连接的设备:{devices}",
|
"setting__sync_client_host_tip": "请输入同步服务地址",
|
||||||
"setting__sync_enable": "启用同步功能(由于数据是明文传输,请在受信任的网络下使用)",
|
"setting__sync_client_mode": "客户端模式",
|
||||||
"setting__sync_port": "同步端口设置",
|
"setting__sync_client_status": "状态:{status}",
|
||||||
"setting__sync_port_tip": "请输入同步服务端口号",
|
"setting__sync_code_blocked_ip": "当前设备的IP已被服务端封禁!",
|
||||||
"setting__sync_refresh_code": "刷新连接码",
|
"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__sync_tip": "使用方式请看常见问题“同步功能”部分",
|
||||||
"setting__update": "软件更新",
|
"setting__update": "软件更新",
|
||||||
"setting__update_checking": "检查更新中...",
|
"setting__update_checking": "检查更新中...",
|
||||||
|
@ -478,6 +489,7 @@
|
||||||
"setting__update_show_change_log": "更新版本后的首次启动时显示更新日志",
|
"setting__update_show_change_log": "更新版本后的首次启动时显示更新日志",
|
||||||
"setting__update_try_auto_update": "发现新版本时尝试自动下载更新",
|
"setting__update_try_auto_update": "发现新版本时尝试自动下载更新",
|
||||||
"setting__update_unknown": "未知",
|
"setting__update_unknown": "未知",
|
||||||
|
"setting_sync_status_enabled": "已连接",
|
||||||
"song_list": "歌单",
|
"song_list": "歌单",
|
||||||
"songlist__import_input_btn_confirm": "打开",
|
"songlist__import_input_btn_confirm": "打开",
|
||||||
"songlist__import_input_show_btn": "打开歌单",
|
"songlist__import_input_show_btn": "打开歌单",
|
||||||
|
@ -505,6 +517,8 @@
|
||||||
"source_tx": "企鹅音乐",
|
"source_tx": "企鹅音乐",
|
||||||
"source_wy": "网易音乐",
|
"source_wy": "网易音乐",
|
||||||
"source_xm": "虾米音乐",
|
"source_xm": "虾米音乐",
|
||||||
|
"sync__auth_code_input_tip": "请输入连接码",
|
||||||
|
"sync__auth_code_title": "需要输入连接码",
|
||||||
"sync__merge_btn_local_remote": "本机列表 合并 远程列表",
|
"sync__merge_btn_local_remote": "本机列表 合并 远程列表",
|
||||||
"sync__merge_btn_remote_local": "远程列表 合并 本机列表",
|
"sync__merge_btn_remote_local": "远程列表 合并 本机列表",
|
||||||
"sync__merge_label": "合并",
|
"sync__merge_label": "合并",
|
||||||
|
@ -522,6 +536,7 @@
|
||||||
"sync__overwrite_tip": "覆盖:",
|
"sync__overwrite_tip": "覆盖:",
|
||||||
"sync__overwrite_tip_desc": "被覆盖者与覆盖者列表ID相同的列表将被删除后替换成覆盖者的列表(列表ID不同的列表将被合并到一起),若勾选完全覆盖,则被覆盖者的所有列表将被移除,然后替换成覆盖者的列表。",
|
"sync__overwrite_tip_desc": "被覆盖者与覆盖者列表ID相同的列表将被删除后替换成覆盖者的列表(列表ID不同的列表将被合并到一起),若勾选完全覆盖,则被覆盖者的所有列表将被移除,然后替换成覆盖者的列表。",
|
||||||
"sync__title": "选择与 {name} 的列表同步方式",
|
"sync__title": "选择与 {name} 的列表同步方式",
|
||||||
|
"sync_status_disabled": "未连接",
|
||||||
"tag__high_quality": "HQ",
|
"tag__high_quality": "HQ",
|
||||||
"tag__lossless": "SQ",
|
"tag__lossless": "SQ",
|
||||||
"tag__lossless_24bit": "24bit",
|
"tag__lossless_24bit": "24bit",
|
||||||
|
|
|
@ -455,13 +455,24 @@
|
||||||
"setting__setting__desktop_lyric_font_weight_font": "逐字歌詞",
|
"setting__setting__desktop_lyric_font_weight_font": "逐字歌詞",
|
||||||
"setting__setting__desktop_lyric_font_weight_line": "逐行歌詞",
|
"setting__setting__desktop_lyric_font_weight_line": "逐行歌詞",
|
||||||
"setting__sync": "數據同步",
|
"setting__sync": "數據同步",
|
||||||
"setting__sync_address": "同步服務地址:{address}",
|
"setting__sync_client_address": "當前設備地址:{address}",
|
||||||
"setting__sync_auth_code": "連接碼:{code}",
|
"setting__sync_client_host": "同步服務地址",
|
||||||
"setting__sync_device": "已連接的設備:{devices}",
|
"setting__sync_client_host_tip": "請輸入同步服務地址",
|
||||||
"setting__sync_enable": "啟用同步功能(由於數據是明文傳輸,請在受信任的網絡下使用)",
|
"setting__sync_client_mode": "客戶端模式",
|
||||||
"setting__sync_port": "同步端口設置",
|
"setting__sync_client_status": "狀態:{status}",
|
||||||
"setting__sync_port_tip": "請輸入同步服務端口號",
|
"setting__sync_code_blocked_ip": "當前設備的IP已被服務端封禁!",
|
||||||
"setting__sync_refresh_code": "刷新連接碼",
|
"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__sync_tip": "使用方式請看常見問題“同步功能”部分",
|
||||||
"setting__update": "軟件更新",
|
"setting__update": "軟件更新",
|
||||||
"setting__update_checking": "檢查更新中...",
|
"setting__update_checking": "檢查更新中...",
|
||||||
|
@ -476,6 +487,7 @@
|
||||||
"setting__update_show_change_log": "更新版本後的首次啟動時顯示更新日誌",
|
"setting__update_show_change_log": "更新版本後的首次啟動時顯示更新日誌",
|
||||||
"setting__update_try_auto_update": "發現新版本時嘗試自動下載更新",
|
"setting__update_try_auto_update": "發現新版本時嘗試自動下載更新",
|
||||||
"setting__update_unknown": "未知",
|
"setting__update_unknown": "未知",
|
||||||
|
"setting_sync_status_enabled": "已連接",
|
||||||
"song_list": "歌單",
|
"song_list": "歌單",
|
||||||
"songlist__import_input_show_btn": "打開歌單",
|
"songlist__import_input_show_btn": "打開歌單",
|
||||||
"songlist__import_input_tip": "輸入歌單鏈接或歌單ID",
|
"songlist__import_input_tip": "輸入歌單鏈接或歌單ID",
|
||||||
|
@ -502,6 +514,8 @@
|
||||||
"source_tx": "企鵝音樂",
|
"source_tx": "企鵝音樂",
|
||||||
"source_wy": "網易音樂",
|
"source_wy": "網易音樂",
|
||||||
"source_xm": "蝦米音樂",
|
"source_xm": "蝦米音樂",
|
||||||
|
"sync__auth_code_input_tip": "請輸入連接碼",
|
||||||
|
"sync__auth_code_title": "需要輸入連接碼",
|
||||||
"sync__merge_btn_local_remote": "本機列表 合併 遠程列表",
|
"sync__merge_btn_local_remote": "本機列表 合併 遠程列表",
|
||||||
"sync__merge_btn_remote_local": "遠程列表 合併 本機列表",
|
"sync__merge_btn_remote_local": "遠程列表 合併 本機列表",
|
||||||
"sync__merge_label": "合併",
|
"sync__merge_label": "合併",
|
||||||
|
@ -519,6 +533,7 @@
|
||||||
"sync__overwrite_tip": "覆蓋:",
|
"sync__overwrite_tip": "覆蓋:",
|
||||||
"sync__overwrite_tip_desc": "被覆蓋者與覆蓋者列表ID相同的列表將被刪除後替換成覆蓋者的列表(列表ID不同的列表將被合併到一起),若勾選完全覆蓋,則被覆蓋者的所有列表將被移除,然後替換成覆蓋者的列表。",
|
"sync__overwrite_tip_desc": "被覆蓋者與覆蓋者列表ID相同的列表將被刪除後替換成覆蓋者的列表(列表ID不同的列表將被合併到一起),若勾選完全覆蓋,則被覆蓋者的所有列表將被移除,然後替換成覆蓋者的列表。",
|
||||||
"sync__title": "選擇與 {name} 的列表同步方式",
|
"sync__title": "選擇與 {name} 的列表同步方式",
|
||||||
|
"sync_status_disabled": "未連接",
|
||||||
"tag__high_quality": "HQ",
|
"tag__high_quality": "HQ",
|
||||||
"tag__lossless": "SQ",
|
"tag__lossless": "SQ",
|
||||||
"tag__lossless_24bit": "24bit",
|
"tag__lossless_24bit": "24bit",
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
|
@ -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
|
|
@ -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'
|
|
@ -0,0 +1 @@
|
||||||
|
export { default as list } from './list'
|
|
@ -0,0 +1,7 @@
|
||||||
|
import initOn from './on'
|
||||||
|
import initSend from './send'
|
||||||
|
|
||||||
|
export default (socket: LX.Sync.Client.Socket) => {
|
||||||
|
initOn(socket)
|
||||||
|
initSend(socket)
|
||||||
|
}
|
|
@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}))
|
||||||
|
})
|
|
@ -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))
|
||||||
|
}
|
|
@ -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)
|
||||||
|
})
|
||||||
|
}
|
|
@ -1,21 +1,28 @@
|
||||||
// import Event from './event/event'
|
// import Event from './event/event'
|
||||||
|
|
||||||
|
import { disconnectServer } from './client'
|
||||||
|
import { stopServer } from './server'
|
||||||
|
|
||||||
// import eventNames from './event/name'
|
// import eventNames from './event/name'
|
||||||
import * as modules from './modules'
|
|
||||||
import { startServer, stopServer, getStatus, generateCode } from './server/server'
|
|
||||||
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
startServer,
|
startServer,
|
||||||
stopServer,
|
stopServer,
|
||||||
getStatus,
|
getStatus as getServerStatus,
|
||||||
generateCode,
|
generateCode,
|
||||||
// Event,
|
} from './server'
|
||||||
// eventNames,
|
|
||||||
modules,
|
export {
|
||||||
}
|
connectServer,
|
||||||
|
disconnectServer,
|
||||||
|
getStatus as getClientStatus,
|
||||||
|
} from './client'
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
global.lx.event_app.on('main_window_close', () => {
|
global.lx.event_app.on('main_window_close', () => {
|
||||||
|
if (global.lx.appSetting['sync.mode'] == 'server') {
|
||||||
void stopServer()
|
void stopServer()
|
||||||
|
} else {
|
||||||
|
void disconnectServer()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
},
|
||||||
|
}
|
|
@ -1,22 +1,17 @@
|
||||||
import type http from 'http'
|
import type http from 'http'
|
||||||
import { SYNC_CODE } from './config'
|
import { SYNC_CODE } from '@common/constants'
|
||||||
import {
|
|
||||||
aesEncrypt,
|
|
||||||
aesDecrypt,
|
|
||||||
createClientKeyInfo,
|
|
||||||
getClientKeyInfo,
|
|
||||||
setClientKeyInfo,
|
|
||||||
rsaEncrypt,
|
|
||||||
} from './utils'
|
|
||||||
import querystring from 'node:querystring'
|
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>()
|
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 code = 401
|
||||||
let msg: string = SYNC_CODE.msgAuthFailed
|
let msg: string = SYNC_CODE.msgAuthFailed
|
||||||
|
|
||||||
let ip = req.socket.remoteAddress
|
let ip = getIP(req)
|
||||||
// console.log(req.headers)
|
// console.log(req.headers)
|
||||||
if (typeof req.headers.m == 'string') {
|
if (typeof req.headers.m == 'string') {
|
||||||
if (ip && (requestIps.get(ip) ?? 0) < 10) {
|
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'
|
const deviceName = text.replace(SYNC_CODE.authMsg, '') || 'Unknown'
|
||||||
if (deviceName != keyInfo.deviceName) {
|
if (deviceName != keyInfo.deviceName) {
|
||||||
keyInfo.deviceName = deviceName
|
keyInfo.deviceName = deviceName
|
||||||
setClientKeyInfo(keyInfo)
|
saveClientKeyInfo(keyInfo)
|
||||||
}
|
}
|
||||||
msg = aesEncrypt(SYNC_CODE.helloMsg, keyInfo.key)
|
msg = aesEncrypt(SYNC_CODE.helloMsg, keyInfo.key)
|
||||||
}
|
}
|
||||||
} else { // 连接码验证
|
} 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')
|
// const iv = Buffer.from(key.split('').reverse().join('')).toString('base64')
|
||||||
key = Buffer.from(key).toString('base64')
|
key = Buffer.from(key).toString('base64')
|
||||||
// console.log(req.headers.m, authCode, key)
|
// 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 data = text.split('\n')
|
||||||
const publicKey = `-----BEGIN PUBLIC KEY-----\n${data[1]}\n-----END PUBLIC KEY-----`
|
const publicKey = `-----BEGIN PUBLIC KEY-----\n${data[1]}\n-----END PUBLIC KEY-----`
|
||||||
const deviceName = data[2] || 'Unknown'
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
|
@ -1,7 +1,16 @@
|
||||||
import { startServer, stopServer, getStatus } from './server'
|
import * as modules from './modules'
|
||||||
|
import {
|
||||||
|
startServer,
|
||||||
|
stopServer,
|
||||||
|
getStatus,
|
||||||
|
generateCode,
|
||||||
|
} from './server'
|
||||||
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
startServer,
|
startServer,
|
||||||
stopServer,
|
stopServer,
|
||||||
getStatus,
|
getStatus,
|
||||||
|
generateCode,
|
||||||
|
modules,
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,16 +1,18 @@
|
||||||
import http from 'http'
|
import http, { type IncomingMessage } from 'node:http'
|
||||||
import { Server, type Socket } from 'socket.io'
|
import url from 'node:url'
|
||||||
import { createHttpTerminator, type HttpTerminator } from 'http-terminator'
|
import { WebSocketServer } from 'ws'
|
||||||
import * as modules from '../modules'
|
import * as modules from './modules'
|
||||||
import { authCode, authConnect } from './auth'
|
import { authCode, authConnect } from './auth'
|
||||||
import { getAddress, getServerId, generateCode as handleGenerateCode, getClientKeyInfo, setClientKeyInfo } from './utils'
|
import syncList from './syncList'
|
||||||
import { syncList, removeSnapshot } from './syncList'
|
import log from '../log'
|
||||||
import { log } from '@common/utils'
|
import { SYNC_CLOSE_CODE, SYNC_CODE } from '@common/constants'
|
||||||
import { sendStatus } from '@main/modules/winMain'
|
import { decryptMsg, encryptMsg, generateCode as handleGenerateCode } from './utils'
|
||||||
import { SYNC_CODE } from './config'
|
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,
|
status: false,
|
||||||
message: '',
|
message: '',
|
||||||
address: [],
|
address: [],
|
||||||
|
@ -27,7 +29,7 @@ const codeTools: {
|
||||||
start() {
|
start() {
|
||||||
this.stop()
|
this.stop()
|
||||||
this.timeout = setInterval(() => {
|
this.timeout = setInterval(() => {
|
||||||
void generateCode()
|
void handleGenerateCode()
|
||||||
}, 60 * 3 * 1000)
|
}, 60 * 3 * 1000)
|
||||||
},
|
},
|
||||||
stop() {
|
stop() {
|
||||||
|
@ -37,12 +39,45 @@ const codeTools: {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleConnection = (io: Server, socket: LX.Sync.Socket) => {
|
const handleConnection = async(socket: LX.Sync.Server.Socket, request: IncomingMessage) => {
|
||||||
console.log('connection')
|
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)
|
// console.log(socket.handshake.query)
|
||||||
for (const module of Object.values(modules)) {
|
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 = () => {
|
const handleUnconnection = () => {
|
||||||
|
@ -64,11 +99,16 @@ const authConnection = (req: http.IncomingMessage, callback: (err: string | null
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
let httpTerminator: HttpTerminator | null = null
|
let wss: LX.Sync.Server.SocketServer | null
|
||||||
let io: Server | null = null
|
let httpServer: http.Server
|
||||||
|
|
||||||
const handleStartServer = async(port = 9527) => await new Promise((resolve, reject) => {
|
function noop() {}
|
||||||
const httpServer = http.createServer((req, res) => {
|
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)
|
// console.log(req.url)
|
||||||
let code
|
let code
|
||||||
let msg
|
let msg
|
||||||
|
@ -93,46 +133,96 @@ const handleStartServer = async(port = 9527) => await new Promise((resolve, reje
|
||||||
res.writeHead(code)
|
res.writeHead(code)
|
||||||
res.end(msg)
|
res.end(msg)
|
||||||
})
|
})
|
||||||
httpTerminator = createHttpTerminator({
|
|
||||||
server: httpServer,
|
wss = new WebSocketServer({
|
||||||
})
|
noServer: true,
|
||||||
io = new Server(httpServer, {
|
|
||||||
path: '/sync',
|
|
||||||
serveClient: false,
|
|
||||||
connectTimeout: 10000,
|
|
||||||
pingTimeout: 30000,
|
|
||||||
maxHttpBufferSize: 1e9, // 1G
|
|
||||||
allowRequest: authConnection,
|
|
||||||
transports: ['websocket'],
|
|
||||||
})
|
})
|
||||||
|
|
||||||
io.on('connection', async(_socket: Socket) => {
|
wss.on('connection', function(socket, request) {
|
||||||
const socket = _socket as LX.Sync.Socket
|
socket.isReady = false
|
||||||
socket.on('disconnect', reason => {
|
socket.on('pong', () => {
|
||||||
console.log('disconnect', reason)
|
socket.isAlive = true
|
||||||
status.devices.splice(status.devices.findIndex(k => k.clientId == keyInfo?.clientId), 1)
|
|
||||||
sendStatus(status)
|
|
||||||
if (!status.devices.length) handleUnconnection()
|
|
||||||
})
|
})
|
||||||
if (typeof socket.handshake.query.i != 'string') return socket.disconnect(true)
|
|
||||||
const keyInfo = getClientKeyInfo(socket.handshake.query.i)
|
// const events = new Map<keyof ActionsType, Array<(err: Error | null, data: LX.Sync.ActionSyncType[keyof LX.Sync.ActionSyncType]) => void>>()
|
||||||
if (!keyInfo || !io) return socket.disconnect(true)
|
// const events = new Map<keyof LX.Sync.ActionSyncType, Array<(err: Error | null, data: LX.Sync.ActionSyncType[keyof LX.Sync.ActionSyncType]) => void>>()
|
||||||
keyInfo.connectionTime = Date.now()
|
let events: Partial<{ [K in keyof LX.Sync.ActionSyncType]: Array<(data: LX.Sync.ActionSyncType[K]) => void> }> = {}
|
||||||
setClientKeyInfo(keyInfo)
|
socket.addEventListener('message', ({ data }) => {
|
||||||
// socket.lx_keyInfo = keyInfo
|
if (typeof data === 'string') {
|
||||||
socket.data.keyInfo = keyInfo
|
let syncData: LX.Sync.ActionSync
|
||||||
socket.data.isReady = false
|
|
||||||
try {
|
try {
|
||||||
await syncList(io, socket)
|
syncData = JSON.parse(decryptMsg(socket.keyInfo, data))
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// console.log(err)
|
log.error('parse message error:', err)
|
||||||
log.warn(err)
|
socket.close(SYNC_CLOSE_CODE.failed)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
status.devices.push(keyInfo)
|
const handlers = events[syncData.action]
|
||||||
handleConnection(io, socket)
|
if (handlers) {
|
||||||
socket.data.isReady = true
|
// @ts-expect-error
|
||||||
sendStatus(status)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 => {
|
httpServer.on('error', error => {
|
||||||
|
@ -142,35 +232,41 @@ const handleStartServer = async(port = 9527) => await new Promise((resolve, reje
|
||||||
|
|
||||||
httpServer.on('listening', () => {
|
httpServer.on('listening', () => {
|
||||||
const addr = httpServer.address()
|
const addr = httpServer.address()
|
||||||
|
// console.log(addr)
|
||||||
if (!addr) {
|
if (!addr) {
|
||||||
reject(new Error('address is null'))
|
reject(new Error('address is null'))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const bind = typeof addr == 'string' ? `pipe ${addr}` : `port ${addr.port}`
|
const bind = typeof addr == 'string' ? `pipe ${addr}` : `port ${addr.port}`
|
||||||
console.info(`Listening on ${bind}`)
|
log.info(`Listening on ${ip} ${bind}`)
|
||||||
resolve(null)
|
resolve(null)
|
||||||
})
|
})
|
||||||
|
|
||||||
httpServer.listen(port)
|
httpServer.listen(port, ip)
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleStopServer = async() => {
|
const handleStopServer = async() => new Promise<void>((resolve, reject) => {
|
||||||
if (!httpTerminator) return
|
if (!wss) return
|
||||||
if (!io) return
|
wss.close()
|
||||||
io.close()
|
wss = null
|
||||||
await httpTerminator.terminate().catch(() => {})
|
httpServer.close((err) => {
|
||||||
io = null
|
if (err) {
|
||||||
httpTerminator = null
|
reject(err)
|
||||||
}
|
return
|
||||||
|
}
|
||||||
|
resolve()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
export const stopServer = async() => {
|
export const stopServer = async() => {
|
||||||
|
console.log('stop')
|
||||||
codeTools.stop()
|
codeTools.stop()
|
||||||
if (!status.status) {
|
if (!status.status) {
|
||||||
status.status = false
|
status.status = false
|
||||||
status.message = ''
|
status.message = ''
|
||||||
status.address = []
|
status.address = []
|
||||||
status.code = ''
|
status.code = ''
|
||||||
sendStatus(status)
|
sendServerStatus(status)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
console.log('stoping sync server...')
|
console.log('stoping sync server...')
|
||||||
|
@ -184,14 +280,15 @@ export const stopServer = async() => {
|
||||||
console.log(err)
|
console.log(err)
|
||||||
status.message = err.message
|
status.message = err.message
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
sendStatus(status)
|
sendServerStatus(status)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const startServer = async(port: number) => {
|
export const startServer = async(port: number) => {
|
||||||
|
console.log('status.status', status.status)
|
||||||
if (status.status) await handleStopServer()
|
if (status.status) await handleStopServer()
|
||||||
|
|
||||||
console.log('starting sync server...')
|
log.info('starting sync server')
|
||||||
await handleStartServer(port).then(() => {
|
await handleStartServer(port).then(() => {
|
||||||
console.log('sync server started')
|
console.log('sync server started')
|
||||||
status.status = true
|
status.status = true
|
||||||
|
@ -207,18 +304,14 @@ export const startServer = async(port: number) => {
|
||||||
status.address = []
|
status.address = []
|
||||||
status.code = ''
|
status.code = ''
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
sendStatus(status)
|
sendServerStatus(status)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getStatus = (): LX.Sync.Status => status
|
export const getStatus = (): LX.Sync.ServerStatus => status
|
||||||
|
|
||||||
export const generateCode = async() => {
|
export const generateCode = async() => {
|
||||||
status.code = handleGenerateCode()
|
status.code = handleGenerateCode()
|
||||||
sendStatus(status)
|
sendServerStatus(status)
|
||||||
return status.code
|
return status.code
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
|
||||||
removeSnapshot,
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,13 +1,18 @@
|
||||||
import { promises as fsPromises } from 'fs'
|
import { SYNC_CLOSE_CODE } from '@common/constants'
|
||||||
import { encryptMsg, decryptMsg, getSnapshotFilePath } from './utils'
|
import { removeSelectModeListener, sendCloseSelectMode, sendSelectMode } from '@main/modules/winMain'
|
||||||
import { throttle } from '@common/utils'
|
import { getSnapshot, updateDeviceSnapshotKey } from '../data'
|
||||||
import { type Server } from 'socket.io'
|
import { getLocalListData, setLocalListData } from '../utils'
|
||||||
import { sendCloseSelectMode, sendSelectMode } from '@main/modules/winMain'
|
import { createSnapshot, encryptMsg, getCurrentListInfoKey } from './utils'
|
||||||
import { LIST_IDS } from '@common/constants'
|
|
||||||
|
|
||||||
|
const handleSetLocalListData = async(listData: LX.Sync.ListData) => {
|
||||||
|
await setLocalListData(listData)
|
||||||
|
return createSnapshot()
|
||||||
|
}
|
||||||
|
|
||||||
// type ListInfoType = LX.List.UserListInfoFull | LX.List.MyDefaultListInfoFull | LX.List.MyLoveListInfoFull
|
// 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
|
let syncingId: string | null = null
|
||||||
const wait = async(time = 1000) => await new Promise((resolve, reject) => setTimeout(resolve, time))
|
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)
|
}, 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')
|
console.log('getRemoteListData')
|
||||||
const handleError = (reason: string) => {
|
let removeEventClose = socket.onClose(reject)
|
||||||
socket.removeListener('list:sync', handleSuccess)
|
let removeEvent = socket.onRemoteEvent('list:sync:list_sync_get_list_data', (listData) => {
|
||||||
socket.removeListener('disconnect', handleError)
|
resolve(patchListData(listData))
|
||||||
reject(new Error(reason))
|
removeEventClose()
|
||||||
}
|
removeEvent()
|
||||||
const handleSuccess = (enData: string) => {
|
})
|
||||||
socket.removeListener('disconnect', handleError)
|
socket.sendData('list:sync:list_sync_get_list_data', undefined, (err) => {
|
||||||
socket.removeListener('list:sync', handleSuccess)
|
if (!err) return
|
||||||
console.log('getRemoteListData', 'handleSuccess')
|
reject(err)
|
||||||
const data: LX.Sync.Data | null = JSON.parse(decryptMsg(socket.data.keyInfo, enData))
|
removeEventClose()
|
||||||
if (!data) {
|
removeEvent()
|
||||||
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' })))
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// const getAllLists = async() => {
|
const getRemoteListMD5 = async(socket: LX.Sync.Server.Socket): Promise<string> => await new Promise((resolve, reject) => {
|
||||||
// const lists = []
|
let removeEventClose = socket.onClose(reject)
|
||||||
// lists.push(await getListMusics(defaultList.id).then(musics => ({ ...defaultList, list: toRaw(musics).map(m => toOldMusicInfo(m)) })))
|
let removeEvent = socket.onRemoteEvent('list:sync:list_sync_get_md5', (md5) => {
|
||||||
// lists.push(await getListMusics(loveList.id).then(musics => ({ ...loveList, list: toRaw(musics).map(m => toOldMusicInfo(m)) })))
|
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 getSyncMode = async(socket: LX.Sync.Server.Socket): Promise<LX.Sync.Mode> => new Promise((resolve, reject) => {
|
||||||
// }
|
const handleDisconnect = (err: Error) => {
|
||||||
|
|
||||||
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 = () => {
|
|
||||||
sendCloseSelectMode()
|
sendCloseSelectMode()
|
||||||
reject(new Error('disconnect'))
|
removeSelectModeListener()
|
||||||
|
reject(err)
|
||||||
}
|
}
|
||||||
socket.on('disconnect', handleDisconnect)
|
sendSelectMode(socket.keyInfo.deviceName, (mode) => {
|
||||||
let removeListener = sendSelectMode(socket.data.keyInfo, (mode) => {
|
removeSelectModeListener()
|
||||||
removeListener()
|
removeEventClose()
|
||||||
resolve(mode)
|
resolve(mode)
|
||||||
})
|
})
|
||||||
|
let removeEventClose = socket.onClose(handleDisconnect)
|
||||||
})
|
})
|
||||||
|
|
||||||
const finishedSync = (socket: LX.Sync.Socket) => {
|
const finishedSync = async(socket: LX.Sync.Server.Socket) => new Promise<void>((resolve, reject) => {
|
||||||
return socket.emit('list:sync', encryptMsg(socket.data.keyInfo, JSON.stringify({
|
socket.sendData('list:sync:finished', undefined, (err) => {
|
||||||
action: 'finished',
|
if (err) reject(err)
|
||||||
})))
|
else resolve()
|
||||||
}
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const sendDataPromise = async(socket: LX.Sync.Server.Socket, dataStr: string, key: string) => new Promise<void>((resolve, reject) => {
|
||||||
const setLocalList = (listData: LX.Sync.ListData) => {
|
socket.send(encryptMsg(socket.keyInfo, dataStr), (err) => {
|
||||||
void global.lx.event_list.list_data_overwrite(listData, true)
|
if (err) {
|
||||||
}
|
socket.close(SYNC_CLOSE_CODE.failed)
|
||||||
const setRemotelList = async(socket: LX.Sync.Socket, listData: LX.Sync.ListData) => {
|
resolve()
|
||||||
if (!io) return
|
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 })))
|
|
||||||
}
|
}
|
||||||
}
|
updateDeviceSnapshotKey(socket.keyInfo, key)
|
||||||
|
resolve()
|
||||||
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)
|
|
||||||
})
|
|
||||||
writeFilePromises.set(path, writeFilePromise)
|
|
||||||
await writeFilePromise.finally(() => {
|
|
||||||
if (writeFilePromise !== writeFilePromises.get(path)) return
|
|
||||||
writeFilePromises.delete(path)
|
|
||||||
})
|
})
|
||||||
|
})
|
||||||
|
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 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()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
type UserDataObj = Record<string, LX.List.UserListInfoFull>
|
type UserDataObj = Record<string, LX.List.UserListInfoFull>
|
||||||
|
|
||||||
const createUserListDataObj = (listData: LX.Sync.ListData): UserDataObj => {
|
const createUserListDataObj = (listData: LX.Sync.ListData): UserDataObj => {
|
||||||
const userListDataObj: UserDataObj = {}
|
const userListDataObj: UserDataObj = {}
|
||||||
for (const list of listData.userList) userListDataObj[list.id] = list
|
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
|
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 mode: LX.Sync.Mode = await getSyncMode(socket)
|
||||||
|
|
||||||
const [remoteListData, localListData] = await Promise.all([getRemoteListData(socket), getLocalListData()])
|
const [remoteListData, localListData] = await Promise.all([getRemoteListData(socket), getLocalListData()])
|
||||||
console.log('handleMergeListData', 'remoteListData, localListData')
|
console.log('handleMergeListData', 'remoteListData, localListData')
|
||||||
let listData: LX.Sync.ListData
|
let listData: LX.Sync.ListData
|
||||||
|
let requiredUpdateLocalListData = true
|
||||||
|
let requiredUpdateRemoteListData = true
|
||||||
switch (mode) {
|
switch (mode) {
|
||||||
case 'merge_local_remote':
|
case 'merge_local_remote':
|
||||||
listData = mergeList(localListData, remoteListData)
|
listData = mergeList(localListData, remoteListData)
|
||||||
|
@ -246,63 +239,47 @@ const handleMergeListData = async(socket: LX.Sync.Socket): Promise<LX.Sync.ListD
|
||||||
break
|
break
|
||||||
case 'overwrite_local_remote_full':
|
case 'overwrite_local_remote_full':
|
||||||
listData = localListData
|
listData = localListData
|
||||||
|
requiredUpdateLocalListData = false
|
||||||
break
|
break
|
||||||
case 'overwrite_remote_local_full':
|
case 'overwrite_remote_local_full':
|
||||||
listData = remoteListData
|
listData = remoteListData
|
||||||
|
requiredUpdateRemoteListData = false
|
||||||
break
|
break
|
||||||
case 'none': return null
|
// case 'none': return null
|
||||||
case 'cancel':
|
case 'cancel':
|
||||||
socket.disconnect(true)
|
default:
|
||||||
|
socket.close(SYNC_CLOSE_CODE.normal)
|
||||||
throw new Error('cancel')
|
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()])
|
const [remoteListData, localListData] = await Promise.all([getRemoteListData(socket), getLocalListData()])
|
||||||
console.log('handleSyncList', 'remoteListData, localListData')
|
console.log('handleSyncList', 'remoteListData, localListData')
|
||||||
const listData: LX.Sync.ListData = {
|
|
||||||
defaultList: [],
|
|
||||||
loveList: [],
|
|
||||||
userList: [],
|
|
||||||
}
|
|
||||||
if (localListData.defaultList.length || localListData.loveList.length || localListData.userList.length) {
|
if (localListData.defaultList.length || localListData.loveList.length || localListData.userList.length) {
|
||||||
if (remoteListData.defaultList.length || remoteListData.loveList.length || remoteListData.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('handleMergeListData', 'mergedList')
|
||||||
// console.log(mergedList)
|
let key
|
||||||
if (!mergedList) return null
|
if (requiredUpdateLocalListData) {
|
||||||
listData.defaultList = mergedList.defaultList
|
key = await handleSetLocalListData(mergedList)
|
||||||
listData.loveList = mergedList.loveList
|
await overwriteRemoteListData(mergedList, key, [socket.keyInfo.clientId])
|
||||||
listData.userList = mergedList.userList
|
}
|
||||||
setLocalList(mergedList)
|
if (requiredUpdateRemoteListData) {
|
||||||
void setRemotelList(socket, mergedList)
|
if (!key) key = await getCurrentListInfoKey()
|
||||||
|
await setRemotelList(socket, mergedList, key)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
void setRemotelList(socket, localListData)
|
await setRemotelList(socket, localListData, await getCurrentListInfoKey())
|
||||||
listData.defaultList = localListData.defaultList
|
|
||||||
listData.loveList = localListData.loveList
|
|
||||||
listData.userList = localListData.userList
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (remoteListData.defaultList.length || remoteListData.loveList.length || remoteListData.userList.length) {
|
if (remoteListData.defaultList.length || remoteListData.loveList.length || remoteListData.userList.length) {
|
||||||
setLocalList(remoteListData)
|
const key = await handleSetLocalListData(remoteListData)
|
||||||
listData.defaultList = remoteListData.defaultList
|
updateDeviceSnapshotKey(socket.keyInfo, key)
|
||||||
listData.loveList = remoteListData.loveList
|
await overwriteRemoteListData(remoteListData, key, [socket.keyInfo.clientId])
|
||||||
listData.userList = remoteListData.userList
|
|
||||||
} else {
|
|
||||||
listData.defaultList = localListData.defaultList
|
|
||||||
listData.loveList = localListData.loveList
|
|
||||||
listData.userList = localListData.userList
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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 = (
|
const mergeListDataFromSnapshot = (
|
||||||
|
@ -347,7 +324,16 @@ const mergeListDataFromSnapshot = (
|
||||||
}
|
}
|
||||||
return ids.map(id => map.get(id)) as LX.Music.MusicInfo[]
|
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 addMusicLocationType = global.lx.appSetting['list.addMusicLocationType']
|
||||||
const [remoteListData, localListData] = await Promise.all([getRemoteListData(socket), getLocalListData()])
|
const [remoteListData, localListData] = await Promise.all([getRemoteListData(socket), getLocalListData()])
|
||||||
const newListData: LX.Sync.ListData = {
|
const newListData: LX.Sync.ListData = {
|
||||||
|
@ -404,79 +390,37 @@ const handleMergeListDataFromSnapshot = async(socket: LX.Sync.Socket, snapshot:
|
||||||
})
|
})
|
||||||
|
|
||||||
newListData.userList = newUserList
|
newListData.userList = newUserList
|
||||||
setLocalList(newListData)
|
const key = await handleSetLocalListData(newListData)
|
||||||
void setRemotelList(socket, newListData)
|
await setRemotelList(socket, newListData, key)
|
||||||
return await updateSnapshot(socket.data.snapshotFilePath, JSON.stringify({
|
await overwriteRemoteListData(newListData, key, [socket.keyInfo.clientId])
|
||||||
defaultList: newListData.defaultList,
|
|
||||||
loveList: newListData.loveList,
|
|
||||||
userList: newListData.userList,
|
|
||||||
})).then(() => {
|
|
||||||
socket.data.isCreatedSnapshot = true
|
|
||||||
return newListData
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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> => {
|
const syncList = async(socket: LX.Sync.Server.Socket) => {
|
||||||
socket.data.snapshotFilePath = getSnapshotFilePath(socket.data.keyInfo)
|
// socket.data.snapshotFilePath = getSnapshotFilePath(socket.keyInfo)
|
||||||
let fileData: any
|
if (socket.keyInfo.snapshotKey) {
|
||||||
let isSyncRequired = false
|
const listData = await getSnapshot(socket.keyInfo.snapshotKey)
|
||||||
try {
|
if (listData) {
|
||||||
fileData = (await fsPromises.readFile(socket.data.snapshotFilePath)).toString()
|
await handleMergeListDataFromSnapshot(socket, listData)
|
||||||
fileData = JSON.parse(fileData)
|
return
|
||||||
} catch (error) {
|
|
||||||
const err = error as NodeJS.ErrnoException
|
|
||||||
if (err.code != 'ENOENT') throw err
|
|
||||||
isSyncRequired = true
|
|
||||||
}
|
}
|
||||||
console.log('isSyncRequired', isSyncRequired)
|
}
|
||||||
if (isSyncRequired) return await handleSyncList(socket)
|
await handleSyncList(socket)
|
||||||
return await handleMergeListDataFromSnapshot(socket, patchListData(fileData))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// const checkSyncQueue = async(): Promise<void> => {
|
|
||||||
// if (!syncingId) return
|
|
||||||
// console.log('sync queue...')
|
|
||||||
// await wait()
|
|
||||||
// await checkSyncQueue()
|
|
||||||
// }
|
|
||||||
|
|
||||||
// export {
|
export default async(_wss: LX.Sync.Server.SocketServer, socket: LX.Sync.Server.Socket) => {
|
||||||
// syncList = async(_io: Server, socket: LX.Sync.Socket) => {
|
if (!wss) {
|
||||||
// io = _io
|
wss = _wss
|
||||||
// await checkSyncQueue()
|
_wss.addListener('close', () => {
|
||||||
// syncingId = socket.data.keyInfo.clientId
|
wss = null
|
||||||
// return await syncList(socket).then(newListData => {
|
})
|
||||||
// registerUpdateSnapshotTask(socket, { ...newListData })
|
}
|
||||||
// return finishedSync(socket)
|
|
||||||
// }).finally(() => {
|
|
||||||
// syncingId = null
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
const _syncList = async(_io: Server, socket: LX.Sync.Socket) => {
|
|
||||||
io = _io
|
|
||||||
let disconnected = false
|
let disconnected = false
|
||||||
socket.on('disconnect', () => {
|
socket.onClose(() => {
|
||||||
disconnected = true
|
disconnected = true
|
||||||
if (syncingId == socket.data.keyInfo.clientId) syncingId = null
|
if (syncingId == socket.keyInfo.clientId) syncingId = null
|
||||||
})
|
})
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
|
@ -485,22 +429,16 @@ const _syncList = async(_io: Server, socket: LX.Sync.Socket) => {
|
||||||
await wait()
|
await wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
syncingId = socket.data.keyInfo.clientId
|
syncingId = socket.keyInfo.clientId
|
||||||
return await syncList(socket).then(newListData => {
|
await syncList(socket).then(async() => {
|
||||||
if (newListData) registerUpdateSnapshotTask(socket, { ...newListData })
|
// if (newListData) registerUpdateSnapshotTask(socket, { ...newListData })
|
||||||
return finishedSync(socket)
|
return finishedSync(socket)
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
syncingId = null
|
syncingId = null
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeSnapshot = async(keyInfo: LX.Sync.KeyInfo) => {
|
// const removeSnapshot = async(keyInfo: LX.Sync.KeyInfo) => {
|
||||||
const filePath = getSnapshotFilePath(keyInfo)
|
// const filePath = getSnapshotFilePath(keyInfo)
|
||||||
await fsPromises.unlink(filePath)
|
// await fsPromises.unlink(filePath)
|
||||||
}
|
// }
|
||||||
|
|
||||||
export {
|
|
||||||
_syncList as syncList,
|
|
||||||
removeSnapshot,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
|
@ -1,97 +1,28 @@
|
||||||
import { networkInterfaces } from 'os'
|
import { toMD5 } from '@common/utils/nodejs'
|
||||||
import { randomBytes, createCipheriv, createDecipheriv, publicEncrypt, privateDecrypt, constants } from 'crypto'
|
import type http from 'node:http'
|
||||||
import { join } from 'path'
|
import {
|
||||||
import getStore from '@main/utils/store'
|
getSnapshotInfo,
|
||||||
|
saveSnapshot,
|
||||||
const STORE_NAME = 'sync'
|
saveSnapshotInfo,
|
||||||
|
type SnapshotInfo,
|
||||||
type KeyInfos = Record<string, LX.Sync.KeyInfo>
|
} from '../data'
|
||||||
|
import { getLocalListData } from '../utils'
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
export const generateCode = (): string => {
|
export const generateCode = (): string => {
|
||||||
return Math.random().toString().substring(2, 8)
|
return Math.random().toString().substring(2, 8)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const aesEncrypt = (buffer: string | Buffer, key: string): string => {
|
export const getIP = (request: http.IncomingMessage) => {
|
||||||
const cipher = createCipheriv('aes-128-ecb', Buffer.from(key, 'base64'), '')
|
return request.socket.remoteAddress
|
||||||
return Buffer.concat([cipher.update(buffer), cipher.final()]).toString('base64')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const aesDecrypt = (text: string, key: string): string => {
|
export const encryptMsg = (keyInfo: LX.Sync.ServerKeyInfo, msg: 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 => {
|
|
||||||
return msg
|
return msg
|
||||||
// if (!keyInfo) return ''
|
// if (!keyInfo) return ''
|
||||||
// return aesEncrypt(msg, keyInfo.key, keyInfo.iv)
|
// 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
|
return enMsg
|
||||||
// if (!keyInfo) return ''
|
// if (!keyInfo) return ''
|
||||||
// let msg = ''
|
// let msg = ''
|
||||||
|
@ -103,6 +34,29 @@ export const decryptMsg = (keyInfo: LX.Sync.KeyInfo, enMsg: string): string => {
|
||||||
// return msg
|
// return msg
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getSnapshotFilePath = (keyInfo: LX.Sync.KeyInfo): string => {
|
let snapshotInfo: SnapshotInfo
|
||||||
return join(global.lxDataPath, `snapshot-${Buffer.from(keyInfo.clientId).toString('hex').substring(0, 10)}.json`)
|
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
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,59 +1,84 @@
|
||||||
// import { throttle } from '@common/utils/common'
|
import { createCipheriv, createDecipheriv, publicEncrypt, privateDecrypt, constants } from 'node:crypto'
|
||||||
import { type Server } from 'socket.io'
|
import os, { networkInterfaces } from 'node:os'
|
||||||
// import { sendSyncActionList } from '@main/modules/winMain'
|
import cp from 'node:child_process'
|
||||||
import { encryptMsg, decryptMsg } from '../server/utils'
|
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) => {
|
for (const interfaceInfos of Object.values(nets)) {
|
||||||
// console.log('handleListAction', action)
|
if (!interfaceInfos) continue
|
||||||
|
// Skip over non-IPv4 and internal (i.e. 127.0.0.1) addresses
|
||||||
switch (action) {
|
for (const interfaceInfo of interfaceInfos) {
|
||||||
case 'list_data_overwrite':
|
if (interfaceInfo.family === 'IPv4' && !interfaceInfo.internal) {
|
||||||
void global.lx.event_list.list_data_overwrite(data, true)
|
results.push(interfaceInfo.address)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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) => {
|
const list_data_overwrite = async(listData: MakeOptional<LX.List.ListDataFull, 'tempList'>, isRemote: boolean = false) => {
|
||||||
if (isRemote) return
|
if (isRemote) return
|
||||||
await sendListAction({ action: 'list_data_overwrite', data: listData })
|
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[] = []) => {
|
switch (action) {
|
||||||
if (!io) return
|
case 'list_data_overwrite':
|
||||||
const sockets: LX.Sync.RemoteSocket[] = await io.fetchSockets()
|
void global.lx.event_list.list_data_overwrite(data, true)
|
||||||
for (const socket of sockets) {
|
break
|
||||||
if (excludeIds.includes(socket.data.keyInfo.clientId) || !socket.data.isReady) continue
|
case 'list_create':
|
||||||
socket.emit(action, encryptMsg(socket.data.keyInfo, data))
|
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)
|
||||||
export const sendListAction = async(action: LX.Sync.ActionList) => {
|
break
|
||||||
console.log('sendListAction', action.action)
|
case 'list_update':
|
||||||
// io.sockets
|
void global.lx.event_list.list_update(data, true)
|
||||||
await broadcast('list:action', JSON.stringify(action))
|
break
|
||||||
}
|
case 'list_update_position':
|
||||||
|
void global.lx.event_list.list_update_position(data.position, data.ids, true)
|
||||||
export const registerListHandler = (_io: Server, socket: LX.Sync.Socket) => {
|
break
|
||||||
if (!io) {
|
case 'list_music_add':
|
||||||
io = _io
|
void global.lx.event_list.list_music_add(data.id, data.musicInfos, data.addMusicLocationType, true)
|
||||||
removeListener = registerListActionEvent()
|
break
|
||||||
}
|
case 'list_music_move':
|
||||||
|
void global.lx.event_list.list_music_move(data.fromId, data.toId, data.musicInfos, data.addMusicLocationType, true)
|
||||||
socket.on('list:action', msg => {
|
break
|
||||||
if (!socket.data.isReady) return
|
case 'list_music_remove':
|
||||||
// console.log(msg)
|
void global.lx.event_list.list_music_remove(data.listId, data.ids, true)
|
||||||
msg = decryptMsg(socket.data.keyInfo, msg)
|
break
|
||||||
if (!msg) return
|
case 'list_music_update':
|
||||||
handleListAction(JSON.parse(msg))
|
void global.lx.event_list.list_music_update(data, true)
|
||||||
void broadcast('list:action', msg, [socket.data.keyInfo.clientId])
|
break
|
||||||
// socket.broadcast.emit('list:action', { action: 'list_remove', data: { id: 'default', index: 0 } })
|
case 'list_music_update_position':
|
||||||
})
|
void global.lx.event_list.list_music_update_position(data.listId, data.position, data.ids, true)
|
||||||
|
break
|
||||||
// socket.on('list:add', addMusic)
|
case 'list_music_overwrite':
|
||||||
}
|
void global.lx.event_list.list_music_overwrite(data.listId, data.musicInfos, true)
|
||||||
export const unregisterListHandler = () => {
|
break
|
||||||
io = null
|
case 'list_music_clear':
|
||||||
|
void global.lx.event_list.list_music_clear(data, true)
|
||||||
if (removeListener) {
|
break
|
||||||
removeListener()
|
default:
|
||||||
removeListener = null
|
return false
|
||||||
}
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
import { mainHandle } from '@common/mainIpc'
|
import { mainHandle } from '@common/mainIpc'
|
||||||
import { WIN_MAIN_RENDERER_EVENT_NAME } from '@common/ipcNames'
|
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'
|
import { sendEvent } from '../main'
|
||||||
|
|
||||||
let selectModeListenr: ((mode: LX.Sync.Mode) => void) | null = null
|
let selectModeListenr: ((mode: LX.Sync.Mode) => void) | null = null
|
||||||
|
@ -8,13 +8,15 @@ let selectModeListenr: ((mode: LX.Sync.Mode) => void) | null = null
|
||||||
export default () => {
|
export default () => {
|
||||||
mainHandle<LX.Sync.SyncServiceActions, any>(WIN_MAIN_RENDERER_EVENT_NAME.sync_action, async({ params: data }) => {
|
mainHandle<LX.Sync.SyncServiceActions, any>(WIN_MAIN_RENDERER_EVENT_NAME.sync_action, async({ params: data }) => {
|
||||||
switch (data.action) {
|
switch (data.action) {
|
||||||
case 'enable':
|
case 'enable_server':
|
||||||
data.data.enable ? await startServer(parseInt(data.data.port)) : await stopServer()
|
data.data.enable ? await startServer(parseInt(data.data.port)) : await stopServer()
|
||||||
return
|
return
|
||||||
case 'get_status':
|
case 'enable_client':
|
||||||
return getStatus()
|
data.data.enable ? await connectServer(data.data.host, data.data.authCode) : await disconnectServer()
|
||||||
case 'generate_code':
|
return
|
||||||
return await generateCode()
|
case 'get_server_status': return getServerStatus()
|
||||||
|
case 'get_client_status': return getClientStatus()
|
||||||
|
case 'generate_code': return generateCode()
|
||||||
case 'select_mode':
|
case 'select_mode':
|
||||||
if (selectModeListenr) selectModeListenr(data.data)
|
if (selectModeListenr) selectModeListenr(data.data)
|
||||||
break
|
break
|
||||||
|
@ -29,18 +31,24 @@ export const sendSyncAction = (data: LX.Sync.SyncMainWindowActions) => {
|
||||||
sendEvent(WIN_MAIN_RENDERER_EVENT_NAME.sync_action, data)
|
sendEvent(WIN_MAIN_RENDERER_EVENT_NAME.sync_action, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const sendStatus = (status: LX.Sync.Status) => {
|
export const sendClientStatus = (status: LX.Sync.ClientStatus) => {
|
||||||
sendSyncAction({
|
sendSyncAction({
|
||||||
action: 'status',
|
action: 'client_status',
|
||||||
data: 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
|
selectModeListenr = listener
|
||||||
sendSyncAction({ action: 'select_mode', data: keyInfo })
|
sendSyncAction({ action: 'select_mode', data: deviceName })
|
||||||
return () => {
|
}
|
||||||
|
export const removeSelectModeListener = () => {
|
||||||
selectModeListenr = null
|
selectModeListenr = null
|
||||||
}
|
|
||||||
}
|
}
|
||||||
export const sendCloseSelectMode = () => {
|
export const sendCloseSelectMode = () => {
|
||||||
sendSyncAction({ action: 'close_select_mode' })
|
sendSyncAction({ action: 'close_select_mode' })
|
||||||
|
|
|
@ -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>
|
type DefaultEventsMap = Record<string, (...args: any[]) => void>
|
||||||
|
|
||||||
|
@ -6,24 +6,57 @@ type DefaultEventsMap = Record<string, (...args: any[]) => void>
|
||||||
declare global {
|
declare global {
|
||||||
namespace LX {
|
namespace LX {
|
||||||
namespace Sync {
|
namespace Sync {
|
||||||
class Socket extends _Socket {
|
namespace Client {
|
||||||
data: SocketData
|
interface Socket extends WS.WebSocket {
|
||||||
}
|
|
||||||
class RemoteSocket extends _RemoteSocket<DefaultEventsMap, any> {
|
|
||||||
readonly data: SocketData
|
|
||||||
}
|
|
||||||
interface Data {
|
|
||||||
action: string
|
|
||||||
data: any
|
|
||||||
}
|
|
||||||
interface SocketData {
|
|
||||||
snapshotFilePath: string
|
|
||||||
isCreatedSnapshot: boolean
|
|
||||||
keyInfo: KeyInfo
|
|
||||||
isReady: boolean
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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>
|
||||||
}
|
}
|
||||||
type Action = 'list:sync'
|
|
||||||
type ListAction = 'getData' | 'finished'
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
})
|
||||||
|
}
|
|
@ -11,6 +11,7 @@
|
||||||
<layout-update-modal />
|
<layout-update-modal />
|
||||||
<layout-pact-modal />
|
<layout-pact-modal />
|
||||||
<layout-sync-mode-modal />
|
<layout-sync-mode-modal />
|
||||||
|
<layout-sync-auth-code-modal />
|
||||||
<layout-play-detail />
|
<layout-play-detail />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -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>
|
|
@ -23,7 +23,7 @@
|
||||||
<dl :class="$style.btnGroup">
|
<dl :class="$style.btnGroup">
|
||||||
<dt :class="$style.label">{{ $t('sync__other_label') }}</dt>
|
<dt :class="$style.label">{{ $t('sync__other_label') }}</dt>
|
||||||
<dd :class="$style.btns">
|
<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>
|
<base-btn :class="$style.btn" @click="handleSelectMode('cancel')">{{ $t('sync__overwrite_btn_cancel') }}</base-btn>
|
||||||
</dd>
|
</dd>
|
||||||
</dl>
|
</dl>
|
||||||
|
|
|
@ -32,25 +32,62 @@ export default () => {
|
||||||
immediate: true,
|
immediate: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
watch(() => appSetting['sync.mode'], (mode) => {
|
||||||
|
sync.mode = mode
|
||||||
|
})
|
||||||
|
|
||||||
watch(() => appSetting['sync.enable'], enable => {
|
watch(() => appSetting['sync.enable'], enable => {
|
||||||
|
switch (appSetting['sync.mode']) {
|
||||||
|
case 'server':
|
||||||
|
if (appSetting['sync.server.port']) {
|
||||||
void sendSyncAction({
|
void sendSyncAction({
|
||||||
action: 'enable',
|
action: 'enable_server',
|
||||||
data: {
|
|
||||||
enable,
|
|
||||||
port: appSetting['sync.port'],
|
|
||||||
},
|
|
||||||
})
|
|
||||||
sync.enable = enable
|
|
||||||
})
|
|
||||||
watch(() => appSetting['sync.port'], port => {
|
|
||||||
void sendSyncAction({
|
|
||||||
action: 'enable',
|
|
||||||
data: {
|
data: {
|
||||||
enable: appSetting['sync.enable'],
|
enable: appSetting['sync.enable'],
|
||||||
port: appSetting['sync.port'],
|
port: appSetting['sync.server.port'],
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
sync.port = 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.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 => {
|
watch(() => appSetting['network.proxy.enable'], enable => {
|
||||||
|
|
|
@ -2,23 +2,33 @@ import { markRaw, onBeforeUnmount } from '@common/utils/vueTools'
|
||||||
import { onSyncAction, sendSyncAction } from '@renderer/utils/ipc'
|
import { onSyncAction, sendSyncAction } from '@renderer/utils/ipc'
|
||||||
import { sync } from '@renderer/store'
|
import { sync } from '@renderer/store'
|
||||||
import { appSetting } from '@renderer/store/setting'
|
import { appSetting } from '@renderer/store/setting'
|
||||||
|
import { SYNC_CODE } from '@common/constants'
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
const handleSyncList = (event: LX.Sync.SyncMainWindowActions) => {
|
const handleSyncList = (event: LX.Sync.SyncMainWindowActions) => {
|
||||||
|
console.log(event)
|
||||||
switch (event.action) {
|
switch (event.action) {
|
||||||
case 'select_mode':
|
case 'select_mode':
|
||||||
sync.deviceName = event.data.deviceName
|
sync.deviceName = event.data
|
||||||
sync.isShowSyncMode = true
|
sync.isShowSyncMode = true
|
||||||
break
|
break
|
||||||
case 'close_select_mode':
|
case 'close_select_mode':
|
||||||
sync.isShowSyncMode = false
|
sync.isShowSyncMode = false
|
||||||
break
|
break
|
||||||
case 'status':
|
case 'server_status':
|
||||||
sync.status.status = event.data.status
|
sync.server.status.status = event.data.status
|
||||||
sync.status.message = event.data.message
|
sync.server.status.message = event.data.message
|
||||||
sync.status.address = markRaw(event.data.address)
|
sync.server.status.address = markRaw(event.data.address)
|
||||||
sync.status.code = event.data.code
|
sync.server.status.code = event.data.code
|
||||||
sync.status.devices = markRaw(event.data.devices)
|
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
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -33,15 +43,36 @@ export default () => {
|
||||||
|
|
||||||
return async() => {
|
return async() => {
|
||||||
sync.enable = appSetting['sync.enable']
|
sync.enable = appSetting['sync.enable']
|
||||||
sync.port = appSetting['sync.port']
|
sync.mode = appSetting['sync.mode']
|
||||||
if (appSetting['sync.enable'] && appSetting['sync.port']) {
|
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({
|
void sendSyncAction({
|
||||||
action: 'enable',
|
action: 'enable_server',
|
||||||
data: {
|
data: {
|
||||||
enable: appSetting['sync.enable'],
|
enable: appSetting['sync.enable'],
|
||||||
port: appSetting['sync.port'],
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,21 +27,36 @@ export const proxy: {
|
||||||
}
|
}
|
||||||
export const sync: {
|
export const sync: {
|
||||||
enable: boolean
|
enable: boolean
|
||||||
port: string
|
mode: LX.AppSetting['sync.mode']
|
||||||
isShowSyncMode: boolean
|
isShowSyncMode: boolean
|
||||||
|
isShowAuthCodeModal: boolean
|
||||||
deviceName: string
|
deviceName: string
|
||||||
|
server: {
|
||||||
|
port: string
|
||||||
status: {
|
status: {
|
||||||
status: boolean
|
status: boolean
|
||||||
message: string
|
message: string
|
||||||
address: string[]
|
address: string[]
|
||||||
code: string
|
code: string
|
||||||
devices: LX.Sync.KeyInfo[]
|
devices: LX.Sync.ServerKeyInfo[]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
client: {
|
||||||
|
host: string
|
||||||
|
status: {
|
||||||
|
status: boolean
|
||||||
|
message: string
|
||||||
|
address: string[]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} = reactive({
|
} = reactive({
|
||||||
enable: false,
|
enable: false,
|
||||||
port: '',
|
mode: 'server',
|
||||||
isShowSyncMode: false,
|
isShowSyncMode: false,
|
||||||
|
isShowAuthCodeModal: false,
|
||||||
deviceName: '',
|
deviceName: '',
|
||||||
|
server: {
|
||||||
|
port: '',
|
||||||
status: {
|
status: {
|
||||||
status: false,
|
status: false,
|
||||||
message: '',
|
message: '',
|
||||||
|
@ -49,6 +64,15 @@ export const sync: {
|
||||||
code: '',
|
code: '',
|
||||||
devices: [],
|
devices: [],
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
client: {
|
||||||
|
host: '',
|
||||||
|
status: {
|
||||||
|
status: false,
|
||||||
|
message: '',
|
||||||
|
address: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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')")
|
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")
|
svg-icon(name="help-circle-outline")
|
||||||
dd
|
dd
|
||||||
base-checkbox(id="setting_sync_enable" :modelValue="appSetting['sync.enable']" @update:modelValue="updateSetting({ 'sync.enable': $event })" :label="syncEnableTitle")
|
base-checkbox(id="setting_sync_enable" :modelValue="appSetting['sync.enable']" @update:modelValue="updateSetting({ 'sync.enable': $event })" :label="$t('setting__sync_enable')")
|
||||||
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')}}
|
|
||||||
dd
|
dd
|
||||||
h3#sync_port {{$t('setting__sync_port')}}
|
h3#sync_mode {{$t('setting__sync_mode')}}
|
||||||
div
|
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
|
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>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
@ -26,6 +45,7 @@ import { openUrl } from '@common/utils/electron'
|
||||||
import { useI18n } from '@renderer/plugins/i18n'
|
import { useI18n } from '@renderer/plugins/i18n'
|
||||||
import { appSetting, updateSetting } from '@renderer/store/setting'
|
import { appSetting, updateSetting } from '@renderer/store/setting'
|
||||||
import { debounce } from '@common/utils/common'
|
import { debounce } from '@common/utils/common'
|
||||||
|
import { SYNC_CODE } from '@common/constants'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'SettingSync',
|
name: 'SettingSync',
|
||||||
|
@ -33,19 +53,39 @@ export default {
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
|
|
||||||
|
|
||||||
const syncEnableTitle = computed(() => {
|
const syncEnableServerTitle = computed(() => {
|
||||||
let title = t('setting__sync_enable')
|
let title = t('setting__sync_server_mode')
|
||||||
if (sync.status.message) {
|
if (sync.server.status.message) {
|
||||||
title += ` [${sync.status.message}]`
|
title += ` [${sync.server.status.message}]`
|
||||||
}
|
}
|
||||||
// else if (this.sync.status.address.length) {
|
// else if (this.sync.server.status.address.length) {
|
||||||
// // title += ` [${this.sync.status.address.join(', ')}]`
|
// // title += ` [${this.sync.server.status.address.join(', ')}]`
|
||||||
// }
|
// }
|
||||||
return title
|
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(() => {
|
const syncDevices = computed(() => {
|
||||||
return sync.status.devices.length
|
return sync.server.status.devices.length
|
||||||
? sync.status.devices.map(d => `${d.deviceName} (${d.clientId.substring(0, 5)})`).join(', ')
|
? sync.server.status.devices.map(d => `${d.deviceName} (${d.clientId.substring(0, 5)})`).join(', ')
|
||||||
: ''
|
: ''
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -53,8 +93,12 @@ export default {
|
||||||
sendSyncAction({ action: 'generate_code' })
|
sendSyncAction({ action: 'generate_code' })
|
||||||
}
|
}
|
||||||
|
|
||||||
const setSyncPort = debounce(port => {
|
const setSyncServerPort = debounce(port => {
|
||||||
updateSetting({ 'sync.port': port.trim() })
|
updateSetting({ 'sync.server.port': port.trim() })
|
||||||
|
}, 500)
|
||||||
|
|
||||||
|
const setSyncClientHost = debounce(host => {
|
||||||
|
updateSetting({ 'sync.client.host': host.trim() })
|
||||||
}, 500)
|
}, 500)
|
||||||
|
|
||||||
|
|
||||||
|
@ -62,10 +106,12 @@ export default {
|
||||||
appSetting,
|
appSetting,
|
||||||
updateSetting,
|
updateSetting,
|
||||||
sync,
|
sync,
|
||||||
syncEnableTitle,
|
syncEnableServerTitle,
|
||||||
setSyncPort,
|
setSyncServerPort,
|
||||||
|
setSyncClientHost,
|
||||||
syncDevices,
|
syncDevices,
|
||||||
refreshSyncCode,
|
refreshSyncCode,
|
||||||
|
clientStatus,
|
||||||
openUrl,
|
openUrl,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -76,4 +122,12 @@ export default {
|
||||||
.save-path {
|
.save-path {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.portInput[disabled], .hostInput[disabled] {
|
||||||
|
opacity: .8 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hostInput {
|
||||||
|
min-width: 380px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
Loading…
Reference in New Issue