tt-help-cli-ycl 1.3.34 → 1.3.36
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -17
- package/cli.js +9 -9
- package/package.json +49 -47
- package/scripts/run-explore copy.bat +101 -101
- package/scripts/run-explore.bat +132 -132
- package/scripts/run-explore.ps1 +157 -157
- package/scripts/run-explore.sh +119 -119
- package/scripts/test-captcha-lib.mjs +68 -0
- package/scripts/test-captcha.mjs +81 -0
- package/scripts/test-incognito-lib.mjs +36 -0
- package/scripts/test-login-state.mjs +128 -0
- package/scripts/test-safe-click.mjs +45 -0
- package/scripts/test-watch-db-smoke.mjs +246 -0
- package/src/cli/attach.js +240 -180
- package/src/cli/auto.js +265 -240
- package/src/cli/comments.js +301 -210
- package/src/cli/config.js +170 -152
- package/src/cli/db-import.js +51 -0
- package/src/cli/explore.js +519 -488
- package/src/cli/info.js +88 -88
- package/src/cli/open.js +111 -111
- package/src/cli/progress.js +111 -111
- package/src/cli/refresh.js +288 -216
- package/src/cli/scrape.js +47 -47
- package/src/cli/utils.js +18 -18
- package/src/cli/videos.js +41 -41
- package/src/cli/videostats.js +140 -25
- package/src/cli/watch.js +30 -31
- package/src/lib/args.js +766 -722
- package/src/lib/browser/anti-detect.js +23 -23
- package/src/lib/browser/cdp.js +261 -261
- package/src/lib/browser/health-checker.js +114 -114
- package/src/lib/browser/launch.js +43 -43
- package/src/lib/browser/page.js +183 -183
- package/src/lib/constants.js +238 -216
- package/src/lib/delay.js +54 -54
- package/src/lib/explore-fetch.js +118 -118
- package/src/lib/fetcher.js +45 -45
- package/src/lib/filter.js +66 -66
- package/src/lib/io.js +54 -54
- package/src/lib/output.js +80 -80
- package/src/lib/page-error-detector.js +105 -105
- package/src/lib/parse-ssr.mjs +69 -69
- package/src/lib/parser.js +47 -47
- package/src/lib/retry.js +45 -45
- package/src/lib/scrape.js +89 -89
- package/src/lib/tiktok-scraper.mjs +232 -194
- package/src/lib/url.js +52 -52
- package/src/main.js +70 -48
- package/src/results/user-videos-bar.lar.lar.moeta.json +37 -0
- package/src/scraper/auto-core.js +203 -203
- package/src/scraper/core.js +211 -211
- package/src/scraper/explore-core.js +177 -167
- package/src/scraper/modules/captcha-handler.js +114 -114
- package/src/scraper/modules/follow-extractor.js +194 -194
- package/src/scraper/modules/guess-extractor.js +51 -51
- package/src/scraper/modules/page-helpers.js +48 -48
- package/src/scraper/refresh-core.js +179 -179
- package/src/videos/core.js +125 -125
- package/src/watch/data-store.js +2364 -1030
- package/src/watch/public/index.html +1494 -753
- package/src/watch/server.js +628 -933
package/src/watch/data-store.js
CHANGED
|
@@ -1,1030 +1,2364 @@
|
|
|
1
|
-
import fs from "fs";
|
|
2
|
-
import
|
|
3
|
-
import
|
|
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
|
-
if (
|
|
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
|
-
|
|
547
|
-
sources
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
sources
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
if (
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
if (
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
)
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
}
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import Database from "better-sqlite3";
|
|
4
|
+
|
|
5
|
+
// SQLite 用户表(用于判重)
|
|
6
|
+
let db = null;
|
|
7
|
+
let dbPath = null;
|
|
8
|
+
|
|
9
|
+
function normalizeDbFilePath(filePath) {
|
|
10
|
+
if (!filePath) {
|
|
11
|
+
throw new Error("db path is required");
|
|
12
|
+
}
|
|
13
|
+
const resolved = path.resolve(filePath);
|
|
14
|
+
if (path.extname(resolved).toLowerCase() !== ".db") {
|
|
15
|
+
throw new Error(`仅支持 .db 路径,当前为: ${filePath}`);
|
|
16
|
+
}
|
|
17
|
+
return resolved;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function resetDbConnection() {
|
|
21
|
+
if (db) {
|
|
22
|
+
db.close();
|
|
23
|
+
db = null;
|
|
24
|
+
}
|
|
25
|
+
dbPath = null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function loadLegacyUsersFromFiles(userFilePath, doneFilePath) {
|
|
29
|
+
const merged = new Map();
|
|
30
|
+
|
|
31
|
+
const tryLoad = (targetPath, label) => {
|
|
32
|
+
if (!targetPath) return;
|
|
33
|
+
if (!fs.existsSync(targetPath)) return;
|
|
34
|
+
try {
|
|
35
|
+
const parsed = JSON.parse(fs.readFileSync(targetPath, "utf-8"));
|
|
36
|
+
if (!Array.isArray(parsed)) return;
|
|
37
|
+
for (const item of parsed) {
|
|
38
|
+
const uniqueId = item?.uniqueId || item?.unique_id;
|
|
39
|
+
if (!uniqueId) continue;
|
|
40
|
+
merged.set(uniqueId, {
|
|
41
|
+
...merged.get(uniqueId),
|
|
42
|
+
...item,
|
|
43
|
+
uniqueId,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
} catch (e) {
|
|
47
|
+
console.error(`[data-store] SQLite 导入 ${label} 失败: ${e.message}`);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
tryLoad(userFilePath, "result.json");
|
|
52
|
+
tryLoad(doneFilePath, "result-done.json");
|
|
53
|
+
|
|
54
|
+
return [...merged.values()];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function loadLegacyVideosFromFile(videoPath) {
|
|
58
|
+
if (!videoPath) return [];
|
|
59
|
+
if (!fs.existsSync(videoPath)) return [];
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
const parsed = JSON.parse(fs.readFileSync(videoPath, "utf-8"));
|
|
63
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
64
|
+
} catch (e) {
|
|
65
|
+
console.error(
|
|
66
|
+
`[data-store] SQLite 导入 result-videos.json 失败: ${e.message}`,
|
|
67
|
+
);
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function initUserDb(filePath) {
|
|
73
|
+
dbPath = normalizeDbFilePath(filePath);
|
|
74
|
+
fs.mkdirSync(path.dirname(dbPath), { recursive: true });
|
|
75
|
+
db = new Database(dbPath);
|
|
76
|
+
db.pragma("journal_mode = WAL");
|
|
77
|
+
db.exec(`
|
|
78
|
+
CREATE TABLE IF NOT EXISTS users (
|
|
79
|
+
unique_id TEXT PRIMARY KEY,
|
|
80
|
+
tt_seller TEXT,
|
|
81
|
+
verified INTEGER,
|
|
82
|
+
location_created TEXT,
|
|
83
|
+
created_at TEXT,
|
|
84
|
+
updated_at TEXT
|
|
85
|
+
)
|
|
86
|
+
`);
|
|
87
|
+
db.exec(`
|
|
88
|
+
CREATE TABLE IF NOT EXISTS jobs (
|
|
89
|
+
unique_id TEXT PRIMARY KEY,
|
|
90
|
+
nickname TEXT,
|
|
91
|
+
status TEXT DEFAULT 'pending',
|
|
92
|
+
sources TEXT,
|
|
93
|
+
claimed_by TEXT,
|
|
94
|
+
claimed_at INTEGER,
|
|
95
|
+
error TEXT,
|
|
96
|
+
pinned INTEGER DEFAULT 0,
|
|
97
|
+
no_video INTEGER DEFAULT 0,
|
|
98
|
+
restricted INTEGER DEFAULT 0,
|
|
99
|
+
user_update_count INTEGER DEFAULT 0,
|
|
100
|
+
tt_seller INTEGER,
|
|
101
|
+
verified INTEGER,
|
|
102
|
+
video_count INTEGER DEFAULT 0,
|
|
103
|
+
comment_count INTEGER DEFAULT 0,
|
|
104
|
+
guessed_location TEXT,
|
|
105
|
+
location_created TEXT,
|
|
106
|
+
follower_count INTEGER DEFAULT 0,
|
|
107
|
+
following_count INTEGER DEFAULT 0,
|
|
108
|
+
heart_count INTEGER DEFAULT 0,
|
|
109
|
+
refresh_time INTEGER,
|
|
110
|
+
processed INTEGER DEFAULT 0,
|
|
111
|
+
processed_at INTEGER,
|
|
112
|
+
created_at INTEGER,
|
|
113
|
+
updated_at INTEGER,
|
|
114
|
+
region TEXT,
|
|
115
|
+
signature TEXT,
|
|
116
|
+
sec_uid TEXT
|
|
117
|
+
)
|
|
118
|
+
`);
|
|
119
|
+
db.exec(`
|
|
120
|
+
CREATE TABLE IF NOT EXISTS videos (
|
|
121
|
+
id TEXT PRIMARY KEY,
|
|
122
|
+
href TEXT,
|
|
123
|
+
author_unique_id TEXT,
|
|
124
|
+
location_created TEXT,
|
|
125
|
+
tt_seller INTEGER DEFAULT 0,
|
|
126
|
+
registered_at INTEGER,
|
|
127
|
+
user_update_count INTEGER DEFAULT 0,
|
|
128
|
+
play_count INTEGER,
|
|
129
|
+
digg_count INTEGER,
|
|
130
|
+
comment_count INTEGER,
|
|
131
|
+
share_count INTEGER,
|
|
132
|
+
collect_count INTEGER,
|
|
133
|
+
stats_updated_at INTEGER
|
|
134
|
+
)
|
|
135
|
+
`);
|
|
136
|
+
db.exec(`
|
|
137
|
+
CREATE INDEX IF NOT EXISTS idx_jobs_status_video
|
|
138
|
+
ON jobs(status, video_count DESC)
|
|
139
|
+
`);
|
|
140
|
+
db.exec(`
|
|
141
|
+
CREATE INDEX IF NOT EXISTS idx_jobs_claimed_by_status
|
|
142
|
+
ON jobs(claimed_by, status, claimed_at)
|
|
143
|
+
`);
|
|
144
|
+
db.exec(`
|
|
145
|
+
CREATE INDEX IF NOT EXISTS idx_jobs_status_claimed_at
|
|
146
|
+
ON jobs(status, claimed_at)
|
|
147
|
+
`);
|
|
148
|
+
db.exec(`
|
|
149
|
+
CREATE INDEX IF NOT EXISTS idx_jobs_redo_target
|
|
150
|
+
ON jobs(tt_seller, verified, location_created, refresh_time)
|
|
151
|
+
`);
|
|
152
|
+
db.exec(`
|
|
153
|
+
CREATE INDEX IF NOT EXISTS idx_jobs_pending_priority
|
|
154
|
+
ON jobs(status, pinned DESC, guessed_location, follower_count DESC)
|
|
155
|
+
`);
|
|
156
|
+
db.exec(`
|
|
157
|
+
CREATE INDEX IF NOT EXISTS idx_jobs_claim_pending_pinned
|
|
158
|
+
ON jobs(created_at ASC, unique_id ASC)
|
|
159
|
+
WHERE status = 'pending' AND COALESCE(pinned, 0) = 1
|
|
160
|
+
`);
|
|
161
|
+
db.exec(`
|
|
162
|
+
CREATE INDEX IF NOT EXISTS idx_jobs_claim_pending_seller
|
|
163
|
+
ON jobs(UPPER(COALESCE(guessed_location, '')), follower_count DESC, created_at ASC, unique_id ASC)
|
|
164
|
+
WHERE status = 'pending'
|
|
165
|
+
AND COALESCE(pinned, 0) = 0
|
|
166
|
+
AND tt_seller = 1
|
|
167
|
+
AND verified = 0
|
|
168
|
+
`);
|
|
169
|
+
db.exec(`
|
|
170
|
+
CREATE INDEX IF NOT EXISTS idx_jobs_claim_pending_follow
|
|
171
|
+
ON jobs(UPPER(COALESCE(guessed_location, '')), follower_count DESC, created_at ASC, unique_id ASC)
|
|
172
|
+
WHERE status = 'pending'
|
|
173
|
+
AND COALESCE(pinned, 0) = 0
|
|
174
|
+
AND (
|
|
175
|
+
instr(COALESCE(sources, ''), '"following"') > 0
|
|
176
|
+
OR instr(COALESCE(sources, ''), '"follower"') > 0
|
|
177
|
+
)
|
|
178
|
+
`);
|
|
179
|
+
db.exec(`
|
|
180
|
+
CREATE INDEX IF NOT EXISTS idx_jobs_claim_pending_other
|
|
181
|
+
ON jobs(UPPER(COALESCE(guessed_location, '')), follower_count DESC, created_at ASC, unique_id ASC)
|
|
182
|
+
WHERE status = 'pending' AND COALESCE(pinned, 0) = 0
|
|
183
|
+
`);
|
|
184
|
+
db.exec(`
|
|
185
|
+
CREATE INDEX IF NOT EXISTS idx_jobs_user_update_queue
|
|
186
|
+
ON jobs(created_at ASC, unique_id ASC)
|
|
187
|
+
WHERE (tt_seller IS NULL OR tt_seller = '')
|
|
188
|
+
AND (user_update_count IS NULL OR user_update_count <= 0)
|
|
189
|
+
`);
|
|
190
|
+
db.exec(`
|
|
191
|
+
CREATE INDEX IF NOT EXISTS idx_jobs_user_update_queue_expr
|
|
192
|
+
ON jobs(created_at ASC, unique_id ASC)
|
|
193
|
+
WHERE COALESCE(tt_seller, '') = ''
|
|
194
|
+
AND COALESCE(user_update_count, 0) <= 0
|
|
195
|
+
`);
|
|
196
|
+
db.exec(`
|
|
197
|
+
CREATE INDEX IF NOT EXISTS idx_videos_comment_queue
|
|
198
|
+
ON videos(user_update_count, tt_seller DESC, registered_at ASC)
|
|
199
|
+
`);
|
|
200
|
+
db.exec(`
|
|
201
|
+
CREATE INDEX IF NOT EXISTS idx_videos_comment_queue_pending
|
|
202
|
+
ON videos(tt_seller DESC, registered_at ASC, id)
|
|
203
|
+
WHERE user_update_count IS NULL OR user_update_count <= 0
|
|
204
|
+
`);
|
|
205
|
+
|
|
206
|
+
const existingVideoColumns = new Set(
|
|
207
|
+
db
|
|
208
|
+
.prepare("PRAGMA table_info(videos)")
|
|
209
|
+
.all()
|
|
210
|
+
.map((column) => column.name),
|
|
211
|
+
);
|
|
212
|
+
const requiredVideoColumns = {
|
|
213
|
+
play_count: "INTEGER",
|
|
214
|
+
digg_count: "INTEGER",
|
|
215
|
+
comment_count: "INTEGER",
|
|
216
|
+
share_count: "INTEGER",
|
|
217
|
+
collect_count: "INTEGER",
|
|
218
|
+
stats_updated_at: "INTEGER",
|
|
219
|
+
};
|
|
220
|
+
for (const [column, type] of Object.entries(requiredVideoColumns)) {
|
|
221
|
+
if (!existingVideoColumns.has(column)) {
|
|
222
|
+
db.exec(`ALTER TABLE videos ADD COLUMN ${column} ${type}`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const count = db.prepare("SELECT COUNT(*) as c FROM users").get().c;
|
|
227
|
+
console.log(`[data-store] SQLite users 表初始化完成: ${count} 条`);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function importLegacyJsonToDb({
|
|
231
|
+
dbFilePath,
|
|
232
|
+
usersFilePath,
|
|
233
|
+
doneFilePath,
|
|
234
|
+
videosFilePath,
|
|
235
|
+
}) {
|
|
236
|
+
resetDbConnection();
|
|
237
|
+
initUserDb(dbFilePath);
|
|
238
|
+
|
|
239
|
+
const legacyUsers = loadLegacyUsersFromFiles(usersFilePath, doneFilePath);
|
|
240
|
+
const legacyVideos = loadLegacyVideosFromFile(videosFilePath);
|
|
241
|
+
|
|
242
|
+
const beforeUsers = db.prepare("SELECT COUNT(*) as c FROM users").get().c;
|
|
243
|
+
const beforeJobs = db.prepare("SELECT COUNT(*) as c FROM jobs").get().c;
|
|
244
|
+
const beforeVideos = db.prepare("SELECT COUNT(*) as c FROM videos").get().c;
|
|
245
|
+
|
|
246
|
+
const insertUserStmt = db.prepare(`
|
|
247
|
+
INSERT OR IGNORE INTO users (unique_id) VALUES (?)
|
|
248
|
+
`);
|
|
249
|
+
const insertVideoStmt = db.prepare(`
|
|
250
|
+
INSERT OR IGNORE INTO videos (
|
|
251
|
+
id,
|
|
252
|
+
href,
|
|
253
|
+
author_unique_id,
|
|
254
|
+
location_created,
|
|
255
|
+
tt_seller,
|
|
256
|
+
registered_at,
|
|
257
|
+
user_update_count
|
|
258
|
+
)
|
|
259
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
260
|
+
`);
|
|
261
|
+
|
|
262
|
+
const importUsersTxn = db.transaction((items) => {
|
|
263
|
+
for (const item of items) {
|
|
264
|
+
const uniqueId = item.uniqueId || item.unique_id;
|
|
265
|
+
if (!uniqueId) continue;
|
|
266
|
+
insertUserStmt.run(uniqueId);
|
|
267
|
+
addJobToDb({ ...item, uniqueId });
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
const importVideosTxn = db.transaction((items) => {
|
|
272
|
+
for (const item of items) {
|
|
273
|
+
if (!item?.id) continue;
|
|
274
|
+
insertVideoStmt.run(
|
|
275
|
+
item.id,
|
|
276
|
+
item.href || null,
|
|
277
|
+
item.authorUniqueId || item.author_unique_id || null,
|
|
278
|
+
item.locationCreated || item.location_created || null,
|
|
279
|
+
item.ttSeller ? 1 : 0,
|
|
280
|
+
item.registeredAt || item.registered_at || Date.now(),
|
|
281
|
+
item.userUpdateCount || item.user_update_count || 0,
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
importUsersTxn(legacyUsers);
|
|
287
|
+
importVideosTxn(legacyVideos);
|
|
288
|
+
|
|
289
|
+
const afterUsers = db.prepare("SELECT COUNT(*) as c FROM users").get().c;
|
|
290
|
+
const afterJobs = db.prepare("SELECT COUNT(*) as c FROM jobs").get().c;
|
|
291
|
+
const afterVideos = db.prepare("SELECT COUNT(*) as c FROM videos").get().c;
|
|
292
|
+
|
|
293
|
+
return {
|
|
294
|
+
dbPath,
|
|
295
|
+
usersImported: afterUsers - beforeUsers,
|
|
296
|
+
jobsImported: afterJobs - beforeJobs,
|
|
297
|
+
videosImported: afterVideos - beforeVideos,
|
|
298
|
+
totalUsers: afterUsers,
|
|
299
|
+
totalJobs: afterJobs,
|
|
300
|
+
totalVideos: afterVideos,
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export function closeStoreDb() {
|
|
305
|
+
resetDbConnection();
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function hasUserInDb(uid) {
|
|
309
|
+
if (!db) return false;
|
|
310
|
+
const row = db.prepare("SELECT 1 FROM users WHERE unique_id = ?").get(uid);
|
|
311
|
+
return !!row;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function addUserToDb(user) {
|
|
315
|
+
if (!db) return;
|
|
316
|
+
db.prepare(
|
|
317
|
+
`
|
|
318
|
+
INSERT OR IGNORE INTO users (unique_id, tt_seller, verified, location_created, created_at, updated_at)
|
|
319
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
320
|
+
`,
|
|
321
|
+
).run(
|
|
322
|
+
user.uniqueId,
|
|
323
|
+
user.ttSeller === undefined ||
|
|
324
|
+
user.ttSeller === null ||
|
|
325
|
+
user.ttSeller === ""
|
|
326
|
+
? null
|
|
327
|
+
: user.ttSeller
|
|
328
|
+
? 1
|
|
329
|
+
: 0,
|
|
330
|
+
user.verified === undefined ||
|
|
331
|
+
user.verified === null ||
|
|
332
|
+
user.verified === ""
|
|
333
|
+
? null
|
|
334
|
+
: user.verified
|
|
335
|
+
? 1
|
|
336
|
+
: 0,
|
|
337
|
+
user.locationCreated || null,
|
|
338
|
+
new Date().toISOString(),
|
|
339
|
+
new Date().toISOString(),
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function addJobToDb(user) {
|
|
344
|
+
if (!db) return;
|
|
345
|
+
const now = Date.now();
|
|
346
|
+
db.prepare(
|
|
347
|
+
`
|
|
348
|
+
INSERT OR IGNORE INTO jobs (
|
|
349
|
+
unique_id,
|
|
350
|
+
nickname,
|
|
351
|
+
status,
|
|
352
|
+
sources,
|
|
353
|
+
claimed_by,
|
|
354
|
+
claimed_at,
|
|
355
|
+
error,
|
|
356
|
+
pinned,
|
|
357
|
+
no_video,
|
|
358
|
+
restricted,
|
|
359
|
+
user_update_count,
|
|
360
|
+
tt_seller,
|
|
361
|
+
verified,
|
|
362
|
+
video_count,
|
|
363
|
+
comment_count,
|
|
364
|
+
guessed_location,
|
|
365
|
+
location_created,
|
|
366
|
+
follower_count,
|
|
367
|
+
following_count,
|
|
368
|
+
heart_count,
|
|
369
|
+
refresh_time,
|
|
370
|
+
processed,
|
|
371
|
+
processed_at,
|
|
372
|
+
created_at,
|
|
373
|
+
updated_at,
|
|
374
|
+
region,
|
|
375
|
+
signature,
|
|
376
|
+
sec_uid
|
|
377
|
+
)
|
|
378
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
379
|
+
`,
|
|
380
|
+
).run(
|
|
381
|
+
user.uniqueId,
|
|
382
|
+
user.nickname || null,
|
|
383
|
+
user.status || inferStatus(user),
|
|
384
|
+
JSON.stringify(
|
|
385
|
+
Array.isArray(user.sources) ? [...new Set(user.sources)] : [],
|
|
386
|
+
),
|
|
387
|
+
user.claimedBy || null,
|
|
388
|
+
user.claimedAt || null,
|
|
389
|
+
user.error || null,
|
|
390
|
+
user.pinned ? 1 : 0,
|
|
391
|
+
user.noVideo ? 1 : 0,
|
|
392
|
+
user.restricted ? 1 : 0,
|
|
393
|
+
user.userUpdateCount || 0,
|
|
394
|
+
user.ttSeller === undefined ||
|
|
395
|
+
user.ttSeller === null ||
|
|
396
|
+
user.ttSeller === ""
|
|
397
|
+
? null
|
|
398
|
+
: user.ttSeller
|
|
399
|
+
? 1
|
|
400
|
+
: 0,
|
|
401
|
+
user.verified === undefined ||
|
|
402
|
+
user.verified === null ||
|
|
403
|
+
user.verified === ""
|
|
404
|
+
? null
|
|
405
|
+
: user.verified
|
|
406
|
+
? 1
|
|
407
|
+
: 0,
|
|
408
|
+
user.videoCount || 0,
|
|
409
|
+
user.commentCount || 0,
|
|
410
|
+
user.guessedLocation || null,
|
|
411
|
+
user.locationCreated || null,
|
|
412
|
+
user.followerCount || 0,
|
|
413
|
+
user.followingCount || 0,
|
|
414
|
+
user.heartCount || 0,
|
|
415
|
+
user.refreshTime || null,
|
|
416
|
+
user.processed ? 1 : 0,
|
|
417
|
+
user.processedAt || null,
|
|
418
|
+
user.createdAt || now,
|
|
419
|
+
user.updatedAt || now,
|
|
420
|
+
user.region || null,
|
|
421
|
+
user.signature || null,
|
|
422
|
+
user.secUid || null,
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function getUserDbCount() {
|
|
427
|
+
if (!db) return 0;
|
|
428
|
+
return db.prepare("SELECT COUNT(*) as c FROM users").get().c;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function getJobsCount() {
|
|
432
|
+
if (!db) return 0;
|
|
433
|
+
return db.prepare("SELECT COUNT(*) as c FROM jobs").get().c;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function getPendingJobsCount() {
|
|
437
|
+
if (!db) return 0;
|
|
438
|
+
return db
|
|
439
|
+
.prepare("SELECT COUNT(*) as c FROM jobs WHERE status = 'pending'")
|
|
440
|
+
.get().c;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function getPendingJobsUserUpdateCount() {
|
|
444
|
+
if (!db) return 0;
|
|
445
|
+
return db
|
|
446
|
+
.prepare(
|
|
447
|
+
`
|
|
448
|
+
SELECT COUNT(*) as c
|
|
449
|
+
FROM jobs
|
|
450
|
+
WHERE COALESCE(tt_seller, '') = ''
|
|
451
|
+
AND COALESCE(user_update_count, 0) <= 0
|
|
452
|
+
`,
|
|
453
|
+
)
|
|
454
|
+
.get().c;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function getDashboardStatsFromDb(targetLocations = []) {
|
|
458
|
+
if (!db) return null;
|
|
459
|
+
|
|
460
|
+
const total = getJobsCount();
|
|
461
|
+
const statusCounts = {
|
|
462
|
+
pending: 0,
|
|
463
|
+
processing: 0,
|
|
464
|
+
done: 0,
|
|
465
|
+
error: 0,
|
|
466
|
+
restricted: 0,
|
|
467
|
+
};
|
|
468
|
+
const rows = db
|
|
469
|
+
.prepare(
|
|
470
|
+
`
|
|
471
|
+
SELECT status, COUNT(*) as count
|
|
472
|
+
FROM jobs
|
|
473
|
+
GROUP BY status
|
|
474
|
+
`,
|
|
475
|
+
)
|
|
476
|
+
.all();
|
|
477
|
+
for (const row of rows) {
|
|
478
|
+
if (row.status && statusCounts[row.status] !== undefined) {
|
|
479
|
+
statusCounts[row.status] = row.count;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const targetPlaceholders = targetLocations.map(() => "?").join(", ");
|
|
484
|
+
const targetUsers = targetLocations.length
|
|
485
|
+
? db
|
|
486
|
+
.prepare(
|
|
487
|
+
`
|
|
488
|
+
SELECT COUNT(*) as c
|
|
489
|
+
FROM jobs
|
|
490
|
+
WHERE tt_seller = 1
|
|
491
|
+
AND verified = 0
|
|
492
|
+
AND location_created IN (${targetPlaceholders})
|
|
493
|
+
`,
|
|
494
|
+
)
|
|
495
|
+
.get(...targetLocations).c
|
|
496
|
+
: 0;
|
|
497
|
+
|
|
498
|
+
const countryStats = db
|
|
499
|
+
.prepare(
|
|
500
|
+
`
|
|
501
|
+
SELECT
|
|
502
|
+
COALESCE(location_created, '未知') as country,
|
|
503
|
+
COUNT(*) as count,
|
|
504
|
+
SUM(CASE
|
|
505
|
+
WHEN tt_seller = 1 AND verified = 0 ${
|
|
506
|
+
targetLocations.length
|
|
507
|
+
? `AND location_created IN (${targetPlaceholders})`
|
|
508
|
+
: "AND 1 = 0"
|
|
509
|
+
}
|
|
510
|
+
THEN 1 ELSE 0 END) as targetCount
|
|
511
|
+
FROM jobs
|
|
512
|
+
WHERE status = 'done'
|
|
513
|
+
GROUP BY COALESCE(location_created, '未知')
|
|
514
|
+
ORDER BY count DESC
|
|
515
|
+
`,
|
|
516
|
+
)
|
|
517
|
+
.all(...targetLocations);
|
|
518
|
+
|
|
519
|
+
const targetCountryStats = targetLocations.length
|
|
520
|
+
? db
|
|
521
|
+
.prepare(
|
|
522
|
+
`
|
|
523
|
+
SELECT location_created as country, COUNT(*) as count
|
|
524
|
+
FROM jobs
|
|
525
|
+
WHERE tt_seller = 1
|
|
526
|
+
AND verified = 0
|
|
527
|
+
AND location_created IN (${targetPlaceholders})
|
|
528
|
+
GROUP BY location_created
|
|
529
|
+
ORDER BY count DESC
|
|
530
|
+
`,
|
|
531
|
+
)
|
|
532
|
+
.all(...targetLocations)
|
|
533
|
+
: [];
|
|
534
|
+
|
|
535
|
+
const sourceRow = db
|
|
536
|
+
.prepare(
|
|
537
|
+
`
|
|
538
|
+
SELECT
|
|
539
|
+
SUM(CASE WHEN status = 'done' THEN 1 ELSE 0 END) as processed,
|
|
540
|
+
SUM(CASE WHEN status = 'restricted' THEN 1 ELSE 0 END) as restricted,
|
|
541
|
+
SUM(CASE WHEN status = 'error' THEN 1 ELSE 0 END) as error,
|
|
542
|
+
SUM(CASE WHEN no_video = 1 THEN 1 ELSE 0 END) as noVideo,
|
|
543
|
+
SUM(CASE WHEN status != 'done' AND instr(COALESCE(sources, ''), '"video"') > 0 THEN 1 ELSE 0 END) as video,
|
|
544
|
+
SUM(CASE WHEN status != 'done' AND instr(COALESCE(sources, ''), '"comment"') > 0 THEN 1 ELSE 0 END) as comment,
|
|
545
|
+
SUM(CASE WHEN status != 'done' AND instr(COALESCE(sources, ''), '"guess"') > 0 THEN 1 ELSE 0 END) as guess,
|
|
546
|
+
SUM(CASE WHEN status != 'done' AND instr(COALESCE(sources, ''), '"following"') > 0 THEN 1 ELSE 0 END) as following,
|
|
547
|
+
SUM(CASE WHEN status != 'done' AND instr(COALESCE(sources, ''), '"follower"') > 0 THEN 1 ELSE 0 END) as follower,
|
|
548
|
+
SUM(CASE
|
|
549
|
+
WHEN status != 'done'
|
|
550
|
+
AND instr(COALESCE(sources, ''), '"video"') = 0
|
|
551
|
+
AND instr(COALESCE(sources, ''), '"comment"') = 0
|
|
552
|
+
AND instr(COALESCE(sources, ''), '"guess"') = 0
|
|
553
|
+
AND instr(COALESCE(sources, ''), '"following"') = 0
|
|
554
|
+
AND instr(COALESCE(sources, ''), '"follower"') = 0
|
|
555
|
+
THEN 1 ELSE 0 END) as seed
|
|
556
|
+
FROM jobs
|
|
557
|
+
`,
|
|
558
|
+
)
|
|
559
|
+
.get();
|
|
560
|
+
|
|
561
|
+
return {
|
|
562
|
+
totalUsers: total,
|
|
563
|
+
dbTotalUsers: getUserDbCount(),
|
|
564
|
+
jobsTotal: total,
|
|
565
|
+
jobsPending: getPendingJobsCount(),
|
|
566
|
+
processedUsers: statusCounts.done,
|
|
567
|
+
pendingUsers: statusCounts.pending,
|
|
568
|
+
processingUsers: statusCounts.processing,
|
|
569
|
+
restrictedUsers: statusCounts.restricted,
|
|
570
|
+
errorUsers: statusCounts.error,
|
|
571
|
+
targetUsers,
|
|
572
|
+
userUpdateTasks: getPendingJobsUserUpdateCount(),
|
|
573
|
+
targetCountryStats,
|
|
574
|
+
countryStats,
|
|
575
|
+
sourceStats: {
|
|
576
|
+
seed: sourceRow.seed || 0,
|
|
577
|
+
video: sourceRow.video || 0,
|
|
578
|
+
comment: sourceRow.comment || 0,
|
|
579
|
+
guess: sourceRow.guess || 0,
|
|
580
|
+
following: sourceRow.following || 0,
|
|
581
|
+
follower: sourceRow.follower || 0,
|
|
582
|
+
processed: sourceRow.processed || 0,
|
|
583
|
+
restricted: sourceRow.restricted || 0,
|
|
584
|
+
error: sourceRow.error || 0,
|
|
585
|
+
noVideo: sourceRow.noVideo || 0,
|
|
586
|
+
},
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
function getUsersPageFromDb({
|
|
591
|
+
status,
|
|
592
|
+
search,
|
|
593
|
+
location,
|
|
594
|
+
target,
|
|
595
|
+
targetLocation,
|
|
596
|
+
limit,
|
|
597
|
+
offset,
|
|
598
|
+
targetLocations = [],
|
|
599
|
+
}) {
|
|
600
|
+
if (!db) return null;
|
|
601
|
+
|
|
602
|
+
const safeLimit = Math.max(1, Math.min(200, parseInt(limit) || 50));
|
|
603
|
+
const safeOffset = Math.max(0, parseInt(offset) || 0);
|
|
604
|
+
const where = [];
|
|
605
|
+
const args = [];
|
|
606
|
+
|
|
607
|
+
if (status && status !== "all") {
|
|
608
|
+
where.push("status = ?");
|
|
609
|
+
args.push(status);
|
|
610
|
+
}
|
|
611
|
+
if (target === "1") {
|
|
612
|
+
if (targetLocation) {
|
|
613
|
+
where.push("tt_seller = 1 AND verified = 0 AND location_created = ?");
|
|
614
|
+
args.push(targetLocation);
|
|
615
|
+
} else if (targetLocations.length > 0) {
|
|
616
|
+
where.push(
|
|
617
|
+
`tt_seller = 1 AND verified = 0 AND location_created IN (${targetLocations
|
|
618
|
+
.map(() => "?")
|
|
619
|
+
.join(", ")})`,
|
|
620
|
+
);
|
|
621
|
+
args.push(...targetLocations);
|
|
622
|
+
} else {
|
|
623
|
+
where.push("1 = 0");
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
if (search) {
|
|
627
|
+
where.push(
|
|
628
|
+
"(LOWER(unique_id) LIKE ? OR LOWER(COALESCE(nickname, '')) LIKE ?)",
|
|
629
|
+
);
|
|
630
|
+
const pattern = `%${String(search).toLowerCase()}%`;
|
|
631
|
+
args.push(pattern, pattern);
|
|
632
|
+
}
|
|
633
|
+
if (location) {
|
|
634
|
+
where.push("location_created = ?");
|
|
635
|
+
args.push(location);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
const whereSql = where.length ? `WHERE ${where.join(" AND ")}` : "";
|
|
639
|
+
const total = db
|
|
640
|
+
.prepare(`SELECT COUNT(*) as c FROM jobs ${whereSql}`)
|
|
641
|
+
.get(...args).c;
|
|
642
|
+
|
|
643
|
+
const rows = db
|
|
644
|
+
.prepare(
|
|
645
|
+
`
|
|
646
|
+
SELECT *
|
|
647
|
+
FROM jobs
|
|
648
|
+
${whereSql}
|
|
649
|
+
ORDER BY
|
|
650
|
+
pinned DESC,
|
|
651
|
+
CASE status
|
|
652
|
+
WHEN 'processing' THEN 0
|
|
653
|
+
WHEN 'pending' THEN 1
|
|
654
|
+
WHEN 'done' THEN 2
|
|
655
|
+
WHEN 'error' THEN 3
|
|
656
|
+
WHEN 'restricted' THEN 4
|
|
657
|
+
ELSE 9
|
|
658
|
+
END ASC,
|
|
659
|
+
CASE
|
|
660
|
+
WHEN status = 'done' THEN -COALESCE(processed_at, 0)
|
|
661
|
+
ELSE 0
|
|
662
|
+
END ASC,
|
|
663
|
+
CASE
|
|
664
|
+
WHEN status = 'pending' AND tt_seller = 1 AND verified = 0 THEN 0
|
|
665
|
+
ELSE 1
|
|
666
|
+
END ASC,
|
|
667
|
+
CASE
|
|
668
|
+
WHEN status = 'pending' AND UPPER(COALESCE(guessed_location, '')) IN ('PL', 'NL', 'BE', 'AT') THEN 0
|
|
669
|
+
WHEN status = 'pending' AND UPPER(COALESCE(guessed_location, '')) IN ('DE', 'FR', 'IT', 'IE', 'ES') THEN 1
|
|
670
|
+
ELSE 2
|
|
671
|
+
END ASC,
|
|
672
|
+
COALESCE(follower_count, 0) DESC,
|
|
673
|
+
COALESCE(processed_at, 0) DESC,
|
|
674
|
+
unique_id ASC
|
|
675
|
+
LIMIT ? OFFSET ?
|
|
676
|
+
`,
|
|
677
|
+
)
|
|
678
|
+
.all(...args, safeLimit, safeOffset)
|
|
679
|
+
.map(mapJobRow);
|
|
680
|
+
|
|
681
|
+
return {
|
|
682
|
+
total,
|
|
683
|
+
users: rows,
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
function getTargetUsersFromDb(targetLocations = []) {
|
|
688
|
+
if (!db) return null;
|
|
689
|
+
if (!targetLocations.length) {
|
|
690
|
+
return { total: 0, users: [] };
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
const placeholders = targetLocations.map(() => "?").join(", ");
|
|
694
|
+
const rows = db
|
|
695
|
+
.prepare(
|
|
696
|
+
`
|
|
697
|
+
SELECT
|
|
698
|
+
unique_id,
|
|
699
|
+
nickname,
|
|
700
|
+
follower_count,
|
|
701
|
+
tt_seller,
|
|
702
|
+
verified,
|
|
703
|
+
location_created,
|
|
704
|
+
status,
|
|
705
|
+
sources
|
|
706
|
+
FROM jobs
|
|
707
|
+
WHERE tt_seller = 1
|
|
708
|
+
AND verified = 0
|
|
709
|
+
AND location_created IN (${placeholders})
|
|
710
|
+
ORDER BY COALESCE(follower_count, 0) DESC, unique_id ASC
|
|
711
|
+
`,
|
|
712
|
+
)
|
|
713
|
+
.all(...targetLocations)
|
|
714
|
+
.map(mapJobRow);
|
|
715
|
+
|
|
716
|
+
return {
|
|
717
|
+
total: rows.length,
|
|
718
|
+
users: rows,
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
function snakeToCamel(key) {
|
|
723
|
+
return key.replace(/_([a-z])/g, (_, ch) => ch.toUpperCase());
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
function camelToSnake(key) {
|
|
727
|
+
return key.replace(/[A-Z]/g, (ch) => `_${ch.toLowerCase()}`);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
const jobBooleanColumns = new Set([
|
|
731
|
+
"pinned",
|
|
732
|
+
"no_video",
|
|
733
|
+
"restricted",
|
|
734
|
+
"processed",
|
|
735
|
+
"tt_seller",
|
|
736
|
+
"verified",
|
|
737
|
+
]);
|
|
738
|
+
|
|
739
|
+
const videoBooleanColumns = new Set(["tt_seller"]);
|
|
740
|
+
|
|
741
|
+
const writableJobColumns = new Set([
|
|
742
|
+
"nickname",
|
|
743
|
+
"status",
|
|
744
|
+
"sources",
|
|
745
|
+
"claimed_by",
|
|
746
|
+
"claimed_at",
|
|
747
|
+
"error",
|
|
748
|
+
"pinned",
|
|
749
|
+
"no_video",
|
|
750
|
+
"restricted",
|
|
751
|
+
"user_update_count",
|
|
752
|
+
"tt_seller",
|
|
753
|
+
"verified",
|
|
754
|
+
"video_count",
|
|
755
|
+
"comment_count",
|
|
756
|
+
"guessed_location",
|
|
757
|
+
"location_created",
|
|
758
|
+
"follower_count",
|
|
759
|
+
"following_count",
|
|
760
|
+
"heart_count",
|
|
761
|
+
"refresh_time",
|
|
762
|
+
"processed",
|
|
763
|
+
"processed_at",
|
|
764
|
+
"updated_at",
|
|
765
|
+
"region",
|
|
766
|
+
"signature",
|
|
767
|
+
"sec_uid",
|
|
768
|
+
]);
|
|
769
|
+
|
|
770
|
+
function normalizeJobValue(column, value) {
|
|
771
|
+
if (column === "sources") {
|
|
772
|
+
if (!Array.isArray(value)) return JSON.stringify([]);
|
|
773
|
+
return JSON.stringify([...new Set(value)]);
|
|
774
|
+
}
|
|
775
|
+
if (jobBooleanColumns.has(column)) {
|
|
776
|
+
if (value === undefined || value === null || value === "") return null;
|
|
777
|
+
return value ? 1 : 0;
|
|
778
|
+
}
|
|
779
|
+
return value;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
function mapJobRow(row) {
|
|
783
|
+
if (!row) return undefined;
|
|
784
|
+
const mapped = {};
|
|
785
|
+
for (const [key, value] of Object.entries(row)) {
|
|
786
|
+
const camelKey = snakeToCamel(key);
|
|
787
|
+
if (key === "sources") {
|
|
788
|
+
try {
|
|
789
|
+
mapped[camelKey] = value ? JSON.parse(value) : [];
|
|
790
|
+
} catch {
|
|
791
|
+
mapped[camelKey] = [];
|
|
792
|
+
}
|
|
793
|
+
continue;
|
|
794
|
+
}
|
|
795
|
+
if (jobBooleanColumns.has(key)) {
|
|
796
|
+
mapped[camelKey] = value === null || value === undefined ? null : !!value;
|
|
797
|
+
continue;
|
|
798
|
+
}
|
|
799
|
+
mapped[camelKey] = value;
|
|
800
|
+
}
|
|
801
|
+
return mapped;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
function getJobRow(uniqueId) {
|
|
805
|
+
if (!db) return null;
|
|
806
|
+
return db.prepare("SELECT * FROM jobs WHERE unique_id = ?").get(uniqueId);
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
function getJob(uniqueId) {
|
|
810
|
+
return mapJobRow(getJobRow(uniqueId));
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
function getAllJobs() {
|
|
814
|
+
if (!db) return [];
|
|
815
|
+
return db.prepare("SELECT * FROM jobs").all().map(mapJobRow);
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
function mapVideoRow(row) {
|
|
819
|
+
if (!row) return undefined;
|
|
820
|
+
const mapped = {};
|
|
821
|
+
for (const [key, value] of Object.entries(row)) {
|
|
822
|
+
const camelKey = snakeToCamel(key);
|
|
823
|
+
if (videoBooleanColumns.has(key)) {
|
|
824
|
+
mapped[camelKey] = value === null || value === undefined ? null : !!value;
|
|
825
|
+
continue;
|
|
826
|
+
}
|
|
827
|
+
mapped[camelKey] = value;
|
|
828
|
+
}
|
|
829
|
+
return mapped;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
function getVideoRow(videoId) {
|
|
833
|
+
if (!db) return null;
|
|
834
|
+
return db.prepare("SELECT * FROM videos WHERE id = ?").get(videoId);
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
function getAllVideoRows() {
|
|
838
|
+
if (!db) return [];
|
|
839
|
+
return db.prepare("SELECT * FROM videos").all();
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
function updateJobInfo(uniqueId, info, incrementCount = true) {
|
|
843
|
+
if (!db) return { error: "db not initialized" };
|
|
844
|
+
const existing = getJobRow(uniqueId);
|
|
845
|
+
if (!existing) return { error: "user not found" };
|
|
846
|
+
|
|
847
|
+
const nextValues = {};
|
|
848
|
+
for (const [key, value] of Object.entries(info || {})) {
|
|
849
|
+
if (key === "uniqueId" || key === "unique_id") continue;
|
|
850
|
+
if (value === undefined || value === "") continue;
|
|
851
|
+
const column = camelToSnake(key);
|
|
852
|
+
if (!writableJobColumns.has(column)) continue;
|
|
853
|
+
nextValues[column] = normalizeJobValue(column, value);
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
nextValues.updated_at = Date.now();
|
|
857
|
+
if (incrementCount) {
|
|
858
|
+
nextValues.user_update_count = (existing.user_update_count || 0) + 1;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
const columns = Object.keys(nextValues);
|
|
862
|
+
if (columns.length > 0) {
|
|
863
|
+
const sql = `UPDATE jobs SET ${columns.map((column) => `${column} = ?`).join(", ")} WHERE unique_id = ?`;
|
|
864
|
+
db.prepare(sql).run(
|
|
865
|
+
...columns.map((column) => nextValues[column]),
|
|
866
|
+
uniqueId,
|
|
867
|
+
);
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
return {
|
|
871
|
+
ok: true,
|
|
872
|
+
userUpdateCount:
|
|
873
|
+
nextValues.user_update_count ?? existing.user_update_count ?? 0,
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
function inferStatus(u) {
|
|
878
|
+
if (u.restricted) return "restricted";
|
|
879
|
+
if (u.error) return "error";
|
|
880
|
+
if (u.processed) return "done";
|
|
881
|
+
return "pending";
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
function addJob(user) {
|
|
885
|
+
if (!db) {
|
|
886
|
+
addUserToDb(user);
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
if (!user.status) user.status = inferStatus(user);
|
|
890
|
+
if (!user.createdAt) user.createdAt = Date.now();
|
|
891
|
+
if (!user.updatedAt) user.updatedAt = user.createdAt;
|
|
892
|
+
const writeTxn = db.transaction((job) => {
|
|
893
|
+
addUserToDb(job);
|
|
894
|
+
addJobToDb(job);
|
|
895
|
+
});
|
|
896
|
+
writeTxn(user);
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
export function createStore(filePath) {
|
|
900
|
+
if (!filePath) {
|
|
901
|
+
throw new Error("createStore requires an explicit .db path");
|
|
902
|
+
}
|
|
903
|
+
let data = [];
|
|
904
|
+
// uniqueId → index 内存索引,O(1) 查找
|
|
905
|
+
let uidIndex = new Map();
|
|
906
|
+
let clientErrors = new Map();
|
|
907
|
+
if (filePath) {
|
|
908
|
+
// 初始化 SQLite 用户表(用于判重)
|
|
909
|
+
initUserDb(filePath);
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// stats 缓存
|
|
913
|
+
let statsCache = null;
|
|
914
|
+
let statsDirty = true;
|
|
915
|
+
|
|
916
|
+
function markStatsDirty() {
|
|
917
|
+
statsDirty = true;
|
|
918
|
+
groupsDirty = true;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
function computeStatsInternal() {
|
|
922
|
+
if (db) {
|
|
923
|
+
const total = getJobsCount();
|
|
924
|
+
const statusCounts = {
|
|
925
|
+
pending: 0,
|
|
926
|
+
processing: 0,
|
|
927
|
+
done: 0,
|
|
928
|
+
error: 0,
|
|
929
|
+
restricted: 0,
|
|
930
|
+
};
|
|
931
|
+
const rows = db
|
|
932
|
+
.prepare(
|
|
933
|
+
`
|
|
934
|
+
SELECT status, COUNT(*) as count
|
|
935
|
+
FROM jobs
|
|
936
|
+
GROUP BY status
|
|
937
|
+
`,
|
|
938
|
+
)
|
|
939
|
+
.all();
|
|
940
|
+
for (const row of rows) {
|
|
941
|
+
if (row.status && statusCounts[row.status] !== undefined) {
|
|
942
|
+
statusCounts[row.status] = row.count;
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
statsCache = { total, statusCounts };
|
|
946
|
+
statsDirty = false;
|
|
947
|
+
return statsCache;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
const total = data.length;
|
|
951
|
+
const statusCounts = {
|
|
952
|
+
pending: 0,
|
|
953
|
+
processing: 0,
|
|
954
|
+
done: 0,
|
|
955
|
+
error: 0,
|
|
956
|
+
restricted: 0,
|
|
957
|
+
};
|
|
958
|
+
for (const u of data) {
|
|
959
|
+
statusCounts[u.status] = (statusCounts[u.status] || 0) + 1;
|
|
960
|
+
}
|
|
961
|
+
statsCache = { total, statusCounts };
|
|
962
|
+
statsDirty = false;
|
|
963
|
+
return statsCache;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
function getStats() {
|
|
967
|
+
if (statsDirty) {
|
|
968
|
+
return computeStatsInternal();
|
|
969
|
+
}
|
|
970
|
+
return statsCache;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
// 按 status 的分组索引,避免每次请求全量遍历
|
|
974
|
+
let statusGroups = null;
|
|
975
|
+
let groupsDirty = true;
|
|
976
|
+
|
|
977
|
+
const tier1LocSet = new Set(["PL", "NL", "BE"]);
|
|
978
|
+
const tier2LocSet = new Set(["DE", "FR", "IT", "IE", "ES"]);
|
|
979
|
+
function locationTier(u) {
|
|
980
|
+
const loc = (u.guessedLocation || "").toUpperCase();
|
|
981
|
+
if (tier1LocSet.has(loc)) return 0;
|
|
982
|
+
if (tier2LocSet.has(loc)) return 1;
|
|
983
|
+
return 2;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
function sortGroup(key, arr) {
|
|
987
|
+
if (key === "done")
|
|
988
|
+
arr.sort((a, b) => (b.processedAt || 0) - (a.processedAt || 0));
|
|
989
|
+
else if (key === "pending")
|
|
990
|
+
arr.sort((a, b) => {
|
|
991
|
+
const aSeller = a.ttSeller === true && a.verified === false ? 0 : 1;
|
|
992
|
+
const bSeller = b.ttSeller === true && b.verified === false ? 0 : 1;
|
|
993
|
+
if (aSeller !== bSeller) return aSeller - bSeller;
|
|
994
|
+
const la = locationTier(a),
|
|
995
|
+
lb = locationTier(b);
|
|
996
|
+
if (la !== lb) return la - lb;
|
|
997
|
+
return (b.followerCount || 0) - (a.followerCount || 0);
|
|
998
|
+
});
|
|
999
|
+
else arr.sort((a, b) => (b.followerCount || 0) - (a.followerCount || 0));
|
|
1000
|
+
// 置顶冒泡到组首
|
|
1001
|
+
const pinned = arr.filter((u) => u.pinned);
|
|
1002
|
+
const unpinned = arr.filter((u) => !u.pinned);
|
|
1003
|
+
return pinned.concat(unpinned);
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
function rebuildStatusGroups() {
|
|
1007
|
+
if (db) {
|
|
1008
|
+
statusGroups = {
|
|
1009
|
+
pending: [],
|
|
1010
|
+
processing: [],
|
|
1011
|
+
done: [],
|
|
1012
|
+
error: [],
|
|
1013
|
+
restricted: [],
|
|
1014
|
+
};
|
|
1015
|
+
for (const u of getAllJobs()) {
|
|
1016
|
+
const key = u.status || "pending";
|
|
1017
|
+
if (statusGroups[key]) statusGroups[key].push(u);
|
|
1018
|
+
else statusGroups[key] = [u];
|
|
1019
|
+
}
|
|
1020
|
+
for (const key of Object.keys(statusGroups)) {
|
|
1021
|
+
statusGroups[key] = sortGroup(key, statusGroups[key]);
|
|
1022
|
+
}
|
|
1023
|
+
groupsDirty = false;
|
|
1024
|
+
return;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
statusGroups = {
|
|
1028
|
+
pending: [],
|
|
1029
|
+
processing: [],
|
|
1030
|
+
done: [],
|
|
1031
|
+
error: [],
|
|
1032
|
+
restricted: [],
|
|
1033
|
+
};
|
|
1034
|
+
for (const u of data) {
|
|
1035
|
+
const key = u.status || "pending";
|
|
1036
|
+
if (statusGroups[key]) statusGroups[key].push(u);
|
|
1037
|
+
else statusGroups[key] = [u];
|
|
1038
|
+
}
|
|
1039
|
+
// 各组内排序
|
|
1040
|
+
for (const key of Object.keys(statusGroups)) {
|
|
1041
|
+
statusGroups[key] = sortGroup(key, statusGroups[key]);
|
|
1042
|
+
}
|
|
1043
|
+
groupsDirty = false;
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
function getStatusGroups() {
|
|
1047
|
+
if (groupsDirty) rebuildStatusGroups();
|
|
1048
|
+
return statusGroups;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
function markGroupsDirty() {
|
|
1052
|
+
groupsDirty = true;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
// 视频存储(SQLite 真相源)
|
|
1056
|
+
let videos = [];
|
|
1057
|
+
|
|
1058
|
+
// 构建索引 + 推断 status
|
|
1059
|
+
for (let i = 0; i < data.length; i++) {
|
|
1060
|
+
const u = data[i];
|
|
1061
|
+
if (!u.status) u.status = inferStatus(u);
|
|
1062
|
+
uidIndex.set(u.uniqueId, i);
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
function save() {
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
function flushSave() {
|
|
1070
|
+
return Promise.resolve();
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
function saveVideos() {
|
|
1074
|
+
return;
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
function stopBackup() {
|
|
1078
|
+
return;
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
function getUser(uid) {
|
|
1082
|
+
const idx = uidIndex.get(uid);
|
|
1083
|
+
if (idx !== undefined) return data[idx];
|
|
1084
|
+
if (db) return getJob(uid);
|
|
1085
|
+
return undefined;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
function hasUser(uid) {
|
|
1089
|
+
// 优先用内存索引,兜底用 SQLite
|
|
1090
|
+
if (uidIndex.has(uid)) return true;
|
|
1091
|
+
return hasUserInDb(uid);
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
function userExists(uid) {
|
|
1095
|
+
// 优先用内存索引,兜底用 SQLite
|
|
1096
|
+
if (uidIndex.has(uid)) return true;
|
|
1097
|
+
return hasUserInDb(uid);
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
function addUser(user, append) {
|
|
1101
|
+
const memoryIdx = uidIndex.get(user.uniqueId);
|
|
1102
|
+
if (db && memoryIdx === undefined) {
|
|
1103
|
+
const existingJob = getJobRow(user.uniqueId);
|
|
1104
|
+
if (existingJob) {
|
|
1105
|
+
return updateJobInfo(user.uniqueId, user, false);
|
|
1106
|
+
}
|
|
1107
|
+
addJob(user);
|
|
1108
|
+
return;
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
const existing = getUser(user.uniqueId);
|
|
1112
|
+
if (existing) {
|
|
1113
|
+
let changed = false;
|
|
1114
|
+
for (const key of Object.keys(user)) {
|
|
1115
|
+
if (key === "uniqueId" || key === "sources") continue;
|
|
1116
|
+
if (user[key] !== undefined && user[key] !== null && user[key] !== "") {
|
|
1117
|
+
if (existing[key] !== user[key]) {
|
|
1118
|
+
existing[key] = user[key];
|
|
1119
|
+
changed = true;
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
if (changed) save();
|
|
1124
|
+
} else {
|
|
1125
|
+
if (!user.status) user.status = inferStatus(user);
|
|
1126
|
+
if (user.processed) user.processedAt = user.processedAt || Date.now();
|
|
1127
|
+
if (!user.createdAt) user.createdAt = Date.now();
|
|
1128
|
+
if (append) {
|
|
1129
|
+
const idx = data.length;
|
|
1130
|
+
data.push(user);
|
|
1131
|
+
uidIndex.set(user.uniqueId, idx);
|
|
1132
|
+
} else {
|
|
1133
|
+
data.unshift(user);
|
|
1134
|
+
uidIndex.set(user.uniqueId, 0);
|
|
1135
|
+
}
|
|
1136
|
+
// 同步写入 SQLite
|
|
1137
|
+
addUserToDb(user);
|
|
1138
|
+
markStatsDirty();
|
|
1139
|
+
save();
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
function getPendingUsers() {
|
|
1144
|
+
if (db) {
|
|
1145
|
+
return getAllJobs().filter((u) => u.status === "pending");
|
|
1146
|
+
}
|
|
1147
|
+
return data.filter((u) => u.status === "pending");
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
function getProcessedUsers() {
|
|
1151
|
+
if (db) {
|
|
1152
|
+
return getAllJobs().filter((u) => u.status === "done");
|
|
1153
|
+
}
|
|
1154
|
+
return data.filter((u) => u.status === "done");
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
function getAllUsers() {
|
|
1158
|
+
if (db) {
|
|
1159
|
+
return getAllJobs();
|
|
1160
|
+
}
|
|
1161
|
+
return data;
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
function claimNextJob(
|
|
1165
|
+
userId,
|
|
1166
|
+
expireMs = 5 * 60 * 1000,
|
|
1167
|
+
locations = null,
|
|
1168
|
+
loggedIn = true,
|
|
1169
|
+
) {
|
|
1170
|
+
if (db) {
|
|
1171
|
+
const now = Date.now();
|
|
1172
|
+
const ongoingRow = db
|
|
1173
|
+
.prepare(
|
|
1174
|
+
`
|
|
1175
|
+
SELECT *
|
|
1176
|
+
FROM jobs
|
|
1177
|
+
WHERE status = 'processing'
|
|
1178
|
+
AND claimed_by = ?
|
|
1179
|
+
AND claimed_at IS NOT NULL
|
|
1180
|
+
AND ? - claimed_at < ?
|
|
1181
|
+
ORDER BY claimed_at DESC
|
|
1182
|
+
LIMIT 1
|
|
1183
|
+
`,
|
|
1184
|
+
)
|
|
1185
|
+
.get(userId, now, expireMs);
|
|
1186
|
+
if (ongoingRow) {
|
|
1187
|
+
db.prepare("UPDATE jobs SET claimed_at = ? WHERE unique_id = ?").run(
|
|
1188
|
+
now,
|
|
1189
|
+
ongoingRow.unique_id,
|
|
1190
|
+
);
|
|
1191
|
+
return {
|
|
1192
|
+
uniqueId: ongoingRow.unique_id,
|
|
1193
|
+
nickname: ongoingRow.nickname,
|
|
1194
|
+
claimedAt: now,
|
|
1195
|
+
claimedBy: userId,
|
|
1196
|
+
};
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
const tier1 = new Set(["PL", "NL", "BE"]);
|
|
1200
|
+
const tier2 = new Set(["DE", "FR", "IT", "IE", "ES"]);
|
|
1201
|
+
const normalizedLocations = Array.isArray(locations)
|
|
1202
|
+
? locations
|
|
1203
|
+
.map((loc) => String(loc).trim().toUpperCase())
|
|
1204
|
+
.filter(Boolean)
|
|
1205
|
+
: [];
|
|
1206
|
+
|
|
1207
|
+
function getLocationGroups() {
|
|
1208
|
+
const selected = normalizedLocations.length
|
|
1209
|
+
? normalizedLocations
|
|
1210
|
+
: null;
|
|
1211
|
+
const tier1List = selected
|
|
1212
|
+
? selected.filter((loc) => tier1.has(loc))
|
|
1213
|
+
: [...tier1];
|
|
1214
|
+
const tier2List = selected
|
|
1215
|
+
? selected.filter((loc) => tier2.has(loc))
|
|
1216
|
+
: [...tier2];
|
|
1217
|
+
const otherList = selected
|
|
1218
|
+
? selected.filter((loc) => !tier1.has(loc) && !tier2.has(loc))
|
|
1219
|
+
: null;
|
|
1220
|
+
const groups = [];
|
|
1221
|
+
if (tier1List.length > 0)
|
|
1222
|
+
groups.push({ type: "include", values: tier1List });
|
|
1223
|
+
if (tier2List.length > 0)
|
|
1224
|
+
groups.push({ type: "include", values: tier2List });
|
|
1225
|
+
if (selected) {
|
|
1226
|
+
if (otherList.length > 0)
|
|
1227
|
+
groups.push({ type: "include", values: otherList });
|
|
1228
|
+
} else {
|
|
1229
|
+
groups.push({ type: "exclude", values: [...tier1, ...tier2] });
|
|
1230
|
+
}
|
|
1231
|
+
return groups;
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
const locationGroups = getLocationGroups();
|
|
1235
|
+
|
|
1236
|
+
function applyLocationGroup(where, args, group) {
|
|
1237
|
+
if (!group) return;
|
|
1238
|
+
if (group.type === "include") {
|
|
1239
|
+
where.push(
|
|
1240
|
+
`UPPER(COALESCE(guessed_location, '')) IN (${group.values.map(() => "?").join(", ")})`,
|
|
1241
|
+
);
|
|
1242
|
+
args.push(...group.values);
|
|
1243
|
+
return;
|
|
1244
|
+
}
|
|
1245
|
+
where.push(
|
|
1246
|
+
`UPPER(COALESCE(guessed_location, '')) NOT IN (${group.values.map(() => "?").join(", ")})`,
|
|
1247
|
+
);
|
|
1248
|
+
args.push(...group.values);
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
function queryPendingOne({ requireVideo, group, filters = [] }) {
|
|
1252
|
+
const where = ["status = 'pending'"];
|
|
1253
|
+
const args = [];
|
|
1254
|
+
if (!loggedIn) {
|
|
1255
|
+
where.push("COALESCE(tt_seller, 0) != 1");
|
|
1256
|
+
}
|
|
1257
|
+
if (requireVideo) {
|
|
1258
|
+
where.push("COALESCE(video_count, 0) > 0");
|
|
1259
|
+
}
|
|
1260
|
+
applyLocationGroup(where, args, group);
|
|
1261
|
+
for (const filter of filters) {
|
|
1262
|
+
where.push(filter);
|
|
1263
|
+
}
|
|
1264
|
+
return db
|
|
1265
|
+
.prepare(
|
|
1266
|
+
`
|
|
1267
|
+
SELECT *
|
|
1268
|
+
FROM jobs
|
|
1269
|
+
WHERE ${where.join(" AND ")}
|
|
1270
|
+
ORDER BY follower_count DESC, created_at ASC, unique_id ASC
|
|
1271
|
+
LIMIT 1
|
|
1272
|
+
`,
|
|
1273
|
+
)
|
|
1274
|
+
.get(...args);
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
function queryPendingByGroup({ requireVideo, group, filters = [] }) {
|
|
1278
|
+
if (group?.type === "include" && group.values.length > 1) {
|
|
1279
|
+
for (const location of group.values) {
|
|
1280
|
+
const row = queryPendingOne({
|
|
1281
|
+
requireVideo,
|
|
1282
|
+
group: { type: "include", values: [location] },
|
|
1283
|
+
filters,
|
|
1284
|
+
});
|
|
1285
|
+
if (row) return row;
|
|
1286
|
+
}
|
|
1287
|
+
return null;
|
|
1288
|
+
}
|
|
1289
|
+
return queryPendingOne({ requireVideo, group, filters });
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
function findPinnedPending(requireVideo) {
|
|
1293
|
+
const where = ["status = 'pending'", "COALESCE(pinned, 0) = 1"];
|
|
1294
|
+
const args = [];
|
|
1295
|
+
if (!loggedIn) {
|
|
1296
|
+
where.push("COALESCE(tt_seller, 0) != 1");
|
|
1297
|
+
}
|
|
1298
|
+
if (requireVideo) {
|
|
1299
|
+
where.push("COALESCE(video_count, 0) > 0");
|
|
1300
|
+
}
|
|
1301
|
+
if (normalizedLocations.length > 0) {
|
|
1302
|
+
where.push(
|
|
1303
|
+
`UPPER(COALESCE(guessed_location, '')) IN (${normalizedLocations.map(() => "?").join(", ")})`,
|
|
1304
|
+
);
|
|
1305
|
+
args.push(...normalizedLocations);
|
|
1306
|
+
}
|
|
1307
|
+
return db
|
|
1308
|
+
.prepare(
|
|
1309
|
+
`
|
|
1310
|
+
SELECT *
|
|
1311
|
+
FROM jobs
|
|
1312
|
+
WHERE ${where.join(" AND ")}
|
|
1313
|
+
ORDER BY created_at ASC, unique_id ASC
|
|
1314
|
+
LIMIT 1
|
|
1315
|
+
`,
|
|
1316
|
+
)
|
|
1317
|
+
.get(...args);
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
function findPrioritizedPending(requireVideo) {
|
|
1321
|
+
for (const group of locationGroups) {
|
|
1322
|
+
const seed = queryPendingByGroup({
|
|
1323
|
+
requireVideo,
|
|
1324
|
+
group,
|
|
1325
|
+
filters: [
|
|
1326
|
+
"COALESCE(pinned, 0) = 0",
|
|
1327
|
+
`instr(COALESCE(sources, ''), '"seed"') > 0`,
|
|
1328
|
+
],
|
|
1329
|
+
});
|
|
1330
|
+
if (seed) return seed;
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
if (loggedIn) {
|
|
1334
|
+
for (const group of locationGroups) {
|
|
1335
|
+
const seller = queryPendingByGroup({
|
|
1336
|
+
requireVideo,
|
|
1337
|
+
group,
|
|
1338
|
+
filters: [
|
|
1339
|
+
"COALESCE(pinned, 0) = 0",
|
|
1340
|
+
"tt_seller = 1",
|
|
1341
|
+
"verified = 0",
|
|
1342
|
+
],
|
|
1343
|
+
});
|
|
1344
|
+
if (seller) return seller;
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
for (const group of locationGroups) {
|
|
1349
|
+
const follow = queryPendingByGroup({
|
|
1350
|
+
requireVideo,
|
|
1351
|
+
group,
|
|
1352
|
+
filters: [
|
|
1353
|
+
"COALESCE(pinned, 0) = 0",
|
|
1354
|
+
`(
|
|
1355
|
+
instr(COALESCE(sources, ''), '"following"') > 0
|
|
1356
|
+
OR instr(COALESCE(sources, ''), '"follower"') > 0
|
|
1357
|
+
)`,
|
|
1358
|
+
],
|
|
1359
|
+
});
|
|
1360
|
+
if (follow) return follow;
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
for (const group of locationGroups) {
|
|
1364
|
+
const other = queryPendingByGroup({
|
|
1365
|
+
requireVideo,
|
|
1366
|
+
group,
|
|
1367
|
+
filters: ["COALESCE(pinned, 0) = 0"],
|
|
1368
|
+
});
|
|
1369
|
+
if (other) return other;
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
return null;
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
function claimRow(row) {
|
|
1376
|
+
if (!row) return null;
|
|
1377
|
+
db.prepare(
|
|
1378
|
+
"UPDATE jobs SET status = 'processing', claimed_at = ?, claimed_by = ? WHERE unique_id = ?",
|
|
1379
|
+
).run(now, userId, row.unique_id);
|
|
1380
|
+
markStatsDirty();
|
|
1381
|
+
return {
|
|
1382
|
+
uniqueId: row.unique_id,
|
|
1383
|
+
nickname: row.nickname,
|
|
1384
|
+
claimedAt: now,
|
|
1385
|
+
claimedBy: userId,
|
|
1386
|
+
};
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
const expiredRow = db
|
|
1390
|
+
.prepare(
|
|
1391
|
+
`
|
|
1392
|
+
SELECT *
|
|
1393
|
+
FROM jobs
|
|
1394
|
+
WHERE status = 'processing'
|
|
1395
|
+
AND claimed_at IS NOT NULL
|
|
1396
|
+
AND ? - claimed_at > ?
|
|
1397
|
+
ORDER BY claimed_at ASC
|
|
1398
|
+
LIMIT 1
|
|
1399
|
+
`,
|
|
1400
|
+
)
|
|
1401
|
+
.get(now, expireMs);
|
|
1402
|
+
let expiredCandidate = null;
|
|
1403
|
+
if (expiredRow) {
|
|
1404
|
+
db.prepare(
|
|
1405
|
+
"UPDATE jobs SET status = 'pending', claimed_at = NULL WHERE unique_id = ?",
|
|
1406
|
+
).run(expiredRow.unique_id);
|
|
1407
|
+
expiredCandidate = mapJobRow({
|
|
1408
|
+
...expiredRow,
|
|
1409
|
+
status: "pending",
|
|
1410
|
+
claimed_at: null,
|
|
1411
|
+
});
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
for (const requireVideo of [true, false]) {
|
|
1415
|
+
const pinned = findPinnedPending(requireVideo);
|
|
1416
|
+
if (pinned) {
|
|
1417
|
+
return claimRow(pinned);
|
|
1418
|
+
}
|
|
1419
|
+
if (expiredCandidate) {
|
|
1420
|
+
return claimRow({
|
|
1421
|
+
unique_id: expiredCandidate.uniqueId,
|
|
1422
|
+
nickname: expiredCandidate.nickname,
|
|
1423
|
+
});
|
|
1424
|
+
}
|
|
1425
|
+
const ranked = findPrioritizedPending(requireVideo);
|
|
1426
|
+
if (ranked) {
|
|
1427
|
+
return claimRow(ranked);
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
return null;
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
const now = Date.now();
|
|
1434
|
+
|
|
1435
|
+
// 0. 该客户端有未过期的任务,续期返回
|
|
1436
|
+
const ongoing = data.find(
|
|
1437
|
+
(u) =>
|
|
1438
|
+
u.status === "processing" &&
|
|
1439
|
+
u.claimedBy === userId &&
|
|
1440
|
+
u.claimedAt &&
|
|
1441
|
+
now - u.claimedAt < expireMs,
|
|
1442
|
+
);
|
|
1443
|
+
if (ongoing) {
|
|
1444
|
+
ongoing.claimedAt = now;
|
|
1445
|
+
save();
|
|
1446
|
+
return {
|
|
1447
|
+
uniqueId: ongoing.uniqueId,
|
|
1448
|
+
nickname: ongoing.nickname,
|
|
1449
|
+
claimedAt: ongoing.claimedAt,
|
|
1450
|
+
claimedBy: userId,
|
|
1451
|
+
};
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
// 按猜测国家梯队排序
|
|
1455
|
+
const tier1 = new Set(["PL", "NL", "BE"]);
|
|
1456
|
+
const tier2 = new Set(["DE", "FR", "IT", "IE", "ES"]);
|
|
1457
|
+
function locationTier(u) {
|
|
1458
|
+
const loc = (u.guessedLocation || "").toUpperCase();
|
|
1459
|
+
if (tier1.has(loc)) return 0;
|
|
1460
|
+
if (tier2.has(loc)) return 1;
|
|
1461
|
+
return 2;
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
// 国家过滤:如果指定了 locations,只保留 guessedLocation 在列表中的用户
|
|
1465
|
+
function locationFilter(u) {
|
|
1466
|
+
if (!locations || locations.length === 0) return true;
|
|
1467
|
+
const loc = (u.guessedLocation || "").toUpperCase();
|
|
1468
|
+
return locations.includes(loc);
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
// 从候选列表中按优先级取第一个:pinned > 超时回收 > seed > ttSeller(仅登录) > follow > other
|
|
1472
|
+
function pickCandidate(candidates) {
|
|
1473
|
+
let next = candidates.find((u) => u.pinned);
|
|
1474
|
+
|
|
1475
|
+
if (!next) {
|
|
1476
|
+
const expired = data.find(
|
|
1477
|
+
(u) =>
|
|
1478
|
+
u.status === "processing" &&
|
|
1479
|
+
u.claimedAt &&
|
|
1480
|
+
now - u.claimedAt > expireMs,
|
|
1481
|
+
);
|
|
1482
|
+
if (expired) {
|
|
1483
|
+
expired.status = "pending";
|
|
1484
|
+
markStatsDirty();
|
|
1485
|
+
delete expired.claimedAt;
|
|
1486
|
+
next = expired;
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
if (!next) {
|
|
1491
|
+
const seed = candidates.filter(
|
|
1492
|
+
(u) => u.sources && u.sources.includes("seed"),
|
|
1493
|
+
);
|
|
1494
|
+
seed.sort((a, b) => locationTier(a) - locationTier(b));
|
|
1495
|
+
next = seed[0] || null;
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
// 未登录时跳过 ttSeller 优先级
|
|
1499
|
+
if (!next && loggedIn) {
|
|
1500
|
+
const ttSeller = candidates.filter(
|
|
1501
|
+
(u) => u.ttSeller === true && u.verified === false,
|
|
1502
|
+
);
|
|
1503
|
+
ttSeller.sort((a, b) => locationTier(a) - locationTier(b));
|
|
1504
|
+
next = ttSeller[0] || null;
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
if (!next) {
|
|
1508
|
+
const follow = candidates.filter(
|
|
1509
|
+
(u) =>
|
|
1510
|
+
u.sources &&
|
|
1511
|
+
(u.sources.includes("following") || u.sources.includes("follower")),
|
|
1512
|
+
);
|
|
1513
|
+
follow.sort((a, b) => locationTier(a) - locationTier(b));
|
|
1514
|
+
next = follow[0] || null;
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
if (!next) {
|
|
1518
|
+
candidates.sort((a, b) => locationTier(a) - locationTier(b));
|
|
1519
|
+
next = candidates[0] || null;
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
return next;
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
// 先在有视频的 pending 用户中找;找不到再用全部 pending 用户兜底
|
|
1526
|
+
let pending = data.filter((u) => u.status === "pending");
|
|
1527
|
+
// 应用国家过滤
|
|
1528
|
+
if (locations && locations.length > 0) {
|
|
1529
|
+
pending = pending.filter(locationFilter);
|
|
1530
|
+
}
|
|
1531
|
+
// 未登录客户端不能领取 ttSeller 用户
|
|
1532
|
+
if (!loggedIn) {
|
|
1533
|
+
pending = pending.filter((u) => u.ttSeller !== true);
|
|
1534
|
+
}
|
|
1535
|
+
let hasVideo = pending.filter((u) => u.videoCount > 0);
|
|
1536
|
+
const next = pickCandidate(hasVideo) || pickCandidate(pending);
|
|
1537
|
+
|
|
1538
|
+
if (next) {
|
|
1539
|
+
next.status = "processing";
|
|
1540
|
+
markStatsDirty();
|
|
1541
|
+
next.claimedAt = now;
|
|
1542
|
+
next.claimedBy = userId;
|
|
1543
|
+
save();
|
|
1544
|
+
return {
|
|
1545
|
+
uniqueId: next.uniqueId,
|
|
1546
|
+
nickname: next.nickname,
|
|
1547
|
+
claimedAt: next.claimedAt,
|
|
1548
|
+
claimedBy: userId,
|
|
1549
|
+
};
|
|
1550
|
+
}
|
|
1551
|
+
return null;
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
function processDiscoveredUsers(result) {
|
|
1555
|
+
const guessedLocation = result.guessedLocation || null;
|
|
1556
|
+
const discovered = [
|
|
1557
|
+
...(result.discoveredVideoAuthors || []).map((v) => ({
|
|
1558
|
+
uniqueId:
|
|
1559
|
+
typeof v === "string"
|
|
1560
|
+
? v.replace(/^@/, "")
|
|
1561
|
+
: v.uniqueId?.replace(/^@/, "") || "",
|
|
1562
|
+
nickname: typeof v === "string" ? null : v.nickname || null,
|
|
1563
|
+
locationCreated:
|
|
1564
|
+
typeof v === "string" ? null : v.locationCreated || null,
|
|
1565
|
+
guessedLocation:
|
|
1566
|
+
typeof v === "string"
|
|
1567
|
+
? guessedLocation
|
|
1568
|
+
: v.guessedLocation || guessedLocation,
|
|
1569
|
+
sources: ["video"],
|
|
1570
|
+
})),
|
|
1571
|
+
...(result.discoveredCommentAuthors || []).map((c) => {
|
|
1572
|
+
if (typeof c === "string")
|
|
1573
|
+
return {
|
|
1574
|
+
uniqueId: c.replace(/^@/, ""),
|
|
1575
|
+
sources: ["comment"],
|
|
1576
|
+
guessedLocation,
|
|
1577
|
+
};
|
|
1578
|
+
return {
|
|
1579
|
+
uniqueId: (c.author || c.uniqueId || "").replace(/^@/, ""),
|
|
1580
|
+
nickname: c.nickname || null,
|
|
1581
|
+
sources: ["comment"],
|
|
1582
|
+
guessedLocation: c.guessedLocation || guessedLocation,
|
|
1583
|
+
};
|
|
1584
|
+
}),
|
|
1585
|
+
...(result.discoveredGuessAuthors || []).map((g) => {
|
|
1586
|
+
if (typeof g === "string")
|
|
1587
|
+
return {
|
|
1588
|
+
uniqueId: g.replace(/^@/, ""),
|
|
1589
|
+
sources: ["guess"],
|
|
1590
|
+
guessedLocation,
|
|
1591
|
+
};
|
|
1592
|
+
return {
|
|
1593
|
+
uniqueId: (g.author || g.uniqueId || "").replace(/^@/, ""),
|
|
1594
|
+
nickname: g.nickname || null,
|
|
1595
|
+
sources: ["guess"],
|
|
1596
|
+
guessedLocation: g.guessedLocation || guessedLocation,
|
|
1597
|
+
};
|
|
1598
|
+
}),
|
|
1599
|
+
...(result.discoveredFollowing || []).map((f) => {
|
|
1600
|
+
const handle = Array.isArray(f) ? f[0] : f.handle || "";
|
|
1601
|
+
const name = Array.isArray(f) ? f[1] : f.displayName || null;
|
|
1602
|
+
return {
|
|
1603
|
+
uniqueId: handle.replace(/^@/, ""),
|
|
1604
|
+
nickname: name,
|
|
1605
|
+
sources: ["following"],
|
|
1606
|
+
guessedLocation:
|
|
1607
|
+
(typeof f === "object" && f.guessedLocation) || guessedLocation,
|
|
1608
|
+
};
|
|
1609
|
+
}),
|
|
1610
|
+
...(result.discoveredFollowers || []).map((f) => {
|
|
1611
|
+
const handle = Array.isArray(f) ? f[0] : f.handle || "";
|
|
1612
|
+
const name = Array.isArray(f) ? f[1] : f.displayName || null;
|
|
1613
|
+
return {
|
|
1614
|
+
uniqueId: handle.replace(/^@/, ""),
|
|
1615
|
+
nickname: name,
|
|
1616
|
+
sources: ["follower"],
|
|
1617
|
+
guessedLocation:
|
|
1618
|
+
(typeof f === "object" && f.guessedLocation) || guessedLocation,
|
|
1619
|
+
};
|
|
1620
|
+
}),
|
|
1621
|
+
].filter((u) => u.uniqueId);
|
|
1622
|
+
|
|
1623
|
+
// 先对 discovered 内部去重,再用 uidIndex 批量判断
|
|
1624
|
+
const seen = new Set();
|
|
1625
|
+
const unique = [];
|
|
1626
|
+
for (const d of discovered) {
|
|
1627
|
+
if (!seen.has(d.uniqueId)) {
|
|
1628
|
+
seen.add(d.uniqueId);
|
|
1629
|
+
unique.push(d);
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
const newUsers = [];
|
|
1634
|
+
for (const d of unique) {
|
|
1635
|
+
if (!hasUser(d.uniqueId)) {
|
|
1636
|
+
addJob(d);
|
|
1637
|
+
newUsers.push(d.uniqueId);
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
return newUsers;
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
function updateUserFromResult(user, result) {
|
|
1644
|
+
const oldStatus = user.status;
|
|
1645
|
+
if (result.restricted) {
|
|
1646
|
+
user.status = "restricted";
|
|
1647
|
+
if (result.userInfo) {
|
|
1648
|
+
const info = result.userInfo;
|
|
1649
|
+
for (const key of Object.keys(info)) {
|
|
1650
|
+
if (key === "uniqueId" || key === "sources") continue;
|
|
1651
|
+
if (
|
|
1652
|
+
info[key] !== undefined &&
|
|
1653
|
+
info[key] !== null &&
|
|
1654
|
+
info[key] !== ""
|
|
1655
|
+
) {
|
|
1656
|
+
user[key] = info[key];
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
user.processed = true;
|
|
1661
|
+
user.processedAt = Date.now();
|
|
1662
|
+
user.sources = [...new Set([...(user.sources || []), "restricted"])];
|
|
1663
|
+
} else if (result.error) {
|
|
1664
|
+
user.status = "error";
|
|
1665
|
+
user.error = result.error;
|
|
1666
|
+
user.sources = [...new Set([...(user.sources || []), "error"])];
|
|
1667
|
+
} else {
|
|
1668
|
+
user.status = "done";
|
|
1669
|
+
user.processed = true;
|
|
1670
|
+
user.processedAt = Date.now();
|
|
1671
|
+
user.noVideo = result.noVideo || false;
|
|
1672
|
+
user.keepFollow = result.keepFollow || false;
|
|
1673
|
+
user.hasFollowData = result.hasFollowData || false;
|
|
1674
|
+
|
|
1675
|
+
if (result.userInfo) {
|
|
1676
|
+
const info = result.userInfo;
|
|
1677
|
+
for (const key of Object.keys(info)) {
|
|
1678
|
+
if (key === "uniqueId" || key === "sources") continue;
|
|
1679
|
+
if (
|
|
1680
|
+
info[key] !== undefined &&
|
|
1681
|
+
info[key] !== null &&
|
|
1682
|
+
info[key] !== ""
|
|
1683
|
+
) {
|
|
1684
|
+
user[key] = info[key];
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
user.followerCount = result.userInfo?.followerCount ?? user.followerCount;
|
|
1690
|
+
user.videoCount = result.userInfo?.videoCount ?? user.videoCount;
|
|
1691
|
+
user.nickname = result.userInfo?.nickname || user.nickname;
|
|
1692
|
+
user.locationCreated =
|
|
1693
|
+
result.userInfo?.locationCreated || user.locationCreated;
|
|
1694
|
+
user.ttSeller = result.userInfo?.ttSeller ?? user.ttSeller;
|
|
1695
|
+
user.verified = result.userInfo?.verified ?? user.verified;
|
|
1696
|
+
user.region = result.userInfo?.region || user.region;
|
|
1697
|
+
user.signature = result.userInfo?.signature ?? user.signature;
|
|
1698
|
+
user.followingCount =
|
|
1699
|
+
result.userInfo?.followingCount ?? user.followingCount;
|
|
1700
|
+
user.heartCount = result.userInfo?.heartCount ?? user.heartCount;
|
|
1701
|
+
if (result.userInfo?.secUid) user.secUid = result.userInfo.secUid;
|
|
1702
|
+
const extraFields = [
|
|
1703
|
+
"restricted",
|
|
1704
|
+
"error",
|
|
1705
|
+
"userInfo",
|
|
1706
|
+
"discoveredVideoAuthors",
|
|
1707
|
+
"discoveredCommentAuthors",
|
|
1708
|
+
"discoveredGuessAuthors",
|
|
1709
|
+
"discoveredFollowing",
|
|
1710
|
+
"discoveredFollowers",
|
|
1711
|
+
"uniqueId",
|
|
1712
|
+
"sources",
|
|
1713
|
+
];
|
|
1714
|
+
for (const key of Object.keys(result)) {
|
|
1715
|
+
if (extraFields.includes(key)) continue;
|
|
1716
|
+
if (
|
|
1717
|
+
result[key] !== undefined &&
|
|
1718
|
+
result[key] !== null &&
|
|
1719
|
+
result[key] !== ""
|
|
1720
|
+
) {
|
|
1721
|
+
user[key] = result[key];
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
user.sources = [...new Set([...(user.sources || []), "processed"])];
|
|
1725
|
+
}
|
|
1726
|
+
if (user.status !== oldStatus) markStatsDirty();
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
function commitJob(uniqueId, result) {
|
|
1730
|
+
if (db) {
|
|
1731
|
+
const user = getJob(uniqueId);
|
|
1732
|
+
if (!user) return { saved: false, error: "user not found" };
|
|
1733
|
+
|
|
1734
|
+
updateUserFromResult(user, result);
|
|
1735
|
+
user.claimedAt = null;
|
|
1736
|
+
const newUsers = processDiscoveredUsers(result);
|
|
1737
|
+
const persistRet = updateJobInfo(uniqueId, user, false);
|
|
1738
|
+
if (persistRet.error) {
|
|
1739
|
+
return { saved: false, error: persistRet.error };
|
|
1740
|
+
}
|
|
1741
|
+
return { saved: true, status: user.status, newUsers };
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
const user = getUser(uniqueId);
|
|
1745
|
+
if (!user) return { saved: false, error: "user not found" };
|
|
1746
|
+
|
|
1747
|
+
updateUserFromResult(user, result);
|
|
1748
|
+
delete user.claimedAt;
|
|
1749
|
+
const newUsers = processDiscoveredUsers(result);
|
|
1750
|
+
|
|
1751
|
+
save();
|
|
1752
|
+
return { saved: true, status: user.status, newUsers };
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
function commitNewExplore(uniqueId, result) {
|
|
1756
|
+
if (db) {
|
|
1757
|
+
const existing = getJob(uniqueId);
|
|
1758
|
+
if (existing) {
|
|
1759
|
+
updateUserFromResult(existing, result);
|
|
1760
|
+
const persistRet = updateJobInfo(uniqueId, existing, false);
|
|
1761
|
+
if (persistRet.error) {
|
|
1762
|
+
return { saved: false, error: persistRet.error };
|
|
1763
|
+
}
|
|
1764
|
+
const newUsers = processDiscoveredUsers(result);
|
|
1765
|
+
return {
|
|
1766
|
+
saved: true,
|
|
1767
|
+
created: false,
|
|
1768
|
+
status: existing.status,
|
|
1769
|
+
newUsers,
|
|
1770
|
+
};
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
const userObj = {
|
|
1774
|
+
uniqueId,
|
|
1775
|
+
...(result.userInfo || {}),
|
|
1776
|
+
sources: ["refresh-explore"],
|
|
1777
|
+
};
|
|
1778
|
+
updateUserFromResult(userObj, result);
|
|
1779
|
+
addJob(userObj);
|
|
1780
|
+
const newUsers = processDiscoveredUsers(result);
|
|
1781
|
+
return { saved: true, created: true, status: userObj.status, newUsers };
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
const existing = getUser(uniqueId);
|
|
1785
|
+
if (existing) {
|
|
1786
|
+
updateUserFromResult(existing, result);
|
|
1787
|
+
const newUsers = processDiscoveredUsers(result);
|
|
1788
|
+
save();
|
|
1789
|
+
return { saved: true, created: false, status: existing.status, newUsers };
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
const userObj = {
|
|
1793
|
+
uniqueId,
|
|
1794
|
+
...(result.userInfo || {}),
|
|
1795
|
+
sources: ["refresh-explore"],
|
|
1796
|
+
};
|
|
1797
|
+
updateUserFromResult(userObj, result);
|
|
1798
|
+
addUser(userObj, true);
|
|
1799
|
+
const newUsers = processDiscoveredUsers(result);
|
|
1800
|
+
|
|
1801
|
+
save();
|
|
1802
|
+
return { saved: true, created: true, status: userObj.status, newUsers };
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
function resetJob(uniqueId) {
|
|
1806
|
+
if (db) {
|
|
1807
|
+
const user = getJob(uniqueId);
|
|
1808
|
+
if (!user) return { saved: false, error: "user not found" };
|
|
1809
|
+
user.status = "pending";
|
|
1810
|
+
user.claimedAt = null;
|
|
1811
|
+
user.processedAt = null;
|
|
1812
|
+
user.processed = false;
|
|
1813
|
+
user.error = null;
|
|
1814
|
+
user.restricted = false;
|
|
1815
|
+
user.noVideo = false;
|
|
1816
|
+
const ret = updateJobInfo(uniqueId, user, false);
|
|
1817
|
+
if (ret.error) return { saved: false, error: ret.error };
|
|
1818
|
+
markStatsDirty();
|
|
1819
|
+
return { saved: true };
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
const user = getUser(uniqueId);
|
|
1823
|
+
if (!user) return { saved: false, error: "user not found" };
|
|
1824
|
+
user.status = "pending";
|
|
1825
|
+
markStatsDirty();
|
|
1826
|
+
delete user.claimedAt;
|
|
1827
|
+
delete user.processedAt;
|
|
1828
|
+
delete user.processed;
|
|
1829
|
+
delete user.error;
|
|
1830
|
+
delete user.restricted;
|
|
1831
|
+
delete user.noVideo;
|
|
1832
|
+
save();
|
|
1833
|
+
return { saved: true };
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
function togglePin(uniqueId) {
|
|
1837
|
+
if (db) {
|
|
1838
|
+
const user = getJob(uniqueId);
|
|
1839
|
+
if (!user) return { saved: false, error: "user not found" };
|
|
1840
|
+
const nextPinned = !user.pinned;
|
|
1841
|
+
const ret = updateJobInfo(uniqueId, { pinned: nextPinned }, false);
|
|
1842
|
+
if (ret.error) return { saved: false, error: ret.error };
|
|
1843
|
+
return { saved: true, pinned: nextPinned };
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
const user = getUser(uniqueId);
|
|
1847
|
+
if (!user) return { saved: false, error: "user not found" };
|
|
1848
|
+
user.pinned = !user.pinned;
|
|
1849
|
+
save();
|
|
1850
|
+
return { saved: true, pinned: user.pinned };
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
function getNextRedoJob(userId) {
|
|
1854
|
+
if (db) {
|
|
1855
|
+
const now = Date.now();
|
|
1856
|
+
const defaultTime = new Date("2016-01-01T00:00:00Z").getTime();
|
|
1857
|
+
const targetLocations = ["ES", "PL", "NL", "BE", "DE", "FR", "IT", "IE"];
|
|
1858
|
+
const placeholders = targetLocations.map(() => "?").join(",");
|
|
1859
|
+
const row = db
|
|
1860
|
+
.prepare(
|
|
1861
|
+
`
|
|
1862
|
+
SELECT *
|
|
1863
|
+
FROM jobs
|
|
1864
|
+
WHERE tt_seller = 1
|
|
1865
|
+
AND verified = 0
|
|
1866
|
+
AND location_created IN (${placeholders})
|
|
1867
|
+
ORDER BY COALESCE(refresh_time, ?) ASC
|
|
1868
|
+
LIMIT 1
|
|
1869
|
+
`,
|
|
1870
|
+
)
|
|
1871
|
+
.get(...targetLocations, defaultTime);
|
|
1872
|
+
if (!row) return null;
|
|
1873
|
+
db.prepare(
|
|
1874
|
+
"UPDATE jobs SET refresh_time = ?, updated_at = ? WHERE unique_id = ?",
|
|
1875
|
+
).run(now, now, row.unique_id);
|
|
1876
|
+
return {
|
|
1877
|
+
uniqueId: row.unique_id,
|
|
1878
|
+
nickname: row.nickname,
|
|
1879
|
+
refreshTime: now,
|
|
1880
|
+
};
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1883
|
+
const now = Date.now();
|
|
1884
|
+
const defaultTime = new Date("2016-01-01T00:00:00Z").getTime();
|
|
1885
|
+
|
|
1886
|
+
// 筛选目标国家用户,按 refreshTime 升序取最远的(没有则默认 2016-01-01)
|
|
1887
|
+
const targetLocations = ["ES", "PL", "NL", "BE", "DE", "FR", "IT", "IE"];
|
|
1888
|
+
const targetUsers = data.filter(
|
|
1889
|
+
(u) =>
|
|
1890
|
+
u.ttSeller &&
|
|
1891
|
+
u.verified === false &&
|
|
1892
|
+
targetLocations.includes(u.locationCreated),
|
|
1893
|
+
);
|
|
1894
|
+
if (targetUsers.length === 0) return null;
|
|
1895
|
+
|
|
1896
|
+
targetUsers.sort((a, b) => {
|
|
1897
|
+
const ta = a.refreshTime || defaultTime;
|
|
1898
|
+
const tb = b.refreshTime || defaultTime;
|
|
1899
|
+
return ta - tb;
|
|
1900
|
+
});
|
|
1901
|
+
|
|
1902
|
+
const next = targetUsers[0];
|
|
1903
|
+
next.refreshTime = now;
|
|
1904
|
+
save();
|
|
1905
|
+
return {
|
|
1906
|
+
uniqueId: next.uniqueId,
|
|
1907
|
+
nickname: next.nickname,
|
|
1908
|
+
refreshTime: next.refreshTime,
|
|
1909
|
+
};
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1912
|
+
function commitRedoJob(uniqueId, result) {
|
|
1913
|
+
if (db) {
|
|
1914
|
+
const user = getJob(uniqueId);
|
|
1915
|
+
if (!user) return { saved: false, error: "user not found" };
|
|
1916
|
+
user.refreshTime = Date.now();
|
|
1917
|
+
if (result.userInfo) {
|
|
1918
|
+
const info = result.userInfo;
|
|
1919
|
+
for (const key of Object.keys(info)) {
|
|
1920
|
+
if (key === "uniqueId" || key === "sources") continue;
|
|
1921
|
+
if (
|
|
1922
|
+
info[key] !== undefined &&
|
|
1923
|
+
info[key] !== null &&
|
|
1924
|
+
info[key] !== ""
|
|
1925
|
+
) {
|
|
1926
|
+
user[key] = info[key];
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
const ret = updateJobInfo(uniqueId, user, false);
|
|
1931
|
+
if (ret.error) return { saved: false, error: ret.error };
|
|
1932
|
+
return { saved: true };
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
const user = getUser(uniqueId);
|
|
1936
|
+
if (!user) return { saved: false, error: "user not found" };
|
|
1937
|
+
|
|
1938
|
+
user.refreshTime = Date.now();
|
|
1939
|
+
|
|
1940
|
+
if (result.userInfo) {
|
|
1941
|
+
const info = result.userInfo;
|
|
1942
|
+
for (const key of Object.keys(info)) {
|
|
1943
|
+
if (key === "uniqueId" || key === "sources") continue;
|
|
1944
|
+
if (info[key] !== undefined && info[key] !== null && info[key] !== "") {
|
|
1945
|
+
user[key] = info[key];
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
return { saved: true };
|
|
1951
|
+
}
|
|
1952
|
+
|
|
1953
|
+
function reportClientError(
|
|
1954
|
+
userId,
|
|
1955
|
+
errorType,
|
|
1956
|
+
errorMessage,
|
|
1957
|
+
username,
|
|
1958
|
+
stage,
|
|
1959
|
+
errorStack,
|
|
1960
|
+
) {
|
|
1961
|
+
const existing = clientErrors.get(userId);
|
|
1962
|
+
if (existing) {
|
|
1963
|
+
existing.timestamp = Date.now();
|
|
1964
|
+
if (errorType === "captcha") {
|
|
1965
|
+
existing.captchaCount = (existing.captchaCount || 0) + 1;
|
|
1966
|
+
if (!existing.captchaStage) existing.captchaStage = stage || "";
|
|
1967
|
+
if (!existing.captchaMessage)
|
|
1968
|
+
existing.captchaMessage = errorMessage || "";
|
|
1969
|
+
if (!existing.captchaStack) existing.captchaStack = errorStack || "";
|
|
1970
|
+
} else {
|
|
1971
|
+
existing.errorType = errorType;
|
|
1972
|
+
existing.errorMessage = errorMessage || "";
|
|
1973
|
+
existing.errorStack = errorStack || "";
|
|
1974
|
+
existing.stage = stage || "";
|
|
1975
|
+
existing.reportCount = (existing.reportCount || 1) + 1;
|
|
1976
|
+
}
|
|
1977
|
+
if (username) existing.username = username;
|
|
1978
|
+
} else {
|
|
1979
|
+
clientErrors.set(userId, {
|
|
1980
|
+
userId,
|
|
1981
|
+
errorType,
|
|
1982
|
+
errorMessage: errorMessage || "",
|
|
1983
|
+
errorStack: errorStack || "",
|
|
1984
|
+
username,
|
|
1985
|
+
stage: stage || "",
|
|
1986
|
+
timestamp: Date.now(),
|
|
1987
|
+
reportCount: 1,
|
|
1988
|
+
captchaCount: errorType === "captcha" ? 1 : 0,
|
|
1989
|
+
captchaStage: errorType === "captcha" ? stage || "" : "",
|
|
1990
|
+
captchaMessage: errorType === "captcha" ? errorMessage || "" : "",
|
|
1991
|
+
captchaStack: errorType === "captcha" ? errorStack || "" : "",
|
|
1992
|
+
});
|
|
1993
|
+
}
|
|
1994
|
+
}
|
|
1995
|
+
|
|
1996
|
+
function deleteClientError(userId) {
|
|
1997
|
+
clientErrors.delete(userId);
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
function getClientErrors() {
|
|
2001
|
+
return Array.from(clientErrors.values());
|
|
2002
|
+
}
|
|
2003
|
+
|
|
2004
|
+
function getPendingUserUpdateTasks(limit) {
|
|
2005
|
+
if (db) {
|
|
2006
|
+
const l = Math.max(1, parseInt(limit) || 5);
|
|
2007
|
+
const rows = db
|
|
2008
|
+
.prepare(
|
|
2009
|
+
`
|
|
2010
|
+
SELECT *
|
|
2011
|
+
FROM jobs
|
|
2012
|
+
WHERE COALESCE(tt_seller, '') = ''
|
|
2013
|
+
AND COALESCE(user_update_count, 0) <= 0
|
|
2014
|
+
ORDER BY created_at ASC, unique_id ASC
|
|
2015
|
+
LIMIT ?
|
|
2016
|
+
`,
|
|
2017
|
+
)
|
|
2018
|
+
.all(l);
|
|
2019
|
+
if (rows.length === 0) return [];
|
|
2020
|
+
const now = Date.now();
|
|
2021
|
+
const bumpStmt = db.prepare(
|
|
2022
|
+
`
|
|
2023
|
+
UPDATE jobs
|
|
2024
|
+
SET user_update_count = COALESCE(user_update_count, 0) + 1,
|
|
2025
|
+
updated_at = ?
|
|
2026
|
+
WHERE unique_id = ?
|
|
2027
|
+
`,
|
|
2028
|
+
);
|
|
2029
|
+
const bumpTxn = db.transaction((items) => {
|
|
2030
|
+
for (const item of items) {
|
|
2031
|
+
bumpStmt.run(now, item.unique_id);
|
|
2032
|
+
}
|
|
2033
|
+
});
|
|
2034
|
+
bumpTxn(rows);
|
|
2035
|
+
return rows.map((row) => {
|
|
2036
|
+
const mapped = mapJobRow(row);
|
|
2037
|
+
mapped.userUpdateCount = (mapped.userUpdateCount || 0) + 1;
|
|
2038
|
+
mapped.updatedAt = now;
|
|
2039
|
+
return mapped;
|
|
2040
|
+
});
|
|
2041
|
+
}
|
|
2042
|
+
|
|
2043
|
+
const l = Math.max(1, parseInt(limit) || 5);
|
|
2044
|
+
const pending = data
|
|
2045
|
+
.filter((u) => {
|
|
2046
|
+
const updateCount = u.userUpdateCount;
|
|
2047
|
+
const ttSellerEmpty =
|
|
2048
|
+
u.ttSeller === null || u.ttSeller === undefined || u.ttSeller === "";
|
|
2049
|
+
if (!ttSellerEmpty) return false;
|
|
2050
|
+
return (
|
|
2051
|
+
updateCount === null || updateCount === undefined || updateCount <= 0
|
|
2052
|
+
);
|
|
2053
|
+
})
|
|
2054
|
+
.slice(0, l);
|
|
2055
|
+
// 接受任务时 userUpdateCount + 1
|
|
2056
|
+
pending.forEach((u) => {
|
|
2057
|
+
u.userUpdateCount = (u.userUpdateCount || 0) + 1;
|
|
2058
|
+
u.updatedAt = Date.now();
|
|
2059
|
+
});
|
|
2060
|
+
save();
|
|
2061
|
+
return pending;
|
|
2062
|
+
}
|
|
2063
|
+
|
|
2064
|
+
function updateUserInfo(uniqueId, info) {
|
|
2065
|
+
if (db) {
|
|
2066
|
+
return updateJobInfo(uniqueId, info, true);
|
|
2067
|
+
}
|
|
2068
|
+
|
|
2069
|
+
const user = getUser(uniqueId);
|
|
2070
|
+
if (!user) return { error: "user not found" };
|
|
2071
|
+
for (const key of Object.keys(info)) {
|
|
2072
|
+
if (key === "uniqueId" || key === "sources") continue;
|
|
2073
|
+
if (info[key] !== undefined && info[key] !== null && info[key] !== "") {
|
|
2074
|
+
user[key] = info[key];
|
|
2075
|
+
}
|
|
2076
|
+
}
|
|
2077
|
+
user.userUpdateCount = (user.userUpdateCount || 0) + 1;
|
|
2078
|
+
user.updatedAt = Date.now();
|
|
2079
|
+
save();
|
|
2080
|
+
return { ok: true, userUpdateCount: user.userUpdateCount };
|
|
2081
|
+
}
|
|
2082
|
+
|
|
2083
|
+
function batchUpdateUserInfo(updates) {
|
|
2084
|
+
if (db) {
|
|
2085
|
+
const txn = db.transaction((items) =>
|
|
2086
|
+
items.map((item) => updateJobInfo(item.uniqueId, item.info, true)),
|
|
2087
|
+
);
|
|
2088
|
+
return txn(updates).map((result, index) =>
|
|
2089
|
+
result.error
|
|
2090
|
+
? { uniqueId: updates[index].uniqueId, error: result.error }
|
|
2091
|
+
: {
|
|
2092
|
+
uniqueId: updates[index].uniqueId,
|
|
2093
|
+
ok: true,
|
|
2094
|
+
userUpdateCount: result.userUpdateCount,
|
|
2095
|
+
},
|
|
2096
|
+
);
|
|
2097
|
+
}
|
|
2098
|
+
|
|
2099
|
+
const results = [];
|
|
2100
|
+
for (const item of updates) {
|
|
2101
|
+
const user = getUser(item.uniqueId);
|
|
2102
|
+
if (!user) {
|
|
2103
|
+
results.push({ uniqueId: item.uniqueId, error: "user not found" });
|
|
2104
|
+
continue;
|
|
2105
|
+
}
|
|
2106
|
+
for (const key of Object.keys(item.info)) {
|
|
2107
|
+
if (key === "uniqueId" || key === "sources") continue;
|
|
2108
|
+
if (
|
|
2109
|
+
item.info[key] !== undefined &&
|
|
2110
|
+
item.info[key] !== null &&
|
|
2111
|
+
item.info[key] !== ""
|
|
2112
|
+
) {
|
|
2113
|
+
user[key] = item.info[key];
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
user.userUpdateCount = (user.userUpdateCount || 0) + 1;
|
|
2117
|
+
user.updatedAt = Date.now();
|
|
2118
|
+
results.push({
|
|
2119
|
+
uniqueId: item.uniqueId,
|
|
2120
|
+
ok: true,
|
|
2121
|
+
userUpdateCount: user.userUpdateCount,
|
|
2122
|
+
});
|
|
2123
|
+
}
|
|
2124
|
+
save();
|
|
2125
|
+
return results;
|
|
2126
|
+
}
|
|
2127
|
+
|
|
2128
|
+
// 视频登记
|
|
2129
|
+
function registerVideos(sourceUser, videoList, locationCreated, ttSeller) {
|
|
2130
|
+
if (!videoList || !Array.isArray(videoList) || videoList.length === 0) {
|
|
2131
|
+
return { registered: 0, skipped: 0 };
|
|
2132
|
+
}
|
|
2133
|
+
|
|
2134
|
+
if (db) {
|
|
2135
|
+
const insertStmt = db.prepare(`
|
|
2136
|
+
INSERT OR IGNORE INTO videos (
|
|
2137
|
+
id,
|
|
2138
|
+
href,
|
|
2139
|
+
author_unique_id,
|
|
2140
|
+
location_created,
|
|
2141
|
+
tt_seller,
|
|
2142
|
+
registered_at,
|
|
2143
|
+
user_update_count
|
|
2144
|
+
)
|
|
2145
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
2146
|
+
`);
|
|
2147
|
+
let registered = 0;
|
|
2148
|
+
let skipped = 0;
|
|
2149
|
+
const now = Date.now();
|
|
2150
|
+
const txn = db.transaction((items) => {
|
|
2151
|
+
for (const item of items) {
|
|
2152
|
+
const result = insertStmt.run(
|
|
2153
|
+
item.id,
|
|
2154
|
+
item.href || null,
|
|
2155
|
+
sourceUser,
|
|
2156
|
+
locationCreated || null,
|
|
2157
|
+
ttSeller ? 1 : 0,
|
|
2158
|
+
now,
|
|
2159
|
+
0,
|
|
2160
|
+
);
|
|
2161
|
+
if (result.changes > 0) registered++;
|
|
2162
|
+
else skipped++;
|
|
2163
|
+
}
|
|
2164
|
+
});
|
|
2165
|
+
txn(videoList.filter((item) => item?.id));
|
|
2166
|
+
return { registered, skipped };
|
|
2167
|
+
}
|
|
2168
|
+
|
|
2169
|
+
const existingIds = new Set(videos.map((v) => v.id));
|
|
2170
|
+
let registered = 0;
|
|
2171
|
+
let skipped = 0;
|
|
2172
|
+
|
|
2173
|
+
for (const item of videoList) {
|
|
2174
|
+
if (existingIds.has(item.id)) {
|
|
2175
|
+
skipped++;
|
|
2176
|
+
continue;
|
|
2177
|
+
}
|
|
2178
|
+
videos.push({
|
|
2179
|
+
id: item.id,
|
|
2180
|
+
href: item.href,
|
|
2181
|
+
authorUniqueId: sourceUser,
|
|
2182
|
+
locationCreated: locationCreated || null,
|
|
2183
|
+
ttSeller: ttSeller || false,
|
|
2184
|
+
registeredAt: Date.now(),
|
|
2185
|
+
});
|
|
2186
|
+
existingIds.add(item.id);
|
|
2187
|
+
registered++;
|
|
2188
|
+
}
|
|
2189
|
+
|
|
2190
|
+
saveVideos();
|
|
2191
|
+
return { registered, skipped };
|
|
2192
|
+
}
|
|
2193
|
+
|
|
2194
|
+
function getVideos() {
|
|
2195
|
+
if (db) {
|
|
2196
|
+
return getAllVideoRows().map(mapVideoRow);
|
|
2197
|
+
}
|
|
2198
|
+
return videos;
|
|
2199
|
+
}
|
|
2200
|
+
|
|
2201
|
+
function getVideo(videoId) {
|
|
2202
|
+
if (!videoId) return null;
|
|
2203
|
+
if (db) {
|
|
2204
|
+
return mapVideoRow(getVideoRow(videoId));
|
|
2205
|
+
}
|
|
2206
|
+
return videos.find((video) => video.id === videoId) || null;
|
|
2207
|
+
}
|
|
2208
|
+
|
|
2209
|
+
function getVideosPage(limit, offset) {
|
|
2210
|
+
const safeLimit = Math.max(1, Math.min(200, parseInt(limit) || 50));
|
|
2211
|
+
const safeOffset = Math.max(0, parseInt(offset) || 0);
|
|
2212
|
+
|
|
2213
|
+
if (db) {
|
|
2214
|
+
const rows = db
|
|
2215
|
+
.prepare(
|
|
2216
|
+
`
|
|
2217
|
+
SELECT *
|
|
2218
|
+
FROM videos
|
|
2219
|
+
ORDER BY registered_at DESC, id DESC
|
|
2220
|
+
LIMIT ? OFFSET ?
|
|
2221
|
+
`,
|
|
2222
|
+
)
|
|
2223
|
+
.all(safeLimit, safeOffset);
|
|
2224
|
+
const total = db.prepare("SELECT COUNT(*) as c FROM videos").get().c;
|
|
2225
|
+
return {
|
|
2226
|
+
total,
|
|
2227
|
+
limit: safeLimit,
|
|
2228
|
+
offset: safeOffset,
|
|
2229
|
+
videos: rows.map(mapVideoRow),
|
|
2230
|
+
};
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2233
|
+
return {
|
|
2234
|
+
total: videos.length,
|
|
2235
|
+
limit: safeLimit,
|
|
2236
|
+
offset: safeOffset,
|
|
2237
|
+
videos: videos.slice(safeOffset, safeOffset + safeLimit),
|
|
2238
|
+
};
|
|
2239
|
+
}
|
|
2240
|
+
|
|
2241
|
+
function getVideoCount() {
|
|
2242
|
+
if (db) {
|
|
2243
|
+
return db.prepare("SELECT COUNT(*) as c FROM videos").get().c;
|
|
2244
|
+
}
|
|
2245
|
+
return videos.length;
|
|
2246
|
+
}
|
|
2247
|
+
|
|
2248
|
+
function getPendingCommentTasks(limit) {
|
|
2249
|
+
if (db) {
|
|
2250
|
+
const l = Math.max(1, parseInt(limit) || 1);
|
|
2251
|
+
const rows = db
|
|
2252
|
+
.prepare(
|
|
2253
|
+
`
|
|
2254
|
+
SELECT *
|
|
2255
|
+
FROM videos
|
|
2256
|
+
WHERE user_update_count IS NULL OR user_update_count <= 0
|
|
2257
|
+
ORDER BY tt_seller DESC, registered_at ASC
|
|
2258
|
+
LIMIT ?
|
|
2259
|
+
`,
|
|
2260
|
+
)
|
|
2261
|
+
.all(l);
|
|
2262
|
+
if (rows.length === 0) return [];
|
|
2263
|
+
const bumpStmt = db.prepare(
|
|
2264
|
+
`
|
|
2265
|
+
UPDATE videos
|
|
2266
|
+
SET user_update_count = COALESCE(user_update_count, 0) + 1
|
|
2267
|
+
WHERE id = ?
|
|
2268
|
+
`,
|
|
2269
|
+
);
|
|
2270
|
+
const bumpTxn = db.transaction((items) => {
|
|
2271
|
+
for (const item of items) bumpStmt.run(item.id);
|
|
2272
|
+
});
|
|
2273
|
+
bumpTxn(rows);
|
|
2274
|
+
return rows.map((row) => {
|
|
2275
|
+
const mapped = mapVideoRow(row);
|
|
2276
|
+
mapped.userUpdateCount = (mapped.userUpdateCount || 0) + 1;
|
|
2277
|
+
return mapped;
|
|
2278
|
+
});
|
|
2279
|
+
}
|
|
2280
|
+
|
|
2281
|
+
// 筛选待处理视频(userUpdateCount <= 0 或 null/undefined)
|
|
2282
|
+
const pending = videos.filter((v) => (v.userUpdateCount || 0) <= 0);
|
|
2283
|
+
// ttSeller=true 优先
|
|
2284
|
+
pending.sort((a, b) => {
|
|
2285
|
+
if (a.ttSeller && !b.ttSeller) return -1;
|
|
2286
|
+
if (!a.ttSeller && b.ttSeller) return 1;
|
|
2287
|
+
return (a.registeredAt || 0) - (b.registeredAt || 0);
|
|
2288
|
+
});
|
|
2289
|
+
// 取前 limit 个
|
|
2290
|
+
const tasks = pending.slice(0, limit);
|
|
2291
|
+
// userUpdateCount +1
|
|
2292
|
+
for (const task of tasks) {
|
|
2293
|
+
task.userUpdateCount = (task.userUpdateCount || 0) + 1;
|
|
2294
|
+
}
|
|
2295
|
+
saveVideos();
|
|
2296
|
+
return tasks;
|
|
2297
|
+
}
|
|
2298
|
+
|
|
2299
|
+
function commitCommentTask(videoId) {
|
|
2300
|
+
if (db) {
|
|
2301
|
+
const video = getVideoRow(videoId);
|
|
2302
|
+
if (!video) return { ok: false, error: "video not found" };
|
|
2303
|
+
const nextCount = (video.user_update_count || 0) + 1;
|
|
2304
|
+
db.prepare(
|
|
2305
|
+
`
|
|
2306
|
+
UPDATE videos
|
|
2307
|
+
SET user_update_count = ?
|
|
2308
|
+
WHERE id = ?
|
|
2309
|
+
`,
|
|
2310
|
+
).run(nextCount, videoId);
|
|
2311
|
+
return { ok: true, userUpdateCount: nextCount };
|
|
2312
|
+
}
|
|
2313
|
+
|
|
2314
|
+
const video = videos.find((v) => v.id === videoId);
|
|
2315
|
+
if (!video) return { ok: false, error: "video not found" };
|
|
2316
|
+
video.userUpdateCount = (video.userUpdateCount || 0) + 1;
|
|
2317
|
+
saveVideos();
|
|
2318
|
+
return { ok: true, userUpdateCount: video.userUpdateCount };
|
|
2319
|
+
}
|
|
2320
|
+
|
|
2321
|
+
return {
|
|
2322
|
+
save,
|
|
2323
|
+
flushSave,
|
|
2324
|
+
getUser,
|
|
2325
|
+
hasUser,
|
|
2326
|
+
userExists,
|
|
2327
|
+
addUser,
|
|
2328
|
+
getPendingUsers,
|
|
2329
|
+
getProcessedUsers,
|
|
2330
|
+
getAllUsers,
|
|
2331
|
+
getUserDbCount,
|
|
2332
|
+
getJobsCount,
|
|
2333
|
+
getPendingJobsCount,
|
|
2334
|
+
getPendingJobsUserUpdateCount,
|
|
2335
|
+
getDashboardStats: getDashboardStatsFromDb,
|
|
2336
|
+
getUsersPage: getUsersPageFromDb,
|
|
2337
|
+
getTargetUsers: getTargetUsersFromDb,
|
|
2338
|
+
getStats,
|
|
2339
|
+
getStatusGroups,
|
|
2340
|
+
markGroupsDirty,
|
|
2341
|
+
claimNextJob,
|
|
2342
|
+
commitJob,
|
|
2343
|
+
commitNewExplore,
|
|
2344
|
+
resetJob,
|
|
2345
|
+
togglePin,
|
|
2346
|
+
getNextRedoJob,
|
|
2347
|
+
commitRedoJob,
|
|
2348
|
+
getPendingUserUpdateTasks,
|
|
2349
|
+
updateUserInfo,
|
|
2350
|
+
batchUpdateUserInfo,
|
|
2351
|
+
reportClientError,
|
|
2352
|
+
deleteClientError,
|
|
2353
|
+
getClientErrors,
|
|
2354
|
+
registerVideos,
|
|
2355
|
+
getVideo,
|
|
2356
|
+
getVideos,
|
|
2357
|
+
getVideosPage,
|
|
2358
|
+
getVideoCount,
|
|
2359
|
+
getPendingCommentTasks,
|
|
2360
|
+
commitCommentTask,
|
|
2361
|
+
stopBackup,
|
|
2362
|
+
data,
|
|
2363
|
+
};
|
|
2364
|
+
}
|