-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdataService.js
More file actions
352 lines (329 loc) · 12.1 KB
/
Copy pathdataService.js
File metadata and controls
352 lines (329 loc) · 12.1 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
// 常量已在 shared.js 中定义
// ── 域名分组(同一主站的关联域名合并统计,如 bilivideo.com → bilibili.com)──
// 预置映射只覆盖明确的平台自有域名;用户自定义规则存在 storage(DOMAIN_GROUP_KEY),优先级更高。
const PRESET_GROUP_MAP = {
// 哔哩哔哩
'hdslb.com': 'bilibili.com',
'bilivideo.cn': 'bilibili.com',
'bilivideo.com': 'bilibili.com',
'biliimg.com': 'bilibili.com',
'biliapi.net': 'bilibili.com',
'biliapi.com': 'bilibili.com',
'acgvideo.com': 'bilibili.com',
// GitHub
'githubassets.com': 'github.com',
'githubusercontent.com': 'github.com',
'github.io': 'github.com',
// 阿里 / 淘宝
'aliyuncs.com': 'aliyun.com',
'alicdn.com': 'aliyun.com',
'taobaocdn.com': 'taobao.com',
'tbcdn.cn': 'taobao.com',
'mmstat.com': 'taobao.com',
// 腾讯 / QQ
'qpic.cn': 'qq.com',
'qlogo.cn': 'qq.com',
'gtimg.cn': 'qq.com',
'gtimg.com': 'qq.com',
'myqcloud.com': 'tencent.com',
'qcloud.com': 'tencent.com',
'tencentcs.com': 'tencent.com',
// 字节跳动 / 抖音 / 头条
'volccdn.com': 'volcengine.com',
'byteimg.com': 'douyin.com',
'bytecdn.cn': 'douyin.com',
'douyinstatic.com': 'douyin.com',
'pstatp.com': 'toutiao.com',
'byteacctimg.com': 'toutiao.com',
// CSDN
'csdnimg.cn': 'csdn.net',
// 京东 / 拼多多
'360buyimg.com': 'jd.com',
'pddpic.com': 'yangkeduo.com',
// 网易 / 百度 / 华为 / 小米
'126.net': '163.com',
'ws126.net': '163.com',
'bdstatic.com': 'baidu.com',
'bcebos.com': 'baidu.com',
'myhuaweicloud.com': 'huawei.com',
'mi-img.com': 'xiaomi.com',
// 微博 / 知乎
'sinaimg.cn': 'weibo.com',
'sinajs.cn': 'weibo.com',
'zhimg.com': 'zhihu.com'
};
/** 内存缓存的分组规则(预置 + 用户自定义合并),由 initDomainGroups 加载 */
let domainGroupMap = null;
/**
* 加载分组规则(预置 + 用户自定义合并)。dashboard/popup 启动时调用一次。
*/
async function initDomainGroups() {
const result = await chrome.storage.local.get(DOMAIN_GROUP_KEY);
domainGroupMap = Object.assign({}, PRESET_GROUP_MAP, result[DOMAIN_GROUP_KEY] || {});
}
/**
* 域名归组:命中映射则返回主站域名,否则原样返回。
* 数据层不动,仅在聚合/显示时归组。
* @param {string} domain 已归一化的根域名
* @returns {string}
*/
function resolveGroup(domain) {
if (domainGroupMap === null) return domain; // 未初始化时安全降级
return domainGroupMap[domain] || domain;
}
/**
* 读取用户自定义分组规则(原始存储内容,管理界面用)
* @returns {Promise<Object>}
*/
async function getDomainGroupMap() {
const result = await chrome.storage.local.get(DOMAIN_GROUP_KEY);
return result[DOMAIN_GROUP_KEY] || {};
}
/**
* 设置用户自定义分组规则(整体覆盖)
* @param {Object} map domain → 主站
*/
async function setDomainGroupMap(map) {
await chrome.storage.local.set({ [DOMAIN_GROUP_KEY]: map || {} });
domainGroupMap = Object.assign({}, PRESET_GROUP_MAP, map || {});
}
/** 读取有流量记录的日期列表(升序) */
async function getTrackedDates() {
const result = await chrome.storage.local.get(STORAGE_DATES);
return result[STORAGE_DATES] || [];
}
/**
* 批量读取浏览流量原始数据
* @param {string[]} dates 日期数组 'YYYY-MM-DD'
* @returns {Promise<Object<string, Object<string, number>>>} date -> {domain: bytes}
*/
async function getBrowseData(dates) {
const keys = dates.map(d => DAILY_PREFIX + d);
const items = await chrome.storage.local.get(keys);
const result = {};
for (const date of dates) {
result[date] = items[DAILY_PREFIX + date] || {};
}
return result;
}
/**
* 批量读取下载流量原始数据
* @param {string[]} dates 日期数组 'YYYY-MM-DD'
* @returns {Promise<Object<string, Object<string, number>>>} date -> {domain: bytes}
*/
async function getDownloadData(dates) {
const keys = dates.map(d => DL_DAILY_PREFIX + d);
const items = await chrome.storage.local.get(keys);
const result = {};
for (const date of dates) {
result[date] = items[DL_DAILY_PREFIX + date] || {};
}
return result;
}
/**
* 把日期数组聚合成 {daily, monthly}:daily=按天明细,monthly=跨天按域名求和
* @param {string[]} dates 日期数组 'YYYY-MM-DD'
* @param {(dates:string[])=>Promise<Object>} dailyGetter 批量读取函数
* @returns {Promise<{daily: Object, monthly: Object}>}
*/
async function aggregateDates(dates, dailyGetter) {
if (dates.length === 0) return { daily: {}, monthly: {} };
const daily = await dailyGetter(dates);
const monthly = {};
for (const [dateStr, data] of Object.entries(daily)) {
for (const [domain, bytes] of Object.entries(data)) {
// 域名分组:关联域名(如 bilivideo.com)并入主站(bilibili.com)统计
const group = resolveGroup(domain);
monthly[group] = (monthly[group] || 0) + bytes;
}
}
return { daily, monthly };
}
/**
* 获取某月/某年浏览流量聚合(dashboard 主数据源)
* @param {number} year 年份
* @param {number} [month] 月份(缺省=整年)
* @returns {Promise<{daily: Object, monthly: Object}>} daily: date->{domain:bytes},monthly: {domain:bytes}
*/
async function getBrowseAggregated(year, month) {
const prefix = month !== undefined
? `${year}-${String(month).padStart(2, '0')}`
: String(year);
const tracked = await getTrackedDates();
return aggregateDates(tracked.filter(d => d.startsWith(prefix)), getBrowseData);
}
/** 最近 N 天浏览流量聚合(近7天/近30天视图) */
async function getRecentBrowseAggregated(days) {
return aggregateDates(getRecentDateKeys(days), getBrowseData);
}
/**
* 获取某月/某年下载流量聚合(dashboard 主数据源)
* @param {number} year 年份
* @param {number} [month] 月份(缺省=整年)
* @returns {Promise<{daily: Object, monthly: Object}>} 结构与 getBrowseAggregated 一致
*/
async function getDownloadAggregated(year, month) {
const prefix = month !== undefined
? `${year}-${String(month).padStart(2, '0')}`
: String(year);
const tracked = await getTrackedDates();
return aggregateDates(tracked.filter(d => d.startsWith(prefix)), getDownloadData);
}
/** 最近 N 天下载流量聚合(近7天/近30天视图) */
async function getRecentDownloadAggregated(days) {
return aggregateDates(getRecentDateKeys(days), getDownloadData);
}
/**
* 合并某一天浏览+下载(popup「今日」数据源)
* @param {string} dateKey 'YYYY-MM-DD'
* @returns {Promise<Object<string, number>>} domain -> bytes
*/
async function getMergedDayData(dateKey) {
const [browse, dl] = await Promise.all([
chrome.storage.local.get(DAILY_PREFIX + dateKey),
chrome.storage.local.get(DL_DAILY_PREFIX + dateKey)
]);
// 域名分组:关联域名并入主站
const merged = {};
for (const [domain, bytes] of Object.entries(browse[DAILY_PREFIX + dateKey] || {})) {
const g = resolveGroup(domain);
merged[g] = (merged[g] || 0) + bytes;
}
for (const [domain, bytes] of Object.entries(dl[DL_DAILY_PREFIX + dateKey] || {})) {
const g = resolveGroup(domain);
merged[g] = (merged[g] || 0) + bytes;
}
return merged;
}
/**
* 合并某月浏览+下载(popup「本月」数据源)
* @param {string} monthKey 'YYYY-MM'
* @returns {Promise<Object<string, number>>} domain -> bytes
*/
async function getMergedMonthData(monthKey) {
const dates = (await getTrackedDates()).filter(d => d.startsWith(monthKey));
if (dates.length === 0) return {};
const [browseItems, dlItems] = await Promise.all([
getBrowseData(dates),
getDownloadData(dates)
]);
const aggregated = {};
for (const date of dates) {
const browseData = browseItems[date] || {};
for (const [domain, bytes] of Object.entries(browseData)) {
// 域名分组:关联域名并入主站
const g = resolveGroup(domain);
aggregated[g] = (aggregated[g] || 0) + bytes;
}
const dlData = dlItems[date] || {};
for (const [domain, bytes] of Object.entries(dlData)) {
const g = resolveGroup(domain);
aggregated[g] = (aggregated[g] || 0) + bytes;
}
}
return aggregated;
}
/**
* 计算浏览/下载分开的总量(popup 顶部 breakdown)
* @param {string} dateKey 'YYYY-MM-DD' 或 'YYYY-MM'
* @param {boolean} isMonth 是否为月粒度
* @returns {Promise<{browse: number, download: number}>}
*/
async function getBreakdownTotals(dateKey, isMonth) {
if (isMonth) {
const dates = (await getTrackedDates()).filter(d => d.startsWith(dateKey));
if (dates.length === 0) return { browse: 0, download: 0 };
const [browseItems, dlItems] = await Promise.all([
getBrowseData(dates),
getDownloadData(dates)
]);
let browseTotal = 0;
for (const date of dates) {
for (const v of Object.values(browseItems[date] || {})) { browseTotal += v; }
}
let dlTotal = 0;
for (const date of dates) {
for (const v of Object.values(dlItems[date] || {})) { dlTotal += v; }
}
return { browse: browseTotal, download: dlTotal };
}
const [bRow, dlRow] = await Promise.all([
chrome.storage.local.get(DAILY_PREFIX + dateKey),
chrome.storage.local.get(DL_DAILY_PREFIX + dateKey)
]);
const browseTotal = Object.values(bRow[DAILY_PREFIX + dateKey] || {}).reduce((a, b) => a + b, 0);
const dlTotal = Object.values(dlRow[DL_DAILY_PREFIX + dateKey] || {}).reduce((a, b) => a + b, 0);
return { browse: browseTotal, download: dlTotal };
}
/**
* 获取某月细分域名数据(dashboard 域名详情)
* @returns {Promise<Object<string, number>>} hostname -> bytes
*/
async function getRawDataForMonth(year, month) {
const prefix = `${year}-${String(month).padStart(2, '0')}`;
const tracked = (await getTrackedDates()).filter(d => d.startsWith(prefix));
if (tracked.length === 0) return {};
const keys = tracked.map(d => RAW_PREFIX + d);
const items = await chrome.storage.local.get(keys);
const aggregated = {};
for (const dateStr of tracked) {
for (const [hostname, bytes] of Object.entries(items[RAW_PREFIX + dateStr] || {})) {
aggregated[hostname] = (aggregated[hostname] || 0) + bytes;
}
}
return aggregated;
}
/** @returns {Promise<Set<string>>} 已排除域名集合 */
async function getExcludedDomains() {
const result = await chrome.storage.local.get(EXCLUDED_KEY);
return new Set(result[EXCLUDED_KEY] || []);
}
/** 将域名加入排除列表(不再统计) */
async function addExclusion(domain) {
const result = await chrome.storage.local.get(EXCLUDED_KEY);
const list = result[EXCLUDED_KEY] || [];
if (!list.includes(domain)) {
list.push(domain);
await chrome.storage.local.set({ [EXCLUDED_KEY]: list });
}
}
/** 将域名移出排除列表 */
async function removeExclusion(domain) {
const result = await chrome.storage.local.get(EXCLUDED_KEY);
let list = result[EXCLUDED_KEY] || [];
list = list.filter(d => d !== domain);
await chrome.storage.local.set({ [EXCLUDED_KEY]: list });
}
/** 清空所有统计数据(不可恢复) */
async function clearAllData() {
await chrome.storage.local.clear();
}
/**
* 获取存储用量
* @returns {Promise<{used: number, quota: number, pct: number}>}
*/
async function getStorageUsage() {
const used = await chrome.storage.local.getBytesInUse(null);
const quota = chrome.storage.local.QUOTA_BYTES || 5242880;
return { used, quota, pct: used / quota };
}
/**
* 删除 daysToKeep 天之前的所有流量数据(存储压力自动清理)
* @param {number} daysToKeep 保留天数
* @returns {Promise<number>} 清理的天数
*/
async function pruneOldData(daysToKeep) {
const cutoff = new Date();
cutoff.setDate(cutoff.getDate() - daysToKeep);
const cutoffStr = cutoff.toISOString().split('T')[0];
const dates = await getTrackedDates();
const toDelete = dates.filter(d => d < cutoffStr);
if (toDelete.length === 0) return 0;
const keys = toDelete.flatMap(d => [
DAILY_PREFIX + d, DL_DAILY_PREFIX + d, RAW_PREFIX + d, DL_RAW_PREFIX + d
]);
await chrome.storage.local.remove(keys);
const remaining = dates.filter(d => d >= cutoffStr);
await chrome.storage.local.set({ [STORAGE_DATES]: remaining });
return toDelete.length;
}