-
Notifications
You must be signed in to change notification settings - Fork 51
Expand file tree
/
Copy pathbackupService.js
More file actions
546 lines (484 loc) · 16.4 KB
/
backupService.js
File metadata and controls
546 lines (484 loc) · 16.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
/**
* 数据备份还原服务
* 负责应用数据的备份和还原功能
*/
import { Message } from '@arco-design/web-vue'
import siteService from '@/api/services/site'
// 备份数据版本号,用于兼容性检查
const BACKUP_VERSION = '1.0.0'
// 需要备份的localStorage键名
const BACKUP_KEYS = {
// 设置相关
ADDRESS_SETTINGS: 'addressSettings',
APP_SETTINGS: 'appSettings',
CSP_CONFIG: 'csp_config',
// 用户数据
FAVORITES: 'drplayer-favorites',
WATCH_HISTORY: 'drplayer_watch_history',
DAILY_STATS: 'drplayer_daily_stats',
WEEKLY_STATS: 'drplayer_weekly_stats',
HISTORIES: 'drplayer_histories', // 历史页面数据
// 站点数据
SITE_STORE: 'siteStore',
CONFIG_URL: 'drplayer_config_url',
LIVE_CONFIG_URL: 'drplayer_live_config_url',
CURRENT_SITE: 'drplayer_current_site',
// 聚合搜索相关
SEARCH_AGGREGATION_SETTINGS: 'searchAggregationSettings', // 聚合搜索源选择设置
SEARCH_AGGREGATION_PAGE_STATE: 'pageState_searchAggregation', // 聚合搜索页面状态
SEARCH_HISTORY: 'drplayer_search_history', // 搜索历史记录
// 其他功能设置
SKIP_SETTINGS: 'skipSettings',
PARSER_CONFIG: 'parserConfig',
PARSERS: 'drplayer_parsers', // 解析器数据
SIDEBAR_COLLAPSED: 'sidebarCollapsed',
PAGE_STATE: 'pageState',
// 开发者调试设置
DEBUG_SETTINGS: 'debugSettings',
// 悬浮组件相关
FLOATING_BUTTON_POSITION: 'floating-iframe-button-position',
FLOATING_WINDOW_POSITION: 'floating-iframe-window-position',
FLOATING_WINDOW_SIZE: 'floating-iframe-window-size'
}
// 存储为字符串的键(不需要JSON解析)
const STRING_KEYS = new Set([
BACKUP_KEYS.CONFIG_URL,
BACKUP_KEYS.LIVE_CONFIG_URL,
BACKUP_KEYS.CURRENT_SITE,
BACKUP_KEYS.SIDEBAR_COLLAPSED
])
/**
* 获取当前时间戳字符串
*/
const getTimestamp = () => {
return new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5)
}
/**
* 从localStorage获取数据
*/
const getLocalStorageData = (key, defaultValue = null) => {
try {
const data = localStorage.getItem(key)
if (!data) return defaultValue
// 如果是字符串类型的键,直接返回字符串
if (STRING_KEYS.has(key)) {
return data
}
// 其他键尝试JSON解析
return JSON.parse(data)
} catch (error) {
console.warn(`获取 ${key} 数据失败:`, error)
return defaultValue
}
}
/**
* 设置localStorage数据
*/
const setLocalStorageData = (key, data) => {
try {
if (data === null || data === undefined) {
localStorage.removeItem(key)
} else {
// 如果是字符串类型的键,直接存储字符串
if (STRING_KEYS.has(key)) {
localStorage.setItem(key, data || '')
} else {
// 其他键使用JSON序列化
localStorage.setItem(key, JSON.stringify(data))
}
}
return true
} catch (error) {
console.error(`保存 ${key} 数据失败:`, error)
return false
}
}
/**
* 收集所有地址配置历史数据
*/
const collectAddressHistories = () => {
const addressHistories = {}
const addressHistoryKeys = [
'address-history-vod-config',
'address-history-live-config',
'address-history-proxy-access',
'address-history-proxy-play',
'address-history-proxy-sniff'
]
addressHistoryKeys.forEach(key => {
const data = getLocalStorageData(key, [])
if (data && data.length > 0) {
addressHistories[key] = data
}
})
return addressHistories
}
/**
* 收集所有需要备份的数据
*/
export const collectBackupData = () => {
const backupData = {
// 备份元信息
meta: {
version: BACKUP_VERSION,
timestamp: new Date().toISOString(),
appName: 'DrPlayer',
description: '应用数据备份文件'
},
// 设置数据
settings: {
// 地址设置
addressSettings: getLocalStorageData(BACKUP_KEYS.ADDRESS_SETTINGS, {}),
// 应用设置
appSettings: getLocalStorageData(BACKUP_KEYS.APP_SETTINGS, {}),
// CSP配置
cspConfig: getLocalStorageData(BACKUP_KEYS.CSP_CONFIG, {}),
// 跳过设置
skipSettings: getLocalStorageData(BACKUP_KEYS.SKIP_SETTINGS, {}),
// 解析器配置
parserConfig: getLocalStorageData(BACKUP_KEYS.PARSER_CONFIG, {}),
// 解析器数据
parsers: getLocalStorageData(BACKUP_KEYS.PARSERS, []),
// 聚合搜索设置
searchAggregationSettings: getLocalStorageData(BACKUP_KEYS.SEARCH_AGGREGATION_SETTINGS, {}),
// 侧边栏状态
sidebarCollapsed: getLocalStorageData(BACKUP_KEYS.SIDEBAR_COLLAPSED, false),
// 页面状态
pageState: getLocalStorageData(BACKUP_KEYS.PAGE_STATE, {})
},
// 用户数据
userData: {
// 收藏列表
favorites: getLocalStorageData(BACKUP_KEYS.FAVORITES, []),
// 观看历史(watchStatsService使用)
watchHistory: getLocalStorageData(BACKUP_KEYS.WATCH_HISTORY, []),
// 历史页面数据(historyStore使用)
histories: getLocalStorageData(BACKUP_KEYS.HISTORIES, []),
// 搜索历史记录
searchHistory: getLocalStorageData(BACKUP_KEYS.SEARCH_HISTORY, []),
// 聚合搜索页面状态
searchAggregationPageState: getLocalStorageData(BACKUP_KEYS.SEARCH_AGGREGATION_PAGE_STATE, {}),
// 每日统计
dailyStats: getLocalStorageData(BACKUP_KEYS.DAILY_STATS, {}),
// 周统计
weeklyStats: getLocalStorageData(BACKUP_KEYS.WEEKLY_STATS, {}),
// 地址配置历史
addressHistories: collectAddressHistories()
},
// 站点和配置数据
siteData: {
// 站点存储
siteStore: getLocalStorageData(BACKUP_KEYS.SITE_STORE, {}),
// 配置地址
configUrl: getLocalStorageData(BACKUP_KEYS.CONFIG_URL, ''),
// 直播配置地址
liveConfigUrl: getLocalStorageData(BACKUP_KEYS.LIVE_CONFIG_URL, ''),
// 当前站点
currentSite: getLocalStorageData(BACKUP_KEYS.CURRENT_SITE, null)
},
// 开发者调试设置
debugSettings: getLocalStorageData(BACKUP_KEYS.DEBUG_SETTINGS, {}),
// 悬浮组件数据
floatingData: {
// 悬浮按钮位置
buttonPosition: getLocalStorageData(BACKUP_KEYS.FLOATING_BUTTON_POSITION, null),
// 悬浮窗口位置
windowPosition: getLocalStorageData(BACKUP_KEYS.FLOATING_WINDOW_POSITION, null),
// 悬浮窗口尺寸
windowSize: getLocalStorageData(BACKUP_KEYS.FLOATING_WINDOW_SIZE, null)
}
}
return backupData
}
/**
* 验证备份数据格式
*/
export const validateBackupData = (data) => {
try {
// 检查基本结构
if (!data || typeof data !== 'object') {
return { valid: false, error: '备份数据格式无效' }
}
// 检查元信息
if (!data.meta || !data.meta.version || !data.meta.timestamp) {
return { valid: false, error: '备份文件缺少必要的元信息' }
}
// 检查版本兼容性
if (data.meta.version !== BACKUP_VERSION) {
console.warn(`备份文件版本 ${data.meta.version} 与当前版本 ${BACKUP_VERSION} 不匹配`)
// 暂时允许不同版本,但给出警告
}
// 检查必要的数据结构
const requiredSections = ['settings', 'userData', 'siteData']
for (const section of requiredSections) {
if (!data[section] || typeof data[section] !== 'object') {
return { valid: false, error: `备份数据缺少 ${section} 部分` }
}
}
return { valid: true }
} catch (error) {
return { valid: false, error: '备份数据解析失败: ' + error.message }
}
}
/**
* 导出备份数据到JSON文件
*/
export const exportBackupData = () => {
try {
const backupData = collectBackupData()
const timestamp = getTimestamp()
const filename = `DrPlayer_backup_${timestamp}.json`
// 创建下载链接
const dataStr = JSON.stringify(backupData, null, 2)
const dataBlob = new Blob([dataStr], { type: 'application/json' })
const url = URL.createObjectURL(dataBlob)
// 创建下载元素
const link = document.createElement('a')
link.href = url
link.download = filename
link.style.display = 'none'
// 触发下载
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
// 清理URL对象
URL.revokeObjectURL(url)
Message.success(`备份文件已导出: ${filename}`)
return { success: true, filename }
} catch (error) {
console.error('导出备份数据失败:', error)
Message.error('导出备份失败: ' + error.message)
return { success: false, error: error.message }
}
}
/**
* 还原备份数据
*/
export const restoreBackupData = (backupData) => {
try {
// 验证备份数据
const validation = validateBackupData(backupData)
if (!validation.valid) {
Message.error(validation.error)
return { success: false, error: validation.error }
}
let restoredCount = 0
let failedCount = 0
const errors = []
// 还原设置数据
if (backupData.settings) {
for (const [key, value] of Object.entries(backupData.settings)) {
if (key === 'parsers') {
// 特殊处理解析器数据
if (setLocalStorageData(BACKUP_KEYS.PARSERS, value)) {
restoredCount++
} else {
failedCount++
errors.push(`解析器数据`)
}
} else if (key === 'searchAggregationSettings') {
// 特殊处理聚合搜索设置
if (setLocalStorageData(BACKUP_KEYS.SEARCH_AGGREGATION_SETTINGS, value)) {
restoredCount++
} else {
failedCount++
errors.push(`聚合搜索设置`)
}
} else {
const storageKey = Object.values(BACKUP_KEYS).find(k => k.includes(key) || key.includes(k.split('_').pop()))
if (storageKey) {
if (setLocalStorageData(storageKey, value)) {
restoredCount++
} else {
failedCount++
errors.push(`设置数据 ${key}`)
}
} else {
// 直接使用key名称
if (setLocalStorageData(key, value)) {
restoredCount++
} else {
failedCount++
errors.push(`设置数据 ${key}`)
}
}
}
}
}
// 还原用户数据
if (backupData.userData) {
const userDataMapping = {
favorites: BACKUP_KEYS.FAVORITES,
watchHistory: BACKUP_KEYS.WATCH_HISTORY,
dailyStats: BACKUP_KEYS.DAILY_STATS,
weeklyStats: BACKUP_KEYS.WEEKLY_STATS,
histories: BACKUP_KEYS.HISTORIES, // 历史页面数据
searchHistory: BACKUP_KEYS.SEARCH_HISTORY, // 搜索历史记录
searchAggregationPageState: BACKUP_KEYS.SEARCH_AGGREGATION_PAGE_STATE // 聚合搜索页面状态
}
for (const [key, value] of Object.entries(backupData.userData)) {
const storageKey = userDataMapping[key]
if (storageKey && setLocalStorageData(storageKey, value)) {
restoredCount++
} else if (key === 'addressHistories' && value) {
// 还原地址配置历史
for (const [historyKey, historyValue] of Object.entries(value)) {
if (setLocalStorageData(historyKey, historyValue)) {
restoredCount++
} else {
failedCount++
errors.push(`地址配置历史 ${historyKey}`)
}
}
} else {
failedCount++
errors.push(`用户数据 ${key}`)
}
}
}
// 还原站点数据
if (backupData.siteData) {
const siteDataMapping = {
siteStore: BACKUP_KEYS.SITE_STORE,
configUrl: BACKUP_KEYS.CONFIG_URL,
liveConfigUrl: BACKUP_KEYS.LIVE_CONFIG_URL,
currentSite: BACKUP_KEYS.CURRENT_SITE
}
let restoredCurrentSite = null
for (const [key, value] of Object.entries(backupData.siteData)) {
const storageKey = siteDataMapping[key]
if (storageKey && setLocalStorageData(storageKey, value)) {
restoredCount++
// 记录还原的当前站点信息
if (key === 'currentSite' && value) {
restoredCurrentSite = value
}
} else {
failedCount++
errors.push(`站点数据 ${key}`)
}
}
// 如果还原了当前站点,需要同步到siteService
if (restoredCurrentSite) {
try {
// 解析当前站点数据(可能是字符串或对象)
const currentSiteData = typeof restoredCurrentSite === 'string'
? JSON.parse(restoredCurrentSite)
: restoredCurrentSite
if (currentSiteData && currentSiteData.key) {
// 同步到siteService(如果站点存在)
const success = siteService.setCurrentSite(currentSiteData.key)
if (success) {
console.log('已同步还原的当前站点到siteService:', currentSiteData.name)
} else {
console.warn('还原的当前站点在站点列表中不存在,可能需要重新配置:', currentSiteData.key)
}
}
} catch (error) {
console.error('同步还原的当前站点到siteService失败:', error)
}
}
}
// 还原开发者调试设置
if (backupData.debugSettings) {
if (setLocalStorageData(BACKUP_KEYS.DEBUG_SETTINGS, backupData.debugSettings)) {
restoredCount++
} else {
failedCount++
errors.push('开发者调试设置')
}
}
// 还原悬浮组件数据
if (backupData.floatingData) {
const floatingDataMapping = {
buttonPosition: BACKUP_KEYS.FLOATING_BUTTON_POSITION,
windowPosition: BACKUP_KEYS.FLOATING_WINDOW_POSITION,
windowSize: BACKUP_KEYS.FLOATING_WINDOW_SIZE
}
for (const [key, value] of Object.entries(backupData.floatingData)) {
const storageKey = floatingDataMapping[key]
if (storageKey && value !== null) {
if (setLocalStorageData(storageKey, value)) {
restoredCount++
} else {
failedCount++
errors.push(`悬浮组件数据 ${key}`)
}
}
}
}
// 显示还原结果
if (failedCount === 0) {
Message.success(`数据还原成功!共还原 ${restoredCount} 项数据`)
} else {
Message.warning(`数据还原完成!成功 ${restoredCount} 项,失败 ${failedCount} 项`)
if (errors.length > 0) {
console.warn('还原失败的项目:', errors)
}
}
return {
success: true,
restoredCount,
failedCount,
errors,
needsReload: true // 提示需要刷新页面
}
} catch (error) {
console.error('还原备份数据失败:', error)
Message.error('还原备份失败: ' + error.message)
return { success: false, error: error.message }
}
}
/**
* 从文件导入备份数据
*/
export const importBackupFromFile = (file) => {
return new Promise((resolve, reject) => {
if (!file) {
reject(new Error('请选择备份文件'))
return
}
// 检查文件类型
if (!file.name.endsWith('.json')) {
reject(new Error('请选择JSON格式的备份文件'))
return
}
const reader = new FileReader()
reader.onload = (e) => {
try {
const backupData = JSON.parse(e.target.result)
const result = restoreBackupData(backupData)
resolve(result)
} catch (error) {
reject(new Error('备份文件格式错误: ' + error.message))
}
}
reader.onerror = () => {
reject(new Error('文件读取失败'))
}
reader.readAsText(file)
})
}
/**
* 获取备份数据统计信息
*/
export const getBackupStats = () => {
const backupData = collectBackupData()
const stats = {
settings: Object.keys(backupData.settings).length,
favorites: backupData.userData.favorites?.length || 0,
watchHistory: backupData.userData.histories?.length || 0, // 使用历史页面数据
parsers: backupData.settings.parsers?.length || 0, // 解析器数量
sites: Object.keys(backupData.siteData.siteStore || {}).length,
totalSize: JSON.stringify(backupData).length
}
return stats
}
export default {
collectBackupData,
validateBackupData,
exportBackupData,
restoreBackupData,
importBackupFromFile,
getBackupStats
}