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

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

692
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -282,7 +282,6 @@
"electron-log": "^4.4.8", "electron-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",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,21 +1,28 @@
// import Event from './event/event' // import 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()
}
}) })
} }

View File

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

View File

@ -1,22 +1,17 @@
import type http from 'http' import 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)
} }
} }
} }

View File

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

View File

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

View File

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

View File

@ -1,16 +1,18 @@
import http from 'http' import 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,
}

View File

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

View File

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

View File

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

View File

@ -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' })

View File

@ -1,4 +1,4 @@
import { Socket as _Socket, RemoteSocket as _RemoteSocket } from 'socket.io' import type WS from 'ws'
type DefaultEventsMap = Record<string, (...args: any[]) => void> 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'
} }
} }

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

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

View File

@ -11,6 +11,7 @@
<layout-update-modal /> <layout-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>

View File

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

View File

@ -23,7 +23,7 @@
<dl :class="$style.btnGroup"> <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>

View File

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

View File

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

View File

@ -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: [],
},
},
}) })

View File

@ -4,18 +4,37 @@ dt#sync
button(class="help-btn" @click="openUrl('https://lyswhut.github.io/lx-music-doc/desktop/faq/sync')" :aria-label="$t('setting__sync_tip')") 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>