tt-help-cli-ycl 1.3.41 → 1.3.44
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/package.json +1 -1
- package/scripts/run-explore.bat +3 -1
- package/scripts/run-explore.ps1 +3 -1
- package/scripts/run-explore.sh +3 -1
- package/src/cli/comments.js +14 -22
- package/src/cli/explore.js +5 -4
- package/src/lib/args.js +3 -2
- package/src/lib/browser/page.js +8 -7
- package/src/lib/constants.js +3 -2
- package/src/lib/target-locations.js +61 -0
- package/src/scraper/core.js +8 -9
- package/src/scraper/explore-core.js +32 -14
- package/src/scraper/modules/follow-extractor.js +35 -6
- package/src/scraper/refresh-core.js +2 -1
- package/src/watch/data-store.js +458 -2
- package/src/watch/public/index.html +878 -100
- package/src/watch/server.js +88 -20
package/src/watch/data-store.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import path from "path";
|
|
3
3
|
import Database from "better-sqlite3";
|
|
4
|
+
import { isLocationInList } from "../lib/target-locations.js";
|
|
4
5
|
|
|
5
6
|
// SQLite 用户表(用于判重)
|
|
6
7
|
let db = null;
|
|
@@ -116,6 +117,38 @@ function initUserDb(filePath) {
|
|
|
116
117
|
sec_uid TEXT
|
|
117
118
|
)
|
|
118
119
|
`);
|
|
120
|
+
db.exec(`
|
|
121
|
+
CREATE TABLE IF NOT EXISTS raw_jobs (
|
|
122
|
+
unique_id TEXT PRIMARY KEY,
|
|
123
|
+
nickname TEXT,
|
|
124
|
+
status TEXT DEFAULT 'pending',
|
|
125
|
+
sources TEXT,
|
|
126
|
+
claimed_by TEXT,
|
|
127
|
+
claimed_at INTEGER,
|
|
128
|
+
error TEXT,
|
|
129
|
+
pinned INTEGER DEFAULT 0,
|
|
130
|
+
no_video INTEGER DEFAULT 0,
|
|
131
|
+
restricted INTEGER DEFAULT 0,
|
|
132
|
+
user_update_count INTEGER DEFAULT 0,
|
|
133
|
+
tt_seller INTEGER,
|
|
134
|
+
verified INTEGER,
|
|
135
|
+
video_count INTEGER DEFAULT 0,
|
|
136
|
+
comment_count INTEGER DEFAULT 0,
|
|
137
|
+
guessed_location TEXT,
|
|
138
|
+
location_created TEXT,
|
|
139
|
+
follower_count INTEGER DEFAULT 0,
|
|
140
|
+
following_count INTEGER DEFAULT 0,
|
|
141
|
+
heart_count INTEGER DEFAULT 0,
|
|
142
|
+
refresh_time INTEGER,
|
|
143
|
+
processed INTEGER DEFAULT 0,
|
|
144
|
+
processed_at INTEGER,
|
|
145
|
+
created_at INTEGER,
|
|
146
|
+
updated_at INTEGER,
|
|
147
|
+
region TEXT,
|
|
148
|
+
signature TEXT,
|
|
149
|
+
sec_uid TEXT
|
|
150
|
+
)
|
|
151
|
+
`);
|
|
119
152
|
db.exec(`
|
|
120
153
|
CREATE TABLE IF NOT EXISTS videos (
|
|
121
154
|
id TEXT PRIMARY KEY,
|
|
@@ -454,6 +487,11 @@ function getPendingJobsUserUpdateCount() {
|
|
|
454
487
|
.get().c;
|
|
455
488
|
}
|
|
456
489
|
|
|
490
|
+
function getRawJobsCount() {
|
|
491
|
+
if (!db) return 0;
|
|
492
|
+
return db.prepare("SELECT COUNT(*) as c FROM raw_jobs").get().c;
|
|
493
|
+
}
|
|
494
|
+
|
|
457
495
|
function getDashboardStatsFromDb(targetLocations = []) {
|
|
458
496
|
if (!db) return null;
|
|
459
497
|
|
|
@@ -560,6 +598,7 @@ function getDashboardStatsFromDb(targetLocations = []) {
|
|
|
560
598
|
|
|
561
599
|
return {
|
|
562
600
|
totalUsers: total,
|
|
601
|
+
rawJobs: getRawJobsCount(),
|
|
563
602
|
dbTotalUsers: getUserDbCount(),
|
|
564
603
|
jobsTotal: total,
|
|
565
604
|
jobsPending: getPendingJobsCount(),
|
|
@@ -587,6 +626,415 @@ function getDashboardStatsFromDb(targetLocations = []) {
|
|
|
587
626
|
};
|
|
588
627
|
}
|
|
589
628
|
|
|
629
|
+
function getPendingByCountryFromDb() {
|
|
630
|
+
if (!db) return [];
|
|
631
|
+
|
|
632
|
+
// 按 guessed_location 分组统计待处理任务
|
|
633
|
+
const rows = db
|
|
634
|
+
.prepare(
|
|
635
|
+
`
|
|
636
|
+
SELECT
|
|
637
|
+
COALESCE(guessed_location, '未知') as country,
|
|
638
|
+
COUNT(*) as count
|
|
639
|
+
FROM jobs
|
|
640
|
+
WHERE status = 'pending'
|
|
641
|
+
GROUP BY COALESCE(guessed_location, '未知')
|
|
642
|
+
ORDER BY count DESC
|
|
643
|
+
`,
|
|
644
|
+
)
|
|
645
|
+
.all();
|
|
646
|
+
|
|
647
|
+
return rows;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function getUserUpdateByCountryFromDb() {
|
|
651
|
+
if (!db) return [];
|
|
652
|
+
|
|
653
|
+
// 按 guessed_location 分组统计待补资料任务
|
|
654
|
+
const rows = db
|
|
655
|
+
.prepare(
|
|
656
|
+
`
|
|
657
|
+
SELECT
|
|
658
|
+
COALESCE(guessed_location, '未知') as country,
|
|
659
|
+
COUNT(*) as count
|
|
660
|
+
FROM jobs
|
|
661
|
+
WHERE COALESCE(tt_seller, '') = ''
|
|
662
|
+
AND COALESCE(user_update_count, 0) <= 0
|
|
663
|
+
GROUP BY COALESCE(guessed_location, '未知')
|
|
664
|
+
ORDER BY count DESC
|
|
665
|
+
`,
|
|
666
|
+
)
|
|
667
|
+
.all();
|
|
668
|
+
|
|
669
|
+
return rows;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function getAttachStuckByCountryFromDb() {
|
|
673
|
+
if (!db) return [];
|
|
674
|
+
|
|
675
|
+
return db
|
|
676
|
+
.prepare(
|
|
677
|
+
`
|
|
678
|
+
SELECT
|
|
679
|
+
COALESCE(guessed_location, '未知') as country,
|
|
680
|
+
COUNT(*) as count
|
|
681
|
+
FROM jobs
|
|
682
|
+
WHERE COALESCE(tt_seller, '') = ''
|
|
683
|
+
AND COALESCE(user_update_count, 0) = 1
|
|
684
|
+
GROUP BY COALESCE(guessed_location, '未知')
|
|
685
|
+
ORDER BY count DESC
|
|
686
|
+
`,
|
|
687
|
+
)
|
|
688
|
+
.all();
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
function restoreAttachStuckByCountry(country) {
|
|
692
|
+
if (!db) {
|
|
693
|
+
return { restored: 0, country, error: "db not ready" };
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
const normalizedCountry = String(country == null ? "未知" : country).trim();
|
|
697
|
+
if (!normalizedCountry) {
|
|
698
|
+
return {
|
|
699
|
+
restored: 0,
|
|
700
|
+
country: normalizedCountry,
|
|
701
|
+
error: "country is required",
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
const whereSql = `
|
|
706
|
+
COALESCE(tt_seller, '') = ''
|
|
707
|
+
AND COALESCE(user_update_count, 0) = 1
|
|
708
|
+
AND COALESCE(guessed_location, '未知') = ?
|
|
709
|
+
`;
|
|
710
|
+
const count =
|
|
711
|
+
db
|
|
712
|
+
.prepare(
|
|
713
|
+
`
|
|
714
|
+
SELECT COUNT(*) as c
|
|
715
|
+
FROM jobs
|
|
716
|
+
WHERE ${whereSql}
|
|
717
|
+
`,
|
|
718
|
+
)
|
|
719
|
+
.get(normalizedCountry)?.c || 0;
|
|
720
|
+
|
|
721
|
+
if (!count) {
|
|
722
|
+
return { restored: 0, country: normalizedCountry };
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
db.prepare(
|
|
726
|
+
`
|
|
727
|
+
UPDATE jobs
|
|
728
|
+
SET user_update_count = 0,
|
|
729
|
+
updated_at = ?,
|
|
730
|
+
claimed_by = NULL,
|
|
731
|
+
claimed_at = NULL
|
|
732
|
+
WHERE ${whereSql}
|
|
733
|
+
`,
|
|
734
|
+
).run(Date.now(), normalizedCountry);
|
|
735
|
+
|
|
736
|
+
return { restored: count, country: normalizedCountry };
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
function getRawByCountryFromDb() {
|
|
740
|
+
if (!db) return [];
|
|
741
|
+
|
|
742
|
+
return db
|
|
743
|
+
.prepare(
|
|
744
|
+
`
|
|
745
|
+
SELECT
|
|
746
|
+
COALESCE(guessed_location, '未知') as country,
|
|
747
|
+
COUNT(*) as count
|
|
748
|
+
FROM raw_jobs
|
|
749
|
+
GROUP BY COALESCE(guessed_location, '未知')
|
|
750
|
+
ORDER BY count DESC
|
|
751
|
+
`,
|
|
752
|
+
)
|
|
753
|
+
.all();
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
function moveJobsToRawByCountry(scope, country) {
|
|
757
|
+
if (!db) {
|
|
758
|
+
return { moved: 0, scope, country, error: "db not ready" };
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
const normalizedScope = String(scope || "").trim();
|
|
762
|
+
const normalizedCountry = String(country == null ? "未知" : country).trim();
|
|
763
|
+
if (!normalizedCountry) {
|
|
764
|
+
return {
|
|
765
|
+
moved: 0,
|
|
766
|
+
scope: normalizedScope,
|
|
767
|
+
country: normalizedCountry,
|
|
768
|
+
error: "country is required",
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
let scopeWhere = "";
|
|
773
|
+
if (normalizedScope === "pending") {
|
|
774
|
+
scopeWhere = `status = 'pending'`;
|
|
775
|
+
} else if (normalizedScope === "userUpdate") {
|
|
776
|
+
scopeWhere = `COALESCE(tt_seller, '') = '' AND COALESCE(user_update_count, 0) <= 0`;
|
|
777
|
+
} else {
|
|
778
|
+
return {
|
|
779
|
+
moved: 0,
|
|
780
|
+
scope: normalizedScope,
|
|
781
|
+
country: normalizedCountry,
|
|
782
|
+
error: "unsupported scope",
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
const whereSql = `
|
|
787
|
+
${scopeWhere}
|
|
788
|
+
AND COALESCE(guessed_location, '未知') = ?
|
|
789
|
+
`;
|
|
790
|
+
const count =
|
|
791
|
+
db
|
|
792
|
+
.prepare(
|
|
793
|
+
`
|
|
794
|
+
SELECT COUNT(*) as c
|
|
795
|
+
FROM jobs
|
|
796
|
+
WHERE ${whereSql}
|
|
797
|
+
`,
|
|
798
|
+
)
|
|
799
|
+
.get(normalizedCountry)?.c || 0;
|
|
800
|
+
|
|
801
|
+
if (!count) {
|
|
802
|
+
return { moved: 0, scope: normalizedScope, country: normalizedCountry };
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
const moveTxn = db.transaction((targetCountry) => {
|
|
806
|
+
db.prepare(
|
|
807
|
+
`
|
|
808
|
+
INSERT OR REPLACE INTO raw_jobs (
|
|
809
|
+
unique_id,
|
|
810
|
+
nickname,
|
|
811
|
+
status,
|
|
812
|
+
sources,
|
|
813
|
+
claimed_by,
|
|
814
|
+
claimed_at,
|
|
815
|
+
error,
|
|
816
|
+
pinned,
|
|
817
|
+
no_video,
|
|
818
|
+
restricted,
|
|
819
|
+
user_update_count,
|
|
820
|
+
tt_seller,
|
|
821
|
+
verified,
|
|
822
|
+
video_count,
|
|
823
|
+
comment_count,
|
|
824
|
+
guessed_location,
|
|
825
|
+
location_created,
|
|
826
|
+
follower_count,
|
|
827
|
+
following_count,
|
|
828
|
+
heart_count,
|
|
829
|
+
refresh_time,
|
|
830
|
+
processed,
|
|
831
|
+
processed_at,
|
|
832
|
+
created_at,
|
|
833
|
+
updated_at,
|
|
834
|
+
region,
|
|
835
|
+
signature,
|
|
836
|
+
sec_uid
|
|
837
|
+
)
|
|
838
|
+
SELECT
|
|
839
|
+
unique_id,
|
|
840
|
+
nickname,
|
|
841
|
+
status,
|
|
842
|
+
sources,
|
|
843
|
+
claimed_by,
|
|
844
|
+
claimed_at,
|
|
845
|
+
error,
|
|
846
|
+
pinned,
|
|
847
|
+
no_video,
|
|
848
|
+
restricted,
|
|
849
|
+
user_update_count,
|
|
850
|
+
tt_seller,
|
|
851
|
+
verified,
|
|
852
|
+
video_count,
|
|
853
|
+
comment_count,
|
|
854
|
+
guessed_location,
|
|
855
|
+
location_created,
|
|
856
|
+
follower_count,
|
|
857
|
+
following_count,
|
|
858
|
+
heart_count,
|
|
859
|
+
refresh_time,
|
|
860
|
+
processed,
|
|
861
|
+
processed_at,
|
|
862
|
+
created_at,
|
|
863
|
+
updated_at,
|
|
864
|
+
region,
|
|
865
|
+
signature,
|
|
866
|
+
sec_uid
|
|
867
|
+
FROM jobs
|
|
868
|
+
WHERE ${whereSql}
|
|
869
|
+
`,
|
|
870
|
+
).run(targetCountry);
|
|
871
|
+
|
|
872
|
+
db.prepare(
|
|
873
|
+
`
|
|
874
|
+
DELETE FROM jobs
|
|
875
|
+
WHERE ${whereSql}
|
|
876
|
+
`,
|
|
877
|
+
).run(targetCountry);
|
|
878
|
+
});
|
|
879
|
+
|
|
880
|
+
moveTxn(normalizedCountry);
|
|
881
|
+
return { moved: count, scope: normalizedScope, country: normalizedCountry };
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
function restoreRawJobsByCountry(country) {
|
|
885
|
+
if (!db) {
|
|
886
|
+
return { restored: 0, country, error: "db not ready" };
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
const normalizedCountry = String(country == null ? "未知" : country).trim();
|
|
890
|
+
if (!normalizedCountry) {
|
|
891
|
+
return {
|
|
892
|
+
restored: 0,
|
|
893
|
+
country: normalizedCountry,
|
|
894
|
+
error: "country is required",
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
const whereSql = `COALESCE(guessed_location, '未知') = ?`;
|
|
899
|
+
const count =
|
|
900
|
+
db
|
|
901
|
+
.prepare(
|
|
902
|
+
`
|
|
903
|
+
SELECT COUNT(*) as c
|
|
904
|
+
FROM raw_jobs
|
|
905
|
+
WHERE ${whereSql}
|
|
906
|
+
`,
|
|
907
|
+
)
|
|
908
|
+
.get(normalizedCountry)?.c || 0;
|
|
909
|
+
|
|
910
|
+
if (!count) {
|
|
911
|
+
return { restored: 0, country: normalizedCountry };
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
const restoreTxn = db.transaction((targetCountry) => {
|
|
915
|
+
db.prepare(
|
|
916
|
+
`
|
|
917
|
+
INSERT OR REPLACE INTO jobs (
|
|
918
|
+
unique_id,
|
|
919
|
+
nickname,
|
|
920
|
+
status,
|
|
921
|
+
sources,
|
|
922
|
+
claimed_by,
|
|
923
|
+
claimed_at,
|
|
924
|
+
error,
|
|
925
|
+
pinned,
|
|
926
|
+
no_video,
|
|
927
|
+
restricted,
|
|
928
|
+
user_update_count,
|
|
929
|
+
tt_seller,
|
|
930
|
+
verified,
|
|
931
|
+
video_count,
|
|
932
|
+
comment_count,
|
|
933
|
+
guessed_location,
|
|
934
|
+
location_created,
|
|
935
|
+
follower_count,
|
|
936
|
+
following_count,
|
|
937
|
+
heart_count,
|
|
938
|
+
refresh_time,
|
|
939
|
+
processed,
|
|
940
|
+
processed_at,
|
|
941
|
+
created_at,
|
|
942
|
+
updated_at,
|
|
943
|
+
region,
|
|
944
|
+
signature,
|
|
945
|
+
sec_uid
|
|
946
|
+
)
|
|
947
|
+
SELECT
|
|
948
|
+
unique_id,
|
|
949
|
+
nickname,
|
|
950
|
+
status,
|
|
951
|
+
sources,
|
|
952
|
+
claimed_by,
|
|
953
|
+
claimed_at,
|
|
954
|
+
error,
|
|
955
|
+
pinned,
|
|
956
|
+
no_video,
|
|
957
|
+
restricted,
|
|
958
|
+
user_update_count,
|
|
959
|
+
tt_seller,
|
|
960
|
+
verified,
|
|
961
|
+
video_count,
|
|
962
|
+
comment_count,
|
|
963
|
+
guessed_location,
|
|
964
|
+
location_created,
|
|
965
|
+
follower_count,
|
|
966
|
+
following_count,
|
|
967
|
+
heart_count,
|
|
968
|
+
refresh_time,
|
|
969
|
+
processed,
|
|
970
|
+
processed_at,
|
|
971
|
+
created_at,
|
|
972
|
+
updated_at,
|
|
973
|
+
region,
|
|
974
|
+
signature,
|
|
975
|
+
sec_uid
|
|
976
|
+
FROM raw_jobs
|
|
977
|
+
WHERE ${whereSql}
|
|
978
|
+
`,
|
|
979
|
+
).run(targetCountry);
|
|
980
|
+
|
|
981
|
+
db.prepare(
|
|
982
|
+
`
|
|
983
|
+
DELETE FROM raw_jobs
|
|
984
|
+
WHERE ${whereSql}
|
|
985
|
+
`,
|
|
986
|
+
).run(targetCountry);
|
|
987
|
+
});
|
|
988
|
+
|
|
989
|
+
restoreTxn(normalizedCountry);
|
|
990
|
+
return { restored: count, country: normalizedCountry };
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
function getRawJobsPageFromDb({ search, location, limit, offset }) {
|
|
994
|
+
if (!db) return null;
|
|
995
|
+
|
|
996
|
+
const safeLimit = Math.max(1, Math.min(200, parseInt(limit) || 50));
|
|
997
|
+
const safeOffset = Math.max(0, parseInt(offset) || 0);
|
|
998
|
+
const where = [];
|
|
999
|
+
const args = [];
|
|
1000
|
+
|
|
1001
|
+
if (search) {
|
|
1002
|
+
where.push(
|
|
1003
|
+
"(LOWER(unique_id) LIKE ? OR LOWER(COALESCE(nickname, '')) LIKE ?)",
|
|
1004
|
+
);
|
|
1005
|
+
const pattern = `%${String(search).toLowerCase()}%`;
|
|
1006
|
+
args.push(pattern, pattern);
|
|
1007
|
+
}
|
|
1008
|
+
if (location) {
|
|
1009
|
+
where.push("COALESCE(guessed_location, '未知') = ?");
|
|
1010
|
+
args.push(location);
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
const whereSql = where.length ? `WHERE ${where.join(" AND ")}` : "";
|
|
1014
|
+
const total = db
|
|
1015
|
+
.prepare(`SELECT COUNT(*) as c FROM raw_jobs ${whereSql}`)
|
|
1016
|
+
.get(...args).c;
|
|
1017
|
+
|
|
1018
|
+
const rows = db
|
|
1019
|
+
.prepare(
|
|
1020
|
+
`
|
|
1021
|
+
SELECT *
|
|
1022
|
+
FROM raw_jobs
|
|
1023
|
+
${whereSql}
|
|
1024
|
+
ORDER BY created_at DESC, unique_id ASC
|
|
1025
|
+
LIMIT ? OFFSET ?
|
|
1026
|
+
`,
|
|
1027
|
+
)
|
|
1028
|
+
.all(...args, safeLimit, safeOffset);
|
|
1029
|
+
|
|
1030
|
+
return {
|
|
1031
|
+
total,
|
|
1032
|
+
limit: safeLimit,
|
|
1033
|
+
offset: safeOffset,
|
|
1034
|
+
users: rows.map(mapJobRow),
|
|
1035
|
+
};
|
|
1036
|
+
}
|
|
1037
|
+
|
|
590
1038
|
function getUsersPageFromDb({
|
|
591
1039
|
status,
|
|
592
1040
|
search,
|
|
@@ -1464,8 +1912,7 @@ export function createStore(filePath) {
|
|
|
1464
1912
|
// 国家过滤:如果指定了 locations,只保留 guessedLocation 在列表中的用户
|
|
1465
1913
|
function locationFilter(u) {
|
|
1466
1914
|
if (!locations || locations.length === 0) return true;
|
|
1467
|
-
|
|
1468
|
-
return locations.includes(loc);
|
|
1915
|
+
return isLocationInList(u.guessedLocation, locations);
|
|
1469
1916
|
}
|
|
1470
1917
|
|
|
1471
1918
|
// 从候选列表中按优先级取第一个:pinned > 超时回收 > seed > ttSeller(仅登录) > follow > other
|
|
@@ -2356,10 +2803,19 @@ export function createStore(filePath) {
|
|
|
2356
2803
|
getAllUsers,
|
|
2357
2804
|
getUserDbCount,
|
|
2358
2805
|
getJobsCount,
|
|
2806
|
+
getRawJobsCount,
|
|
2359
2807
|
getPendingJobsCount,
|
|
2360
2808
|
getPendingJobsUserUpdateCount,
|
|
2361
2809
|
getDashboardStats: getDashboardStatsFromDb,
|
|
2810
|
+
getPendingByCountry: getPendingByCountryFromDb,
|
|
2811
|
+
getUserUpdateByCountry: getUserUpdateByCountryFromDb,
|
|
2812
|
+
getAttachStuckByCountry: getAttachStuckByCountryFromDb,
|
|
2813
|
+
getRawByCountry: getRawByCountryFromDb,
|
|
2814
|
+
moveJobsToRawByCountry,
|
|
2815
|
+
restoreAttachStuckByCountry,
|
|
2816
|
+
restoreRawJobsByCountry,
|
|
2362
2817
|
getUsersPage: getUsersPageFromDb,
|
|
2818
|
+
getRawJobsPage: getRawJobsPageFromDb,
|
|
2363
2819
|
getTargetUsers: getTargetUsersFromDb,
|
|
2364
2820
|
getStats,
|
|
2365
2821
|
getStatusGroups,
|