From 098cd44109175cb1bfccde406eda06d1bd70fe9d Mon Sep 17 00:00:00 2001
From: MarSeventh <1193267292@qq.com>
Date: Tue, 28 Jul 2026 21:05:18 +0800
Subject: [PATCH] feat: deploy star history via GitHub Pages
---
.github/star-history/history.json | 3617 +++++++++++++++++++++++
.github/star-history/star_history.py | 840 ++++++
.github/workflows/sync-star-history.yml | 230 ++
README.md | 8 +-
README_zh.md | 8 +-
5 files changed, 4695 insertions(+), 8 deletions(-)
create mode 100644 .github/star-history/history.json
create mode 100644 .github/star-history/star_history.py
create mode 100644 .github/workflows/sync-star-history.yml
diff --git a/.github/star-history/history.json b/.github/star-history/history.json
new file mode 100644
index 00000000..196fe028
--- /dev/null
+++ b/.github/star-history/history.json
@@ -0,0 +1,3617 @@
+{
+ "ongoing_interval_days": 7,
+ "schema_version": 2,
+ "series": [
+ {
+ "reconstruction": {
+ "daily": [
+ {
+ "date": "2024-07-20",
+ "stars": 39
+ },
+ {
+ "date": "2024-07-21",
+ "stars": 59
+ },
+ {
+ "date": "2024-07-22",
+ "stars": 79
+ },
+ {
+ "date": "2024-07-23",
+ "stars": 83
+ },
+ {
+ "date": "2024-07-24",
+ "stars": 88
+ },
+ {
+ "date": "2024-07-25",
+ "stars": 89
+ },
+ {
+ "date": "2024-07-26",
+ "stars": 95
+ },
+ {
+ "date": "2024-07-27",
+ "stars": 98
+ },
+ {
+ "date": "2024-07-28",
+ "stars": 101
+ },
+ {
+ "date": "2024-07-29",
+ "stars": 103
+ },
+ {
+ "date": "2024-07-30",
+ "stars": 108
+ },
+ {
+ "date": "2024-07-31",
+ "stars": 110
+ },
+ {
+ "date": "2024-08-01",
+ "stars": 113
+ },
+ {
+ "date": "2024-08-02",
+ "stars": 127
+ },
+ {
+ "date": "2024-08-05",
+ "stars": 130
+ },
+ {
+ "date": "2024-08-06",
+ "stars": 134
+ },
+ {
+ "date": "2024-08-07",
+ "stars": 138
+ },
+ {
+ "date": "2024-08-08",
+ "stars": 140
+ },
+ {
+ "date": "2024-08-10",
+ "stars": 141
+ },
+ {
+ "date": "2024-08-11",
+ "stars": 142
+ },
+ {
+ "date": "2024-08-12",
+ "stars": 143
+ },
+ {
+ "date": "2024-08-13",
+ "stars": 146
+ },
+ {
+ "date": "2024-08-14",
+ "stars": 149
+ },
+ {
+ "date": "2024-08-15",
+ "stars": 152
+ },
+ {
+ "date": "2024-08-17",
+ "stars": 156
+ },
+ {
+ "date": "2024-08-18",
+ "stars": 158
+ },
+ {
+ "date": "2024-08-19",
+ "stars": 160
+ },
+ {
+ "date": "2024-08-20",
+ "stars": 163
+ },
+ {
+ "date": "2024-08-21",
+ "stars": 167
+ },
+ {
+ "date": "2024-08-22",
+ "stars": 171
+ },
+ {
+ "date": "2024-08-23",
+ "stars": 176
+ },
+ {
+ "date": "2024-08-24",
+ "stars": 181
+ },
+ {
+ "date": "2024-08-25",
+ "stars": 183
+ },
+ {
+ "date": "2024-08-26",
+ "stars": 194
+ },
+ {
+ "date": "2024-08-27",
+ "stars": 196
+ },
+ {
+ "date": "2024-08-28",
+ "stars": 200
+ },
+ {
+ "date": "2024-08-29",
+ "stars": 203
+ },
+ {
+ "date": "2024-08-30",
+ "stars": 206
+ },
+ {
+ "date": "2024-08-31",
+ "stars": 207
+ },
+ {
+ "date": "2024-09-01",
+ "stars": 213
+ },
+ {
+ "date": "2024-09-02",
+ "stars": 217
+ },
+ {
+ "date": "2024-09-03",
+ "stars": 227
+ },
+ {
+ "date": "2024-09-04",
+ "stars": 230
+ },
+ {
+ "date": "2024-09-05",
+ "stars": 235
+ },
+ {
+ "date": "2024-09-07",
+ "stars": 251
+ },
+ {
+ "date": "2024-09-08",
+ "stars": 271
+ },
+ {
+ "date": "2024-09-09",
+ "stars": 402
+ },
+ {
+ "date": "2024-09-10",
+ "stars": 466
+ },
+ {
+ "date": "2024-09-11",
+ "stars": 491
+ },
+ {
+ "date": "2024-09-12",
+ "stars": 504
+ },
+ {
+ "date": "2024-09-13",
+ "stars": 512
+ },
+ {
+ "date": "2024-09-14",
+ "stars": 527
+ },
+ {
+ "date": "2024-09-15",
+ "stars": 531
+ },
+ {
+ "date": "2024-09-16",
+ "stars": 542
+ },
+ {
+ "date": "2024-09-17",
+ "stars": 551
+ },
+ {
+ "date": "2024-09-18",
+ "stars": 559
+ },
+ {
+ "date": "2024-09-19",
+ "stars": 563
+ },
+ {
+ "date": "2024-09-20",
+ "stars": 565
+ },
+ {
+ "date": "2024-09-21",
+ "stars": 570
+ },
+ {
+ "date": "2024-09-22",
+ "stars": 578
+ },
+ {
+ "date": "2024-09-23",
+ "stars": 586
+ },
+ {
+ "date": "2024-09-24",
+ "stars": 590
+ },
+ {
+ "date": "2024-09-25",
+ "stars": 601
+ },
+ {
+ "date": "2024-09-26",
+ "stars": 605
+ },
+ {
+ "date": "2024-09-27",
+ "stars": 608
+ },
+ {
+ "date": "2024-09-28",
+ "stars": 612
+ },
+ {
+ "date": "2024-09-29",
+ "stars": 617
+ },
+ {
+ "date": "2024-09-30",
+ "stars": 619
+ },
+ {
+ "date": "2024-10-01",
+ "stars": 624
+ },
+ {
+ "date": "2024-10-02",
+ "stars": 630
+ },
+ {
+ "date": "2024-10-03",
+ "stars": 638
+ },
+ {
+ "date": "2024-10-04",
+ "stars": 643
+ },
+ {
+ "date": "2024-10-05",
+ "stars": 649
+ },
+ {
+ "date": "2024-10-06",
+ "stars": 658
+ },
+ {
+ "date": "2024-10-07",
+ "stars": 660
+ },
+ {
+ "date": "2024-10-08",
+ "stars": 662
+ },
+ {
+ "date": "2024-10-09",
+ "stars": 665
+ },
+ {
+ "date": "2024-10-10",
+ "stars": 667
+ },
+ {
+ "date": "2024-10-11",
+ "stars": 675
+ },
+ {
+ "date": "2024-10-12",
+ "stars": 681
+ },
+ {
+ "date": "2024-10-13",
+ "stars": 685
+ },
+ {
+ "date": "2024-10-14",
+ "stars": 691
+ },
+ {
+ "date": "2024-10-15",
+ "stars": 695
+ },
+ {
+ "date": "2024-10-16",
+ "stars": 700
+ },
+ {
+ "date": "2024-10-17",
+ "stars": 707
+ },
+ {
+ "date": "2024-10-18",
+ "stars": 713
+ },
+ {
+ "date": "2024-10-19",
+ "stars": 716
+ },
+ {
+ "date": "2024-10-20",
+ "stars": 723
+ },
+ {
+ "date": "2024-10-21",
+ "stars": 754
+ },
+ {
+ "date": "2024-10-22",
+ "stars": 763
+ },
+ {
+ "date": "2024-10-23",
+ "stars": 769
+ },
+ {
+ "date": "2024-10-24",
+ "stars": 773
+ },
+ {
+ "date": "2024-10-25",
+ "stars": 783
+ },
+ {
+ "date": "2024-10-26",
+ "stars": 786
+ },
+ {
+ "date": "2024-10-27",
+ "stars": 790
+ },
+ {
+ "date": "2024-10-28",
+ "stars": 796
+ },
+ {
+ "date": "2024-10-29",
+ "stars": 799
+ },
+ {
+ "date": "2024-10-30",
+ "stars": 804
+ },
+ {
+ "date": "2024-10-31",
+ "stars": 810
+ },
+ {
+ "date": "2024-11-01",
+ "stars": 812
+ },
+ {
+ "date": "2024-11-02",
+ "stars": 814
+ },
+ {
+ "date": "2024-11-03",
+ "stars": 816
+ },
+ {
+ "date": "2024-11-04",
+ "stars": 822
+ },
+ {
+ "date": "2024-11-05",
+ "stars": 830
+ },
+ {
+ "date": "2024-11-06",
+ "stars": 857
+ },
+ {
+ "date": "2024-11-07",
+ "stars": 866
+ },
+ {
+ "date": "2024-11-08",
+ "stars": 873
+ },
+ {
+ "date": "2024-11-09",
+ "stars": 874
+ },
+ {
+ "date": "2024-11-10",
+ "stars": 879
+ },
+ {
+ "date": "2024-11-11",
+ "stars": 882
+ },
+ {
+ "date": "2024-11-12",
+ "stars": 885
+ },
+ {
+ "date": "2024-11-13",
+ "stars": 889
+ },
+ {
+ "date": "2024-11-14",
+ "stars": 893
+ },
+ {
+ "date": "2024-11-15",
+ "stars": 900
+ },
+ {
+ "date": "2024-11-16",
+ "stars": 903
+ },
+ {
+ "date": "2024-11-17",
+ "stars": 935
+ },
+ {
+ "date": "2024-11-18",
+ "stars": 951
+ },
+ {
+ "date": "2024-11-19",
+ "stars": 961
+ },
+ {
+ "date": "2024-11-20",
+ "stars": 962
+ },
+ {
+ "date": "2024-11-21",
+ "stars": 966
+ },
+ {
+ "date": "2024-11-22",
+ "stars": 969
+ },
+ {
+ "date": "2024-11-23",
+ "stars": 972
+ },
+ {
+ "date": "2024-11-24",
+ "stars": 976
+ },
+ {
+ "date": "2024-11-25",
+ "stars": 982
+ },
+ {
+ "date": "2024-11-26",
+ "stars": 986
+ },
+ {
+ "date": "2024-11-27",
+ "stars": 989
+ },
+ {
+ "date": "2024-11-28",
+ "stars": 999
+ },
+ {
+ "date": "2024-11-29",
+ "stars": 1001
+ },
+ {
+ "date": "2024-11-30",
+ "stars": 1006
+ },
+ {
+ "date": "2024-12-01",
+ "stars": 1010
+ },
+ {
+ "date": "2024-12-02",
+ "stars": 1011
+ },
+ {
+ "date": "2024-12-03",
+ "stars": 1016
+ },
+ {
+ "date": "2024-12-04",
+ "stars": 1019
+ },
+ {
+ "date": "2024-12-05",
+ "stars": 1022
+ },
+ {
+ "date": "2024-12-06",
+ "stars": 1027
+ },
+ {
+ "date": "2024-12-07",
+ "stars": 1029
+ },
+ {
+ "date": "2024-12-08",
+ "stars": 1040
+ },
+ {
+ "date": "2024-12-09",
+ "stars": 1067
+ },
+ {
+ "date": "2024-12-10",
+ "stars": 1083
+ },
+ {
+ "date": "2024-12-11",
+ "stars": 1089
+ },
+ {
+ "date": "2024-12-12",
+ "stars": 1105
+ },
+ {
+ "date": "2024-12-13",
+ "stars": 1120
+ },
+ {
+ "date": "2024-12-14",
+ "stars": 1131
+ },
+ {
+ "date": "2024-12-15",
+ "stars": 1136
+ },
+ {
+ "date": "2024-12-16",
+ "stars": 1149
+ },
+ {
+ "date": "2024-12-17",
+ "stars": 1155
+ },
+ {
+ "date": "2024-12-18",
+ "stars": 1158
+ },
+ {
+ "date": "2024-12-19",
+ "stars": 1168
+ },
+ {
+ "date": "2024-12-20",
+ "stars": 1182
+ },
+ {
+ "date": "2024-12-21",
+ "stars": 1188
+ },
+ {
+ "date": "2024-12-22",
+ "stars": 1194
+ },
+ {
+ "date": "2024-12-23",
+ "stars": 1199
+ },
+ {
+ "date": "2024-12-25",
+ "stars": 1210
+ },
+ {
+ "date": "2024-12-26",
+ "stars": 1217
+ },
+ {
+ "date": "2024-12-27",
+ "stars": 1222
+ },
+ {
+ "date": "2024-12-28",
+ "stars": 1228
+ },
+ {
+ "date": "2024-12-29",
+ "stars": 1237
+ },
+ {
+ "date": "2024-12-30",
+ "stars": 1244
+ },
+ {
+ "date": "2024-12-31",
+ "stars": 1247
+ },
+ {
+ "date": "2025-01-01",
+ "stars": 1251
+ },
+ {
+ "date": "2025-01-02",
+ "stars": 1258
+ },
+ {
+ "date": "2025-01-03",
+ "stars": 1265
+ },
+ {
+ "date": "2025-01-04",
+ "stars": 1268
+ },
+ {
+ "date": "2025-01-05",
+ "stars": 1271
+ },
+ {
+ "date": "2025-01-06",
+ "stars": 1285
+ },
+ {
+ "date": "2025-01-07",
+ "stars": 1293
+ },
+ {
+ "date": "2025-01-08",
+ "stars": 1299
+ },
+ {
+ "date": "2025-01-09",
+ "stars": 1304
+ },
+ {
+ "date": "2025-01-10",
+ "stars": 1308
+ },
+ {
+ "date": "2025-01-11",
+ "stars": 1309
+ },
+ {
+ "date": "2025-01-12",
+ "stars": 1314
+ },
+ {
+ "date": "2025-01-13",
+ "stars": 1322
+ },
+ {
+ "date": "2025-01-14",
+ "stars": 1323
+ },
+ {
+ "date": "2025-01-15",
+ "stars": 1325
+ },
+ {
+ "date": "2025-01-16",
+ "stars": 1326
+ },
+ {
+ "date": "2025-01-17",
+ "stars": 1332
+ },
+ {
+ "date": "2025-01-18",
+ "stars": 1337
+ },
+ {
+ "date": "2025-01-19",
+ "stars": 1341
+ },
+ {
+ "date": "2025-01-20",
+ "stars": 1342
+ },
+ {
+ "date": "2025-01-21",
+ "stars": 1344
+ },
+ {
+ "date": "2025-01-22",
+ "stars": 1347
+ },
+ {
+ "date": "2025-01-23",
+ "stars": 1353
+ },
+ {
+ "date": "2025-01-24",
+ "stars": 1355
+ },
+ {
+ "date": "2025-01-25",
+ "stars": 1357
+ },
+ {
+ "date": "2025-01-26",
+ "stars": 1361
+ },
+ {
+ "date": "2025-01-27",
+ "stars": 1363
+ },
+ {
+ "date": "2025-01-28",
+ "stars": 1366
+ },
+ {
+ "date": "2025-01-29",
+ "stars": 1370
+ },
+ {
+ "date": "2025-01-30",
+ "stars": 1373
+ },
+ {
+ "date": "2025-01-31",
+ "stars": 1374
+ },
+ {
+ "date": "2025-02-01",
+ "stars": 1375
+ },
+ {
+ "date": "2025-02-02",
+ "stars": 1381
+ },
+ {
+ "date": "2025-02-03",
+ "stars": 1394
+ },
+ {
+ "date": "2025-02-04",
+ "stars": 1407
+ },
+ {
+ "date": "2025-02-05",
+ "stars": 1408
+ },
+ {
+ "date": "2025-02-06",
+ "stars": 1417
+ },
+ {
+ "date": "2025-02-07",
+ "stars": 1424
+ },
+ {
+ "date": "2025-02-08",
+ "stars": 1430
+ },
+ {
+ "date": "2025-02-09",
+ "stars": 1431
+ },
+ {
+ "date": "2025-02-10",
+ "stars": 1434
+ },
+ {
+ "date": "2025-02-11",
+ "stars": 1441
+ },
+ {
+ "date": "2025-02-12",
+ "stars": 1442
+ },
+ {
+ "date": "2025-02-13",
+ "stars": 1445
+ },
+ {
+ "date": "2025-02-14",
+ "stars": 1446
+ },
+ {
+ "date": "2025-02-15",
+ "stars": 1451
+ },
+ {
+ "date": "2025-02-16",
+ "stars": 1452
+ },
+ {
+ "date": "2025-02-17",
+ "stars": 1457
+ },
+ {
+ "date": "2025-02-18",
+ "stars": 1460
+ },
+ {
+ "date": "2025-02-19",
+ "stars": 1462
+ },
+ {
+ "date": "2025-02-20",
+ "stars": 1463
+ },
+ {
+ "date": "2025-02-21",
+ "stars": 1464
+ },
+ {
+ "date": "2025-02-22",
+ "stars": 1468
+ },
+ {
+ "date": "2025-02-23",
+ "stars": 1470
+ },
+ {
+ "date": "2025-02-24",
+ "stars": 1474
+ },
+ {
+ "date": "2025-02-25",
+ "stars": 1479
+ },
+ {
+ "date": "2025-02-26",
+ "stars": 1482
+ },
+ {
+ "date": "2025-02-27",
+ "stars": 1484
+ },
+ {
+ "date": "2025-02-28",
+ "stars": 1490
+ },
+ {
+ "date": "2025-03-01",
+ "stars": 1493
+ },
+ {
+ "date": "2025-03-02",
+ "stars": 1496
+ },
+ {
+ "date": "2025-03-03",
+ "stars": 1503
+ },
+ {
+ "date": "2025-03-04",
+ "stars": 1506
+ },
+ {
+ "date": "2025-03-05",
+ "stars": 1512
+ },
+ {
+ "date": "2025-03-06",
+ "stars": 1517
+ },
+ {
+ "date": "2025-03-07",
+ "stars": 1521
+ },
+ {
+ "date": "2025-03-08",
+ "stars": 1525
+ },
+ {
+ "date": "2025-03-09",
+ "stars": 1531
+ },
+ {
+ "date": "2025-03-10",
+ "stars": 1535
+ },
+ {
+ "date": "2025-03-11",
+ "stars": 1544
+ },
+ {
+ "date": "2025-03-12",
+ "stars": 1552
+ },
+ {
+ "date": "2025-03-13",
+ "stars": 1555
+ },
+ {
+ "date": "2025-03-14",
+ "stars": 1570
+ },
+ {
+ "date": "2025-03-15",
+ "stars": 1574
+ },
+ {
+ "date": "2025-03-16",
+ "stars": 1576
+ },
+ {
+ "date": "2025-03-17",
+ "stars": 1581
+ },
+ {
+ "date": "2025-03-18",
+ "stars": 1589
+ },
+ {
+ "date": "2025-03-19",
+ "stars": 1593
+ },
+ {
+ "date": "2025-03-20",
+ "stars": 1596
+ },
+ {
+ "date": "2025-03-21",
+ "stars": 1601
+ },
+ {
+ "date": "2025-03-22",
+ "stars": 1607
+ },
+ {
+ "date": "2025-03-23",
+ "stars": 1609
+ },
+ {
+ "date": "2025-03-24",
+ "stars": 1611
+ },
+ {
+ "date": "2025-03-25",
+ "stars": 1616
+ },
+ {
+ "date": "2025-03-26",
+ "stars": 1625
+ },
+ {
+ "date": "2025-03-27",
+ "stars": 1630
+ },
+ {
+ "date": "2025-03-28",
+ "stars": 1632
+ },
+ {
+ "date": "2025-03-29",
+ "stars": 1634
+ },
+ {
+ "date": "2025-03-30",
+ "stars": 1636
+ },
+ {
+ "date": "2025-03-31",
+ "stars": 1638
+ },
+ {
+ "date": "2025-04-01",
+ "stars": 1639
+ },
+ {
+ "date": "2025-04-02",
+ "stars": 1671
+ },
+ {
+ "date": "2025-04-03",
+ "stars": 1693
+ },
+ {
+ "date": "2025-04-04",
+ "stars": 1708
+ },
+ {
+ "date": "2025-04-05",
+ "stars": 1712
+ },
+ {
+ "date": "2025-04-06",
+ "stars": 1723
+ },
+ {
+ "date": "2025-04-07",
+ "stars": 1735
+ },
+ {
+ "date": "2025-04-08",
+ "stars": 1745
+ },
+ {
+ "date": "2025-04-09",
+ "stars": 1755
+ },
+ {
+ "date": "2025-04-10",
+ "stars": 1762
+ },
+ {
+ "date": "2025-04-11",
+ "stars": 1767
+ },
+ {
+ "date": "2025-04-12",
+ "stars": 1784
+ },
+ {
+ "date": "2025-04-13",
+ "stars": 1792
+ },
+ {
+ "date": "2025-04-14",
+ "stars": 1797
+ },
+ {
+ "date": "2025-04-15",
+ "stars": 1799
+ },
+ {
+ "date": "2025-04-16",
+ "stars": 1804
+ },
+ {
+ "date": "2025-04-17",
+ "stars": 1807
+ },
+ {
+ "date": "2025-04-18",
+ "stars": 1812
+ },
+ {
+ "date": "2025-04-19",
+ "stars": 1816
+ },
+ {
+ "date": "2025-04-20",
+ "stars": 1820
+ },
+ {
+ "date": "2025-04-21",
+ "stars": 1823
+ },
+ {
+ "date": "2025-04-22",
+ "stars": 1831
+ },
+ {
+ "date": "2025-04-23",
+ "stars": 1839
+ },
+ {
+ "date": "2025-04-24",
+ "stars": 1843
+ },
+ {
+ "date": "2025-04-25",
+ "stars": 1848
+ },
+ {
+ "date": "2025-04-26",
+ "stars": 1851
+ },
+ {
+ "date": "2025-04-27",
+ "stars": 1860
+ },
+ {
+ "date": "2025-04-28",
+ "stars": 1862
+ },
+ {
+ "date": "2025-04-29",
+ "stars": 1864
+ },
+ {
+ "date": "2025-04-30",
+ "stars": 1872
+ },
+ {
+ "date": "2025-05-01",
+ "stars": 1879
+ },
+ {
+ "date": "2025-05-02",
+ "stars": 1889
+ },
+ {
+ "date": "2025-05-03",
+ "stars": 1894
+ },
+ {
+ "date": "2025-05-04",
+ "stars": 1899
+ },
+ {
+ "date": "2025-05-05",
+ "stars": 1903
+ },
+ {
+ "date": "2025-05-06",
+ "stars": 1905
+ },
+ {
+ "date": "2025-05-07",
+ "stars": 1910
+ },
+ {
+ "date": "2025-05-08",
+ "stars": 1914
+ },
+ {
+ "date": "2025-05-09",
+ "stars": 1922
+ },
+ {
+ "date": "2025-05-10",
+ "stars": 1930
+ },
+ {
+ "date": "2025-05-11",
+ "stars": 1938
+ },
+ {
+ "date": "2025-05-12",
+ "stars": 1946
+ },
+ {
+ "date": "2025-05-13",
+ "stars": 1953
+ },
+ {
+ "date": "2025-05-14",
+ "stars": 1959
+ },
+ {
+ "date": "2025-05-15",
+ "stars": 1965
+ },
+ {
+ "date": "2025-05-16",
+ "stars": 1971
+ },
+ {
+ "date": "2025-05-17",
+ "stars": 1976
+ },
+ {
+ "date": "2025-05-18",
+ "stars": 1980
+ },
+ {
+ "date": "2025-05-19",
+ "stars": 1985
+ },
+ {
+ "date": "2025-05-20",
+ "stars": 1995
+ },
+ {
+ "date": "2025-05-21",
+ "stars": 2007
+ },
+ {
+ "date": "2025-05-22",
+ "stars": 2017
+ },
+ {
+ "date": "2025-05-23",
+ "stars": 2021
+ },
+ {
+ "date": "2025-05-24",
+ "stars": 2026
+ },
+ {
+ "date": "2025-05-25",
+ "stars": 2027
+ },
+ {
+ "date": "2025-05-26",
+ "stars": 2035
+ },
+ {
+ "date": "2025-05-27",
+ "stars": 2039
+ },
+ {
+ "date": "2025-05-28",
+ "stars": 2046
+ },
+ {
+ "date": "2025-05-29",
+ "stars": 2050
+ },
+ {
+ "date": "2025-05-30",
+ "stars": 2055
+ },
+ {
+ "date": "2025-05-31",
+ "stars": 2057
+ },
+ {
+ "date": "2025-06-01",
+ "stars": 2061
+ },
+ {
+ "date": "2025-06-02",
+ "stars": 2064
+ },
+ {
+ "date": "2025-06-03",
+ "stars": 2069
+ },
+ {
+ "date": "2025-06-04",
+ "stars": 2072
+ },
+ {
+ "date": "2025-06-05",
+ "stars": 2076
+ },
+ {
+ "date": "2025-06-06",
+ "stars": 2079
+ },
+ {
+ "date": "2025-06-07",
+ "stars": 2084
+ },
+ {
+ "date": "2025-06-08",
+ "stars": 2085
+ },
+ {
+ "date": "2025-06-09",
+ "stars": 2125
+ },
+ {
+ "date": "2025-06-10",
+ "stars": 2133
+ },
+ {
+ "date": "2025-06-11",
+ "stars": 2141
+ },
+ {
+ "date": "2025-06-12",
+ "stars": 2143
+ },
+ {
+ "date": "2025-06-13",
+ "stars": 2147
+ },
+ {
+ "date": "2025-06-14",
+ "stars": 2149
+ },
+ {
+ "date": "2025-06-15",
+ "stars": 2167
+ },
+ {
+ "date": "2025-06-16",
+ "stars": 2177
+ },
+ {
+ "date": "2025-06-17",
+ "stars": 2181
+ },
+ {
+ "date": "2025-06-18",
+ "stars": 2186
+ },
+ {
+ "date": "2025-06-19",
+ "stars": 2198
+ },
+ {
+ "date": "2025-06-20",
+ "stars": 2204
+ },
+ {
+ "date": "2025-06-21",
+ "stars": 2209
+ },
+ {
+ "date": "2025-06-22",
+ "stars": 2212
+ },
+ {
+ "date": "2025-06-23",
+ "stars": 2213
+ },
+ {
+ "date": "2025-06-24",
+ "stars": 2217
+ },
+ {
+ "date": "2025-06-25",
+ "stars": 2222
+ },
+ {
+ "date": "2025-06-26",
+ "stars": 2226
+ },
+ {
+ "date": "2025-06-27",
+ "stars": 2232
+ },
+ {
+ "date": "2025-06-28",
+ "stars": 2238
+ },
+ {
+ "date": "2025-06-29",
+ "stars": 2245
+ },
+ {
+ "date": "2025-06-30",
+ "stars": 2249
+ },
+ {
+ "date": "2025-07-01",
+ "stars": 2255
+ },
+ {
+ "date": "2025-07-02",
+ "stars": 2258
+ },
+ {
+ "date": "2025-07-03",
+ "stars": 2260
+ },
+ {
+ "date": "2025-07-04",
+ "stars": 2268
+ },
+ {
+ "date": "2025-07-05",
+ "stars": 2277
+ },
+ {
+ "date": "2025-07-06",
+ "stars": 2285
+ },
+ {
+ "date": "2025-07-07",
+ "stars": 2302
+ },
+ {
+ "date": "2025-07-08",
+ "stars": 2310
+ },
+ {
+ "date": "2025-07-09",
+ "stars": 2320
+ },
+ {
+ "date": "2025-07-10",
+ "stars": 2330
+ },
+ {
+ "date": "2025-07-11",
+ "stars": 2341
+ },
+ {
+ "date": "2025-07-12",
+ "stars": 2350
+ },
+ {
+ "date": "2025-07-13",
+ "stars": 2355
+ },
+ {
+ "date": "2025-07-14",
+ "stars": 2360
+ },
+ {
+ "date": "2025-07-15",
+ "stars": 2365
+ },
+ {
+ "date": "2025-07-16",
+ "stars": 2373
+ },
+ {
+ "date": "2025-07-17",
+ "stars": 2413
+ },
+ {
+ "date": "2025-07-18",
+ "stars": 2443
+ },
+ {
+ "date": "2025-07-19",
+ "stars": 2461
+ },
+ {
+ "date": "2025-07-20",
+ "stars": 2474
+ },
+ {
+ "date": "2025-07-21",
+ "stars": 2479
+ },
+ {
+ "date": "2025-07-22",
+ "stars": 2485
+ },
+ {
+ "date": "2025-07-23",
+ "stars": 2492
+ },
+ {
+ "date": "2025-07-24",
+ "stars": 2505
+ },
+ {
+ "date": "2025-07-25",
+ "stars": 2509
+ },
+ {
+ "date": "2025-07-26",
+ "stars": 2519
+ },
+ {
+ "date": "2025-07-27",
+ "stars": 2524
+ },
+ {
+ "date": "2025-07-28",
+ "stars": 2529
+ },
+ {
+ "date": "2025-07-29",
+ "stars": 2536
+ },
+ {
+ "date": "2025-07-30",
+ "stars": 2543
+ },
+ {
+ "date": "2025-07-31",
+ "stars": 2554
+ },
+ {
+ "date": "2025-08-01",
+ "stars": 2565
+ },
+ {
+ "date": "2025-08-02",
+ "stars": 2572
+ },
+ {
+ "date": "2025-08-03",
+ "stars": 2582
+ },
+ {
+ "date": "2025-08-04",
+ "stars": 2592
+ },
+ {
+ "date": "2025-08-05",
+ "stars": 2595
+ },
+ {
+ "date": "2025-08-06",
+ "stars": 2602
+ },
+ {
+ "date": "2025-08-07",
+ "stars": 2611
+ },
+ {
+ "date": "2025-08-08",
+ "stars": 2613
+ },
+ {
+ "date": "2025-08-09",
+ "stars": 2618
+ },
+ {
+ "date": "2025-08-10",
+ "stars": 2626
+ },
+ {
+ "date": "2025-08-11",
+ "stars": 2641
+ },
+ {
+ "date": "2025-08-12",
+ "stars": 2661
+ },
+ {
+ "date": "2025-08-13",
+ "stars": 2669
+ },
+ {
+ "date": "2025-08-14",
+ "stars": 2681
+ },
+ {
+ "date": "2025-08-15",
+ "stars": 2685
+ },
+ {
+ "date": "2025-08-16",
+ "stars": 2689
+ },
+ {
+ "date": "2025-08-17",
+ "stars": 2698
+ },
+ {
+ "date": "2025-08-18",
+ "stars": 2706
+ },
+ {
+ "date": "2025-08-19",
+ "stars": 2711
+ },
+ {
+ "date": "2025-08-20",
+ "stars": 2718
+ },
+ {
+ "date": "2025-08-21",
+ "stars": 2723
+ },
+ {
+ "date": "2025-08-22",
+ "stars": 2729
+ },
+ {
+ "date": "2025-08-23",
+ "stars": 2736
+ },
+ {
+ "date": "2025-08-24",
+ "stars": 2743
+ },
+ {
+ "date": "2025-08-25",
+ "stars": 2756
+ },
+ {
+ "date": "2025-08-26",
+ "stars": 2774
+ },
+ {
+ "date": "2025-08-27",
+ "stars": 2796
+ },
+ {
+ "date": "2025-08-28",
+ "stars": 2824
+ },
+ {
+ "date": "2025-08-29",
+ "stars": 2830
+ },
+ {
+ "date": "2025-08-30",
+ "stars": 2834
+ },
+ {
+ "date": "2025-08-31",
+ "stars": 2843
+ },
+ {
+ "date": "2025-09-01",
+ "stars": 2855
+ },
+ {
+ "date": "2025-09-02",
+ "stars": 2863
+ },
+ {
+ "date": "2025-09-03",
+ "stars": 2871
+ },
+ {
+ "date": "2025-09-04",
+ "stars": 2877
+ },
+ {
+ "date": "2025-09-05",
+ "stars": 2881
+ },
+ {
+ "date": "2025-09-06",
+ "stars": 2887
+ },
+ {
+ "date": "2025-09-07",
+ "stars": 2890
+ },
+ {
+ "date": "2025-09-08",
+ "stars": 2897
+ },
+ {
+ "date": "2025-09-09",
+ "stars": 2905
+ },
+ {
+ "date": "2025-09-10",
+ "stars": 2907
+ },
+ {
+ "date": "2025-09-11",
+ "stars": 2912
+ },
+ {
+ "date": "2025-09-12",
+ "stars": 2916
+ },
+ {
+ "date": "2025-09-13",
+ "stars": 2920
+ },
+ {
+ "date": "2025-09-14",
+ "stars": 2927
+ },
+ {
+ "date": "2025-09-15",
+ "stars": 2930
+ },
+ {
+ "date": "2025-09-16",
+ "stars": 2949
+ },
+ {
+ "date": "2025-09-17",
+ "stars": 2960
+ },
+ {
+ "date": "2025-09-18",
+ "stars": 2969
+ },
+ {
+ "date": "2025-09-19",
+ "stars": 2978
+ },
+ {
+ "date": "2025-09-20",
+ "stars": 2980
+ },
+ {
+ "date": "2025-09-21",
+ "stars": 2985
+ },
+ {
+ "date": "2025-09-22",
+ "stars": 2991
+ },
+ {
+ "date": "2025-09-23",
+ "stars": 2992
+ },
+ {
+ "date": "2025-09-24",
+ "stars": 3000
+ },
+ {
+ "date": "2025-09-25",
+ "stars": 3005
+ },
+ {
+ "date": "2025-09-26",
+ "stars": 3010
+ },
+ {
+ "date": "2025-09-27",
+ "stars": 3014
+ },
+ {
+ "date": "2025-09-28",
+ "stars": 3020
+ },
+ {
+ "date": "2025-09-29",
+ "stars": 3031
+ },
+ {
+ "date": "2025-09-30",
+ "stars": 3035
+ },
+ {
+ "date": "2025-10-01",
+ "stars": 3038
+ },
+ {
+ "date": "2025-10-02",
+ "stars": 3042
+ },
+ {
+ "date": "2025-10-03",
+ "stars": 3045
+ },
+ {
+ "date": "2025-10-04",
+ "stars": 3049
+ },
+ {
+ "date": "2025-10-05",
+ "stars": 3061
+ },
+ {
+ "date": "2025-10-06",
+ "stars": 3066
+ },
+ {
+ "date": "2025-10-07",
+ "stars": 3069
+ },
+ {
+ "date": "2025-10-08",
+ "stars": 3071
+ },
+ {
+ "date": "2025-10-09",
+ "stars": 3076
+ },
+ {
+ "date": "2025-10-10",
+ "stars": 3084
+ },
+ {
+ "date": "2025-10-11",
+ "stars": 3087
+ },
+ {
+ "date": "2025-10-12",
+ "stars": 3091
+ },
+ {
+ "date": "2025-10-13",
+ "stars": 3093
+ },
+ {
+ "date": "2025-10-14",
+ "stars": 3106
+ },
+ {
+ "date": "2025-10-15",
+ "stars": 3110
+ },
+ {
+ "date": "2025-10-16",
+ "stars": 3113
+ },
+ {
+ "date": "2025-10-17",
+ "stars": 3120
+ },
+ {
+ "date": "2025-10-18",
+ "stars": 3126
+ },
+ {
+ "date": "2025-10-19",
+ "stars": 3137
+ },
+ {
+ "date": "2025-10-20",
+ "stars": 3140
+ },
+ {
+ "date": "2025-10-21",
+ "stars": 3145
+ },
+ {
+ "date": "2025-10-22",
+ "stars": 3147
+ },
+ {
+ "date": "2025-10-23",
+ "stars": 3152
+ },
+ {
+ "date": "2025-10-24",
+ "stars": 3159
+ },
+ {
+ "date": "2025-10-25",
+ "stars": 3165
+ },
+ {
+ "date": "2025-10-26",
+ "stars": 3168
+ },
+ {
+ "date": "2025-10-27",
+ "stars": 3169
+ },
+ {
+ "date": "2025-10-28",
+ "stars": 3178
+ },
+ {
+ "date": "2025-10-29",
+ "stars": 3181
+ },
+ {
+ "date": "2025-10-30",
+ "stars": 3186
+ },
+ {
+ "date": "2025-10-31",
+ "stars": 3194
+ },
+ {
+ "date": "2025-11-01",
+ "stars": 3202
+ },
+ {
+ "date": "2025-11-02",
+ "stars": 3208
+ },
+ {
+ "date": "2025-11-03",
+ "stars": 3214
+ },
+ {
+ "date": "2025-11-04",
+ "stars": 3222
+ },
+ {
+ "date": "2025-11-05",
+ "stars": 3226
+ },
+ {
+ "date": "2025-11-06",
+ "stars": 3236
+ },
+ {
+ "date": "2025-11-07",
+ "stars": 3241
+ },
+ {
+ "date": "2025-11-08",
+ "stars": 3244
+ },
+ {
+ "date": "2025-11-09",
+ "stars": 3247
+ },
+ {
+ "date": "2025-11-10",
+ "stars": 3260
+ },
+ {
+ "date": "2025-11-11",
+ "stars": 3266
+ },
+ {
+ "date": "2025-11-12",
+ "stars": 3269
+ },
+ {
+ "date": "2025-11-13",
+ "stars": 3285
+ },
+ {
+ "date": "2025-11-14",
+ "stars": 3301
+ },
+ {
+ "date": "2025-11-15",
+ "stars": 3309
+ },
+ {
+ "date": "2025-11-16",
+ "stars": 3318
+ },
+ {
+ "date": "2025-11-17",
+ "stars": 3326
+ },
+ {
+ "date": "2025-11-18",
+ "stars": 3333
+ },
+ {
+ "date": "2025-11-19",
+ "stars": 3340
+ },
+ {
+ "date": "2025-11-20",
+ "stars": 3349
+ },
+ {
+ "date": "2025-11-21",
+ "stars": 3356
+ },
+ {
+ "date": "2025-11-22",
+ "stars": 3361
+ },
+ {
+ "date": "2025-11-23",
+ "stars": 3368
+ },
+ {
+ "date": "2025-11-24",
+ "stars": 3372
+ },
+ {
+ "date": "2025-11-25",
+ "stars": 3381
+ },
+ {
+ "date": "2025-11-26",
+ "stars": 3387
+ },
+ {
+ "date": "2025-11-27",
+ "stars": 3396
+ },
+ {
+ "date": "2025-11-28",
+ "stars": 3407
+ },
+ {
+ "date": "2025-11-29",
+ "stars": 3415
+ },
+ {
+ "date": "2025-11-30",
+ "stars": 3421
+ },
+ {
+ "date": "2025-12-01",
+ "stars": 3430
+ },
+ {
+ "date": "2025-12-02",
+ "stars": 3436
+ },
+ {
+ "date": "2025-12-03",
+ "stars": 3447
+ },
+ {
+ "date": "2025-12-04",
+ "stars": 3459
+ },
+ {
+ "date": "2025-12-05",
+ "stars": 3476
+ },
+ {
+ "date": "2025-12-06",
+ "stars": 3484
+ },
+ {
+ "date": "2025-12-07",
+ "stars": 3489
+ },
+ {
+ "date": "2025-12-08",
+ "stars": 3498
+ },
+ {
+ "date": "2025-12-09",
+ "stars": 3503
+ },
+ {
+ "date": "2025-12-10",
+ "stars": 3516
+ },
+ {
+ "date": "2025-12-11",
+ "stars": 3529
+ },
+ {
+ "date": "2025-12-12",
+ "stars": 3533
+ },
+ {
+ "date": "2025-12-13",
+ "stars": 3541
+ },
+ {
+ "date": "2025-12-14",
+ "stars": 3547
+ },
+ {
+ "date": "2025-12-15",
+ "stars": 3559
+ },
+ {
+ "date": "2025-12-16",
+ "stars": 3566
+ },
+ {
+ "date": "2025-12-17",
+ "stars": 3577
+ },
+ {
+ "date": "2025-12-18",
+ "stars": 3585
+ },
+ {
+ "date": "2025-12-19",
+ "stars": 3590
+ },
+ {
+ "date": "2025-12-20",
+ "stars": 3603
+ },
+ {
+ "date": "2025-12-21",
+ "stars": 3608
+ },
+ {
+ "date": "2025-12-22",
+ "stars": 3611
+ },
+ {
+ "date": "2025-12-23",
+ "stars": 3621
+ },
+ {
+ "date": "2025-12-24",
+ "stars": 3623
+ },
+ {
+ "date": "2025-12-25",
+ "stars": 3636
+ },
+ {
+ "date": "2025-12-26",
+ "stars": 3642
+ },
+ {
+ "date": "2025-12-27",
+ "stars": 3646
+ },
+ {
+ "date": "2025-12-28",
+ "stars": 3655
+ },
+ {
+ "date": "2025-12-29",
+ "stars": 3664
+ },
+ {
+ "date": "2025-12-30",
+ "stars": 3673
+ },
+ {
+ "date": "2025-12-31",
+ "stars": 3681
+ },
+ {
+ "date": "2026-01-01",
+ "stars": 3708
+ },
+ {
+ "date": "2026-01-02",
+ "stars": 3718
+ },
+ {
+ "date": "2026-01-03",
+ "stars": 3729
+ },
+ {
+ "date": "2026-01-04",
+ "stars": 3739
+ },
+ {
+ "date": "2026-01-05",
+ "stars": 3748
+ },
+ {
+ "date": "2026-01-06",
+ "stars": 3755
+ },
+ {
+ "date": "2026-01-07",
+ "stars": 3774
+ },
+ {
+ "date": "2026-01-08",
+ "stars": 3783
+ },
+ {
+ "date": "2026-01-09",
+ "stars": 3796
+ },
+ {
+ "date": "2026-01-10",
+ "stars": 3810
+ },
+ {
+ "date": "2026-01-11",
+ "stars": 3824
+ },
+ {
+ "date": "2026-01-12",
+ "stars": 3843
+ },
+ {
+ "date": "2026-01-13",
+ "stars": 3852
+ },
+ {
+ "date": "2026-01-14",
+ "stars": 3862
+ },
+ {
+ "date": "2026-01-15",
+ "stars": 3873
+ },
+ {
+ "date": "2026-01-16",
+ "stars": 3892
+ },
+ {
+ "date": "2026-01-17",
+ "stars": 3906
+ },
+ {
+ "date": "2026-01-18",
+ "stars": 3911
+ },
+ {
+ "date": "2026-01-19",
+ "stars": 3923
+ },
+ {
+ "date": "2026-01-20",
+ "stars": 3930
+ },
+ {
+ "date": "2026-01-21",
+ "stars": 3937
+ },
+ {
+ "date": "2026-01-22",
+ "stars": 3939
+ },
+ {
+ "date": "2026-01-23",
+ "stars": 3949
+ },
+ {
+ "date": "2026-01-24",
+ "stars": 3954
+ },
+ {
+ "date": "2026-01-25",
+ "stars": 3964
+ },
+ {
+ "date": "2026-01-26",
+ "stars": 3985
+ },
+ {
+ "date": "2026-01-27",
+ "stars": 3996
+ },
+ {
+ "date": "2026-01-28",
+ "stars": 4006
+ },
+ {
+ "date": "2026-01-29",
+ "stars": 4024
+ },
+ {
+ "date": "2026-01-30",
+ "stars": 4033
+ },
+ {
+ "date": "2026-01-31",
+ "stars": 4045
+ },
+ {
+ "date": "2026-02-01",
+ "stars": 4055
+ },
+ {
+ "date": "2026-02-02",
+ "stars": 4068
+ },
+ {
+ "date": "2026-02-03",
+ "stars": 4086
+ },
+ {
+ "date": "2026-02-04",
+ "stars": 4095
+ },
+ {
+ "date": "2026-02-05",
+ "stars": 4110
+ },
+ {
+ "date": "2026-02-06",
+ "stars": 4131
+ },
+ {
+ "date": "2026-02-07",
+ "stars": 4144
+ },
+ {
+ "date": "2026-02-08",
+ "stars": 4163
+ },
+ {
+ "date": "2026-02-09",
+ "stars": 4172
+ },
+ {
+ "date": "2026-02-10",
+ "stars": 4189
+ },
+ {
+ "date": "2026-02-11",
+ "stars": 4205
+ },
+ {
+ "date": "2026-02-12",
+ "stars": 4227
+ },
+ {
+ "date": "2026-02-13",
+ "stars": 4237
+ },
+ {
+ "date": "2026-02-14",
+ "stars": 4248
+ },
+ {
+ "date": "2026-02-15",
+ "stars": 4268
+ },
+ {
+ "date": "2026-02-16",
+ "stars": 4278
+ },
+ {
+ "date": "2026-02-17",
+ "stars": 4284
+ },
+ {
+ "date": "2026-02-18",
+ "stars": 4286
+ },
+ {
+ "date": "2026-02-19",
+ "stars": 4295
+ },
+ {
+ "date": "2026-02-20",
+ "stars": 4301
+ },
+ {
+ "date": "2026-02-21",
+ "stars": 4306
+ },
+ {
+ "date": "2026-02-22",
+ "stars": 4312
+ },
+ {
+ "date": "2026-02-23",
+ "stars": 4326
+ },
+ {
+ "date": "2026-02-24",
+ "stars": 4334
+ },
+ {
+ "date": "2026-02-25",
+ "stars": 4348
+ },
+ {
+ "date": "2026-02-26",
+ "stars": 4357
+ },
+ {
+ "date": "2026-02-27",
+ "stars": 4371
+ },
+ {
+ "date": "2026-02-28",
+ "stars": 4385
+ },
+ {
+ "date": "2026-03-01",
+ "stars": 4391
+ },
+ {
+ "date": "2026-03-02",
+ "stars": 4399
+ },
+ {
+ "date": "2026-03-03",
+ "stars": 4405
+ },
+ {
+ "date": "2026-03-04",
+ "stars": 4413
+ },
+ {
+ "date": "2026-03-05",
+ "stars": 4426
+ },
+ {
+ "date": "2026-03-06",
+ "stars": 4435
+ },
+ {
+ "date": "2026-03-07",
+ "stars": 4447
+ },
+ {
+ "date": "2026-03-08",
+ "stars": 4451
+ },
+ {
+ "date": "2026-03-09",
+ "stars": 4466
+ },
+ {
+ "date": "2026-03-10",
+ "stars": 4477
+ },
+ {
+ "date": "2026-03-11",
+ "stars": 4485
+ },
+ {
+ "date": "2026-03-12",
+ "stars": 4490
+ },
+ {
+ "date": "2026-03-13",
+ "stars": 4494
+ },
+ {
+ "date": "2026-03-14",
+ "stars": 4496
+ },
+ {
+ "date": "2026-03-15",
+ "stars": 4502
+ },
+ {
+ "date": "2026-03-16",
+ "stars": 4508
+ },
+ {
+ "date": "2026-03-17",
+ "stars": 4517
+ },
+ {
+ "date": "2026-03-18",
+ "stars": 4530
+ },
+ {
+ "date": "2026-03-19",
+ "stars": 4539
+ },
+ {
+ "date": "2026-03-20",
+ "stars": 4551
+ },
+ {
+ "date": "2026-03-21",
+ "stars": 4557
+ },
+ {
+ "date": "2026-03-22",
+ "stars": 4568
+ },
+ {
+ "date": "2026-03-23",
+ "stars": 4577
+ },
+ {
+ "date": "2026-03-24",
+ "stars": 4586
+ },
+ {
+ "date": "2026-03-25",
+ "stars": 4600
+ },
+ {
+ "date": "2026-03-26",
+ "stars": 4611
+ },
+ {
+ "date": "2026-03-27",
+ "stars": 4622
+ },
+ {
+ "date": "2026-03-28",
+ "stars": 4626
+ },
+ {
+ "date": "2026-03-29",
+ "stars": 4633
+ },
+ {
+ "date": "2026-03-30",
+ "stars": 4640
+ },
+ {
+ "date": "2026-03-31",
+ "stars": 4646
+ },
+ {
+ "date": "2026-04-01",
+ "stars": 4650
+ },
+ {
+ "date": "2026-04-02",
+ "stars": 4655
+ },
+ {
+ "date": "2026-04-03",
+ "stars": 4670
+ },
+ {
+ "date": "2026-04-04",
+ "stars": 4678
+ },
+ {
+ "date": "2026-04-05",
+ "stars": 4685
+ },
+ {
+ "date": "2026-04-06",
+ "stars": 4690
+ },
+ {
+ "date": "2026-04-07",
+ "stars": 4697
+ },
+ {
+ "date": "2026-04-08",
+ "stars": 4705
+ },
+ {
+ "date": "2026-04-09",
+ "stars": 4711
+ },
+ {
+ "date": "2026-04-10",
+ "stars": 4718
+ },
+ {
+ "date": "2026-04-11",
+ "stars": 4731
+ },
+ {
+ "date": "2026-04-12",
+ "stars": 4741
+ },
+ {
+ "date": "2026-04-13",
+ "stars": 4749
+ },
+ {
+ "date": "2026-04-14",
+ "stars": 4757
+ },
+ {
+ "date": "2026-04-15",
+ "stars": 4764
+ },
+ {
+ "date": "2026-04-16",
+ "stars": 4769
+ },
+ {
+ "date": "2026-04-17",
+ "stars": 4772
+ },
+ {
+ "date": "2026-04-18",
+ "stars": 4782
+ },
+ {
+ "date": "2026-04-19",
+ "stars": 4785
+ },
+ {
+ "date": "2026-04-20",
+ "stars": 4794
+ },
+ {
+ "date": "2026-04-21",
+ "stars": 4804
+ },
+ {
+ "date": "2026-04-22",
+ "stars": 4808
+ },
+ {
+ "date": "2026-04-23",
+ "stars": 4817
+ },
+ {
+ "date": "2026-04-24",
+ "stars": 4827
+ },
+ {
+ "date": "2026-04-25",
+ "stars": 4837
+ },
+ {
+ "date": "2026-04-26",
+ "stars": 4849
+ },
+ {
+ "date": "2026-04-27",
+ "stars": 4859
+ },
+ {
+ "date": "2026-04-28",
+ "stars": 4861
+ },
+ {
+ "date": "2026-04-29",
+ "stars": 4881
+ },
+ {
+ "date": "2026-04-30",
+ "stars": 4889
+ },
+ {
+ "date": "2026-05-01",
+ "stars": 4897
+ },
+ {
+ "date": "2026-05-02",
+ "stars": 4907
+ },
+ {
+ "date": "2026-05-03",
+ "stars": 4917
+ },
+ {
+ "date": "2026-05-04",
+ "stars": 4932
+ },
+ {
+ "date": "2026-05-05",
+ "stars": 4944
+ },
+ {
+ "date": "2026-05-06",
+ "stars": 4954
+ },
+ {
+ "date": "2026-05-07",
+ "stars": 4963
+ },
+ {
+ "date": "2026-05-08",
+ "stars": 4973
+ },
+ {
+ "date": "2026-05-09",
+ "stars": 4988
+ },
+ {
+ "date": "2026-05-10",
+ "stars": 4997
+ },
+ {
+ "date": "2026-05-11",
+ "stars": 5016
+ },
+ {
+ "date": "2026-05-12",
+ "stars": 5024
+ },
+ {
+ "date": "2026-05-13",
+ "stars": 5030
+ },
+ {
+ "date": "2026-05-14",
+ "stars": 5035
+ },
+ {
+ "date": "2026-05-15",
+ "stars": 5040
+ },
+ {
+ "date": "2026-05-16",
+ "stars": 5045
+ },
+ {
+ "date": "2026-05-17",
+ "stars": 5050
+ },
+ {
+ "date": "2026-05-18",
+ "stars": 5062
+ },
+ {
+ "date": "2026-05-19",
+ "stars": 5066
+ },
+ {
+ "date": "2026-05-20",
+ "stars": 5072
+ },
+ {
+ "date": "2026-05-21",
+ "stars": 5079
+ },
+ {
+ "date": "2026-05-22",
+ "stars": 5089
+ },
+ {
+ "date": "2026-05-23",
+ "stars": 5100
+ },
+ {
+ "date": "2026-05-24",
+ "stars": 5107
+ },
+ {
+ "date": "2026-05-25",
+ "stars": 5119
+ },
+ {
+ "date": "2026-05-26",
+ "stars": 5127
+ },
+ {
+ "date": "2026-05-27",
+ "stars": 5136
+ },
+ {
+ "date": "2026-05-28",
+ "stars": 5147
+ },
+ {
+ "date": "2026-05-29",
+ "stars": 5155
+ },
+ {
+ "date": "2026-05-30",
+ "stars": 5164
+ },
+ {
+ "date": "2026-05-31",
+ "stars": 5175
+ },
+ {
+ "date": "2026-06-01",
+ "stars": 5185
+ },
+ {
+ "date": "2026-06-02",
+ "stars": 5194
+ },
+ {
+ "date": "2026-06-03",
+ "stars": 5199
+ },
+ {
+ "date": "2026-06-04",
+ "stars": 5210
+ },
+ {
+ "date": "2026-06-05",
+ "stars": 5217
+ },
+ {
+ "date": "2026-06-06",
+ "stars": 5227
+ },
+ {
+ "date": "2026-06-07",
+ "stars": 5231
+ },
+ {
+ "date": "2026-06-08",
+ "stars": 5236
+ },
+ {
+ "date": "2026-06-09",
+ "stars": 5246
+ },
+ {
+ "date": "2026-06-10",
+ "stars": 5252
+ },
+ {
+ "date": "2026-06-11",
+ "stars": 5261
+ },
+ {
+ "date": "2026-06-12",
+ "stars": 5270
+ },
+ {
+ "date": "2026-06-13",
+ "stars": 5278
+ },
+ {
+ "date": "2026-06-14",
+ "stars": 5284
+ },
+ {
+ "date": "2026-06-15",
+ "stars": 5294
+ },
+ {
+ "date": "2026-06-16",
+ "stars": 5304
+ },
+ {
+ "date": "2026-06-17",
+ "stars": 5312
+ },
+ {
+ "date": "2026-06-18",
+ "stars": 5316
+ },
+ {
+ "date": "2026-06-19",
+ "stars": 5322
+ },
+ {
+ "date": "2026-06-20",
+ "stars": 5329
+ },
+ {
+ "date": "2026-06-21",
+ "stars": 5333
+ },
+ {
+ "date": "2026-06-22",
+ "stars": 5346
+ },
+ {
+ "date": "2026-06-23",
+ "stars": 5358
+ },
+ {
+ "date": "2026-06-24",
+ "stars": 5375
+ },
+ {
+ "date": "2026-06-25",
+ "stars": 5381
+ },
+ {
+ "date": "2026-06-26",
+ "stars": 5386
+ },
+ {
+ "date": "2026-06-27",
+ "stars": 5396
+ },
+ {
+ "date": "2026-06-28",
+ "stars": 5402
+ },
+ {
+ "date": "2026-06-29",
+ "stars": 5404
+ },
+ {
+ "date": "2026-06-30",
+ "stars": 5414
+ },
+ {
+ "date": "2026-07-01",
+ "stars": 5422
+ },
+ {
+ "date": "2026-07-02",
+ "stars": 5436
+ },
+ {
+ "date": "2026-07-03",
+ "stars": 5441
+ },
+ {
+ "date": "2026-07-04",
+ "stars": 5452
+ },
+ {
+ "date": "2026-07-05",
+ "stars": 5460
+ },
+ {
+ "date": "2026-07-06",
+ "stars": 5471
+ },
+ {
+ "date": "2026-07-07",
+ "stars": 5480
+ },
+ {
+ "date": "2026-07-08",
+ "stars": 5496
+ },
+ {
+ "date": "2026-07-09",
+ "stars": 5502
+ },
+ {
+ "date": "2026-07-10",
+ "stars": 5513
+ },
+ {
+ "date": "2026-07-11",
+ "stars": 5523
+ },
+ {
+ "date": "2026-07-12",
+ "stars": 5539
+ },
+ {
+ "date": "2026-07-13",
+ "stars": 5567
+ },
+ {
+ "date": "2026-07-14",
+ "stars": 5593
+ },
+ {
+ "date": "2026-07-15",
+ "stars": 5623
+ },
+ {
+ "date": "2026-07-16",
+ "stars": 5641
+ },
+ {
+ "date": "2026-07-17",
+ "stars": 5648
+ },
+ {
+ "date": "2026-07-18",
+ "stars": 5663
+ },
+ {
+ "date": "2026-07-19",
+ "stars": 5704
+ },
+ {
+ "date": "2026-07-20",
+ "stars": 5714
+ },
+ {
+ "date": "2026-07-21",
+ "stars": 5725
+ },
+ {
+ "date": "2026-07-22",
+ "stars": 5738
+ },
+ {
+ "date": "2026-07-23",
+ "stars": 5758
+ },
+ {
+ "date": "2026-07-24",
+ "stars": 5870
+ },
+ {
+ "date": "2026-07-25",
+ "stars": 5891
+ },
+ {
+ "date": "2026-07-26",
+ "stars": 5911
+ },
+ {
+ "date": "2026-07-27",
+ "stars": 5942
+ }
+ ],
+ "generated_at": "2026-07-28T12:26:26Z",
+ "method": "current_stargazers_starred_at"
+ },
+ "repository": "MarSeventh/CloudFlare-ImgBed",
+ "snapshots": [
+ {
+ "at": "2026-07-28T12:29:23Z",
+ "stars": 5959
+ }
+ ]
+ },
+ {
+ "reconstruction": {
+ "daily": [
+ {
+ "date": "2024-07-19",
+ "stars": 1
+ },
+ {
+ "date": "2024-07-20",
+ "stars": 7
+ },
+ {
+ "date": "2024-07-21",
+ "stars": 10
+ },
+ {
+ "date": "2024-07-22",
+ "stars": 12
+ },
+ {
+ "date": "2024-07-23",
+ "stars": 13
+ },
+ {
+ "date": "2024-07-25",
+ "stars": 16
+ },
+ {
+ "date": "2024-07-26",
+ "stars": 17
+ },
+ {
+ "date": "2024-07-27",
+ "stars": 18
+ },
+ {
+ "date": "2024-07-31",
+ "stars": 20
+ },
+ {
+ "date": "2024-08-06",
+ "stars": 21
+ },
+ {
+ "date": "2024-08-11",
+ "stars": 23
+ },
+ {
+ "date": "2024-08-13",
+ "stars": 24
+ },
+ {
+ "date": "2024-08-14",
+ "stars": 25
+ },
+ {
+ "date": "2024-08-19",
+ "stars": 26
+ },
+ {
+ "date": "2024-08-20",
+ "stars": 28
+ },
+ {
+ "date": "2024-08-22",
+ "stars": 29
+ },
+ {
+ "date": "2024-08-23",
+ "stars": 30
+ },
+ {
+ "date": "2024-08-24",
+ "stars": 31
+ },
+ {
+ "date": "2024-08-29",
+ "stars": 34
+ },
+ {
+ "date": "2024-09-01",
+ "stars": 35
+ },
+ {
+ "date": "2024-09-02",
+ "stars": 36
+ },
+ {
+ "date": "2024-09-04",
+ "stars": 37
+ },
+ {
+ "date": "2024-09-05",
+ "stars": 39
+ },
+ {
+ "date": "2024-09-06",
+ "stars": 40
+ },
+ {
+ "date": "2024-09-07",
+ "stars": 42
+ },
+ {
+ "date": "2024-09-09",
+ "stars": 45
+ },
+ {
+ "date": "2024-09-12",
+ "stars": 47
+ },
+ {
+ "date": "2024-09-13",
+ "stars": 48
+ },
+ {
+ "date": "2024-09-21",
+ "stars": 49
+ },
+ {
+ "date": "2024-09-25",
+ "stars": 50
+ },
+ {
+ "date": "2024-09-26",
+ "stars": 51
+ },
+ {
+ "date": "2024-09-29",
+ "stars": 52
+ },
+ {
+ "date": "2024-10-01",
+ "stars": 54
+ },
+ {
+ "date": "2024-10-12",
+ "stars": 55
+ },
+ {
+ "date": "2024-10-19",
+ "stars": 56
+ },
+ {
+ "date": "2024-10-20",
+ "stars": 57
+ },
+ {
+ "date": "2024-10-22",
+ "stars": 58
+ },
+ {
+ "date": "2024-11-03",
+ "stars": 59
+ },
+ {
+ "date": "2024-11-04",
+ "stars": 60
+ },
+ {
+ "date": "2024-11-12",
+ "stars": 61
+ },
+ {
+ "date": "2024-11-15",
+ "stars": 63
+ },
+ {
+ "date": "2024-11-18",
+ "stars": 64
+ },
+ {
+ "date": "2024-11-19",
+ "stars": 66
+ },
+ {
+ "date": "2024-11-23",
+ "stars": 67
+ },
+ {
+ "date": "2024-11-28",
+ "stars": 68
+ },
+ {
+ "date": "2024-11-30",
+ "stars": 69
+ },
+ {
+ "date": "2024-12-09",
+ "stars": 70
+ },
+ {
+ "date": "2024-12-10",
+ "stars": 72
+ },
+ {
+ "date": "2024-12-13",
+ "stars": 74
+ },
+ {
+ "date": "2024-12-19",
+ "stars": 75
+ },
+ {
+ "date": "2024-12-20",
+ "stars": 76
+ },
+ {
+ "date": "2024-12-26",
+ "stars": 77
+ },
+ {
+ "date": "2024-12-27",
+ "stars": 78
+ },
+ {
+ "date": "2024-12-29",
+ "stars": 79
+ },
+ {
+ "date": "2025-01-01",
+ "stars": 80
+ },
+ {
+ "date": "2025-01-07",
+ "stars": 81
+ },
+ {
+ "date": "2025-01-09",
+ "stars": 82
+ },
+ {
+ "date": "2025-01-13",
+ "stars": 83
+ },
+ {
+ "date": "2025-01-18",
+ "stars": 84
+ },
+ {
+ "date": "2025-01-28",
+ "stars": 86
+ },
+ {
+ "date": "2025-01-29",
+ "stars": 87
+ },
+ {
+ "date": "2025-02-04",
+ "stars": 88
+ },
+ {
+ "date": "2025-02-09",
+ "stars": 89
+ },
+ {
+ "date": "2025-02-11",
+ "stars": 91
+ },
+ {
+ "date": "2025-02-12",
+ "stars": 92
+ },
+ {
+ "date": "2025-02-13",
+ "stars": 93
+ },
+ {
+ "date": "2025-02-19",
+ "stars": 94
+ },
+ {
+ "date": "2025-02-22",
+ "stars": 95
+ },
+ {
+ "date": "2025-03-02",
+ "stars": 96
+ },
+ {
+ "date": "2025-03-06",
+ "stars": 97
+ },
+ {
+ "date": "2025-03-15",
+ "stars": 100
+ },
+ {
+ "date": "2025-03-18",
+ "stars": 101
+ },
+ {
+ "date": "2025-03-27",
+ "stars": 102
+ },
+ {
+ "date": "2025-03-29",
+ "stars": 103
+ },
+ {
+ "date": "2025-04-02",
+ "stars": 104
+ },
+ {
+ "date": "2025-04-03",
+ "stars": 106
+ },
+ {
+ "date": "2025-04-07",
+ "stars": 107
+ },
+ {
+ "date": "2025-04-12",
+ "stars": 110
+ },
+ {
+ "date": "2025-04-15",
+ "stars": 111
+ },
+ {
+ "date": "2025-04-20",
+ "stars": 112
+ },
+ {
+ "date": "2025-04-21",
+ "stars": 113
+ },
+ {
+ "date": "2025-04-23",
+ "stars": 114
+ },
+ {
+ "date": "2025-04-24",
+ "stars": 115
+ },
+ {
+ "date": "2025-04-25",
+ "stars": 116
+ },
+ {
+ "date": "2025-04-27",
+ "stars": 117
+ },
+ {
+ "date": "2025-05-01",
+ "stars": 118
+ },
+ {
+ "date": "2025-05-04",
+ "stars": 119
+ },
+ {
+ "date": "2025-05-13",
+ "stars": 120
+ },
+ {
+ "date": "2025-05-17",
+ "stars": 121
+ },
+ {
+ "date": "2025-06-09",
+ "stars": 122
+ },
+ {
+ "date": "2025-06-10",
+ "stars": 123
+ },
+ {
+ "date": "2025-06-14",
+ "stars": 124
+ },
+ {
+ "date": "2025-07-01",
+ "stars": 125
+ },
+ {
+ "date": "2025-07-04",
+ "stars": 126
+ },
+ {
+ "date": "2025-07-05",
+ "stars": 127
+ },
+ {
+ "date": "2025-07-12",
+ "stars": 128
+ },
+ {
+ "date": "2025-07-16",
+ "stars": 129
+ },
+ {
+ "date": "2025-07-18",
+ "stars": 131
+ },
+ {
+ "date": "2025-07-23",
+ "stars": 132
+ },
+ {
+ "date": "2025-07-24",
+ "stars": 133
+ },
+ {
+ "date": "2025-07-28",
+ "stars": 134
+ },
+ {
+ "date": "2025-08-02",
+ "stars": 135
+ },
+ {
+ "date": "2025-08-05",
+ "stars": 136
+ },
+ {
+ "date": "2025-08-20",
+ "stars": 137
+ },
+ {
+ "date": "2025-08-28",
+ "stars": 139
+ },
+ {
+ "date": "2025-08-31",
+ "stars": 140
+ },
+ {
+ "date": "2025-09-01",
+ "stars": 141
+ },
+ {
+ "date": "2025-09-04",
+ "stars": 142
+ },
+ {
+ "date": "2025-09-07",
+ "stars": 143
+ },
+ {
+ "date": "2025-09-19",
+ "stars": 144
+ },
+ {
+ "date": "2025-10-01",
+ "stars": 145
+ },
+ {
+ "date": "2025-10-14",
+ "stars": 146
+ },
+ {
+ "date": "2025-11-07",
+ "stars": 147
+ },
+ {
+ "date": "2025-11-10",
+ "stars": 148
+ },
+ {
+ "date": "2025-11-11",
+ "stars": 150
+ },
+ {
+ "date": "2025-11-20",
+ "stars": 151
+ },
+ {
+ "date": "2025-12-08",
+ "stars": 153
+ },
+ {
+ "date": "2025-12-29",
+ "stars": 154
+ },
+ {
+ "date": "2026-01-01",
+ "stars": 155
+ },
+ {
+ "date": "2026-01-04",
+ "stars": 156
+ },
+ {
+ "date": "2026-01-07",
+ "stars": 157
+ },
+ {
+ "date": "2026-01-08",
+ "stars": 158
+ },
+ {
+ "date": "2026-01-09",
+ "stars": 159
+ },
+ {
+ "date": "2026-01-17",
+ "stars": 160
+ },
+ {
+ "date": "2026-01-19",
+ "stars": 161
+ },
+ {
+ "date": "2026-01-20",
+ "stars": 162
+ },
+ {
+ "date": "2026-01-25",
+ "stars": 163
+ },
+ {
+ "date": "2026-02-03",
+ "stars": 164
+ },
+ {
+ "date": "2026-02-07",
+ "stars": 165
+ },
+ {
+ "date": "2026-02-09",
+ "stars": 166
+ },
+ {
+ "date": "2026-02-12",
+ "stars": 167
+ },
+ {
+ "date": "2026-02-14",
+ "stars": 168
+ },
+ {
+ "date": "2026-02-22",
+ "stars": 169
+ },
+ {
+ "date": "2026-02-24",
+ "stars": 170
+ },
+ {
+ "date": "2026-02-25",
+ "stars": 171
+ },
+ {
+ "date": "2026-02-27",
+ "stars": 172
+ },
+ {
+ "date": "2026-02-28",
+ "stars": 173
+ },
+ {
+ "date": "2026-03-07",
+ "stars": 174
+ },
+ {
+ "date": "2026-03-21",
+ "stars": 175
+ },
+ {
+ "date": "2026-03-25",
+ "stars": 176
+ },
+ {
+ "date": "2026-04-05",
+ "stars": 177
+ },
+ {
+ "date": "2026-04-11",
+ "stars": 178
+ },
+ {
+ "date": "2026-04-12",
+ "stars": 179
+ },
+ {
+ "date": "2026-04-14",
+ "stars": 180
+ },
+ {
+ "date": "2026-04-17",
+ "stars": 181
+ },
+ {
+ "date": "2026-04-20",
+ "stars": 182
+ },
+ {
+ "date": "2026-04-29",
+ "stars": 183
+ },
+ {
+ "date": "2026-05-05",
+ "stars": 184
+ },
+ {
+ "date": "2026-05-20",
+ "stars": 185
+ },
+ {
+ "date": "2026-05-24",
+ "stars": 186
+ },
+ {
+ "date": "2026-05-28",
+ "stars": 187
+ },
+ {
+ "date": "2026-05-31",
+ "stars": 188
+ },
+ {
+ "date": "2026-06-12",
+ "stars": 189
+ },
+ {
+ "date": "2026-06-13",
+ "stars": 190
+ },
+ {
+ "date": "2026-06-14",
+ "stars": 191
+ },
+ {
+ "date": "2026-07-02",
+ "stars": 192
+ },
+ {
+ "date": "2026-07-07",
+ "stars": 193
+ },
+ {
+ "date": "2026-07-11",
+ "stars": 194
+ },
+ {
+ "date": "2026-07-12",
+ "stars": 195
+ },
+ {
+ "date": "2026-07-15",
+ "stars": 196
+ },
+ {
+ "date": "2026-07-20",
+ "stars": 197
+ },
+ {
+ "date": "2026-07-22",
+ "stars": 198
+ },
+ {
+ "date": "2026-07-23",
+ "stars": 199
+ }
+ ],
+ "generated_at": "2026-07-28T12:26:26Z",
+ "method": "current_stargazers_starred_at"
+ },
+ "repository": "MarSeventh/Sanyue-ImgHub",
+ "snapshots": [
+ {
+ "at": "2026-07-28T12:29:23Z",
+ "stars": 200
+ }
+ ]
+ }
+ ],
+ "timezone": "UTC"
+}
diff --git a/.github/star-history/star_history.py b/.github/star-history/star_history.py
new file mode 100644
index 00000000..55b0d200
--- /dev/null
+++ b/.github/star-history/star_history.py
@@ -0,0 +1,840 @@
+#!/usr/bin/env python3
+"""Maintain and render Star History without a hosted chart service.
+
+The three subcommands form one pipeline:
+
+* ``refresh-data`` extends the public date/count cache with one GitHub
+ repository-metadata request per configured repository;
+* ``patch-upstream`` seeds a pinned official backend from that local cache;
+* ``render`` validates and saves the official localhost-rendered SVG pair.
+
+The official JSDOM, XYChart, xkcd styling, theme logic, and SVGO rendering path
+remain unchanged.
+"""
+
+from __future__ import annotations
+
+import argparse
+import base64
+import copy
+import json
+import os
+import re
+import sys
+import tempfile
+import time
+import urllib.error
+import urllib.parse
+import urllib.request
+import xml.etree.ElementTree as ET
+from datetime import datetime, timedelta, timezone
+from pathlib import Path
+from typing import Any, Dict, List, Optional, Tuple
+
+
+DEFAULT_BACKEND_URL = "http://127.0.0.1:8080"
+SCHEMA_VERSION = 2
+DATA_FORMAT = "star-history-pages"
+THEMES = ("light", "dark")
+REPOSITORY_PATTERN = re.compile(
+ r"^[A-Za-z0-9](?:[A-Za-z0-9_.-]*[A-Za-z0-9])?/"
+ r"[A-Za-z0-9](?:[A-Za-z0-9_.-]*[A-Za-z0-9])?$"
+)
+USER_AGENT = "deploy-star-history-pages local updater"
+MAX_JSON_BYTES = 5_000_000
+MAX_SVG_BYTES = 5_000_000
+
+# One retry covers a short runner-local startup/readiness race. The backend is
+# seeded from local data and therefore performs no stargazers API fan-out.
+LOCAL_RENDER_ATTEMPTS = 2
+# The fallback fetches the last-deployed pair from this project's Pages CDN,
+# where retries usefully ride out short-lived network hiccups.
+FALLBACK_RENDER_ATTEMPTS = 4
+
+MAIN_PATH = Path("backend/main.ts")
+
+STARTUP_ORIGINAL = """const startServer = async () => {
+ await initTokenFromEnv();
+ initOgAssets();
+ const repoStore = loadRepos();
+
+ const app = new Hono();"""
+
+STARTUP_PATCHED = """const startServer = async () => {
+ const seedPath = process.env.STAR_HISTORY_DATA_PATH;
+ if (!seedPath) {
+ await initTokenFromEnv();
+ }
+ initOgAssets();
+ const repoStore = loadRepos();
+
+ if (seedPath) {
+ const fs = await import("node:fs");
+ const seed = JSON.parse(fs.readFileSync(seedPath, "utf8"));
+ const series = seed.series;
+ if (!Array.isArray(series) || series.length === 0) {
+ throw new Error("Invalid STAR_HISTORY_DATA_PATH payload");
+ }
+ for (const item of series) {
+ const repository = String(item.repository || "").toLowerCase();
+ const starRecords = item.star_records;
+ const logoUrl = String(item.logo_url || "");
+ if (!repository || !Array.isArray(starRecords) || starRecords.length === 0 || !logoUrl) {
+ throw new Error("Invalid STAR_HISTORY_DATA_PATH series");
+ }
+ cache.set(repository, {
+ starRecords,
+ starAmount: starRecords[starRecords.length - 1].count,
+ logoUrl,
+ });
+ logger.info(`Loaded ${starRecords.length} local Star History records for ${repository}`);
+ }
+ }
+
+ const app = new Hono();"""
+
+
+# ---------------------------------------------------------------------------
+# Shared command-line interface
+# ---------------------------------------------------------------------------
+
+
+def build_parser() -> argparse.ArgumentParser:
+ parser = argparse.ArgumentParser(description=__doc__)
+ commands = parser.add_subparsers(dest="command", required=True)
+
+ refresh = commands.add_parser(
+ "refresh-data",
+ help="Update the local date/count cache from GitHub repository metadata",
+ )
+ refresh.add_argument(
+ "--repository",
+ default=os.environ.get("STAR_HISTORY_REPOSITORIES"),
+ required="STAR_HISTORY_REPOSITORIES" not in os.environ,
+ help="Comma-separated GitHub repositories in owner/name form",
+ )
+ refresh.add_argument("--seed-file", type=Path, required=True)
+ refresh.add_argument("--deployed-url")
+ refresh.add_argument("--output", type=Path, required=True)
+ refresh.add_argument(
+ "--token-env",
+ default="GITHUB_TOKEN",
+ help="Environment variable containing a GitHub token",
+ )
+
+ patch_parser = commands.add_parser(
+ "patch-upstream",
+ help="Patch the pinned official backend to load the local cache",
+ )
+ patch_parser.add_argument(
+ "--source-dir",
+ type=Path,
+ required=True,
+ help="Path to the checked-out star-history/star-history source",
+ )
+
+ render = commands.add_parser(
+ "render",
+ help="Save light and dark SVGs produced by the local official backend",
+ )
+ render.add_argument(
+ "--backend-url",
+ default=os.environ.get("STAR_HISTORY_BACKEND_URL", DEFAULT_BACKEND_URL),
+ help="Local official backend origin (default: %(default)s)",
+ )
+ render.add_argument(
+ "--repository",
+ default=os.environ.get("STAR_HISTORY_REPOSITORIES"),
+ required="STAR_HISTORY_REPOSITORIES" not in os.environ,
+ help="Comma-separated GitHub repositories in owner/name form",
+ )
+ render.add_argument(
+ "--output-dir",
+ type=Path,
+ default=Path(".star-history-site"),
+ help="Directory that receives the two SVG files (default: %(default)s)",
+ )
+ render.add_argument(
+ "--fallback-base-url",
+ default=os.environ.get("STAR_HISTORY_FALLBACK_BASE_URL"),
+ help=(
+ "Base URL containing this project's last deployed light/dark SVG pair"
+ ),
+ )
+ return parser
+
+
+# ---------------------------------------------------------------------------
+# History data cache
+# ---------------------------------------------------------------------------
+
+
+def parse_record_date(value: str) -> datetime:
+ return datetime.strptime(value, "%Y/%m/%d %H:%M:%S").replace(
+ tzinfo=timezone.utc
+ )
+
+
+def format_record_date(value: datetime) -> str:
+ current = value.astimezone(timezone.utc)
+ return "{0}/{1}/{2} {3}:{4}:{5}".format(
+ current.year,
+ current.month,
+ current.day,
+ current.hour,
+ current.minute,
+ current.second,
+ )
+
+
+def validate_repository(value: str) -> str:
+ repository = value.strip().lower()
+ if not REPOSITORY_PATTERN.fullmatch(repository):
+ raise ValueError("repository must use GitHub owner/name format")
+ return repository
+
+
+def validate_repositories(value: str) -> Tuple[str, ...]:
+ repositories = tuple(validate_repository(item) for item in value.split(","))
+ if not repositories or len(set(repositories)) != len(repositories):
+ raise ValueError("repositories must be a non-empty unique list")
+ return repositories
+
+
+def _validate_runtime_series(payload: Any, repository: str) -> Dict[str, Any]:
+ if not isinstance(payload, dict):
+ raise ValueError("Star History series must be a JSON object")
+ if str(payload.get("repository", "")).lower() != repository.lower():
+ raise ValueError("Star History series targets a different repository")
+ logo_url = payload.get("logo_url")
+ if not isinstance(logo_url, str) or not logo_url.startswith("data:image/"):
+ raise ValueError("Star History series is missing an embedded logo")
+
+ records = payload.get("star_records")
+ if not isinstance(records, list) or len(records) < 2:
+ raise ValueError("Star History data requires at least two records")
+ previous_date: Optional[datetime] = None
+ for record in records:
+ if not isinstance(record, dict):
+ raise ValueError("Star History record must be an object")
+ date_value = record.get("date")
+ if not isinstance(date_value, str):
+ raise ValueError("Star History record date must be a string")
+ date = parse_record_date(date_value)
+ count = record.get("count")
+ if isinstance(count, bool) or not isinstance(count, int) or count < 0:
+ raise ValueError("Star History record count must be a non-negative integer")
+ if previous_date is not None and date <= previous_date:
+ raise ValueError("Star History records must be strictly chronological")
+ previous_date = date
+ return payload
+
+
+def validate_data(
+ payload: Any, repositories: Tuple[str, ...]
+) -> Dict[str, Any]:
+ if not isinstance(payload, dict):
+ raise ValueError("Star History data must be a JSON object")
+ if payload.get("format") != DATA_FORMAT:
+ raise ValueError("unsupported Star History data format")
+ if payload.get("schema_version") != SCHEMA_VERSION:
+ raise ValueError("unsupported Star History data schema")
+ series = payload.get("series")
+ if not isinstance(series, list) or len(series) != len(repositories):
+ raise ValueError("Star History data must contain every repository")
+ for index, repository in enumerate(repositories):
+ _validate_runtime_series(series[index], repository)
+ updated_at = payload.get("updated_at")
+ if not isinstance(updated_at, str):
+ raise ValueError("Star History data is missing updated_at")
+ datetime.fromisoformat(updated_at.replace("Z", "+00:00"))
+ return payload
+
+
+def validate_legacy_seed(
+ payload: Any, repositories: Tuple[str, ...]
+) -> Dict[str, Any]:
+ if not isinstance(payload, dict) or payload.get("schema_version") != 2:
+ raise ValueError("legacy Star History seed has an unsupported schema")
+ series = payload.get("series")
+ if not isinstance(series, list) or len(series) != len(repositories):
+ raise ValueError("legacy Star History seed must contain every repository")
+ for index, repository in enumerate(repositories):
+ item = series[index]
+ if not isinstance(item, dict) or str(item.get("repository", "")).lower() != repository:
+ raise ValueError("legacy Star History repository order is invalid")
+ reconstruction = item.get("reconstruction")
+ snapshots = item.get("snapshots")
+ if not isinstance(reconstruction, dict) or not isinstance(snapshots, list):
+ raise ValueError("legacy Star History series is malformed")
+ daily = reconstruction.get("daily")
+ if not isinstance(daily, list):
+ raise ValueError("legacy Star History reconstruction is malformed")
+ for point in daily:
+ datetime.fromisoformat(point["date"])
+ count = point.get("stars")
+ if isinstance(count, bool) or not isinstance(count, int) or count < 0:
+ raise ValueError("legacy Star History count is invalid")
+ for snapshot in snapshots:
+ datetime.fromisoformat(str(snapshot["at"]).replace("Z", "+00:00"))
+ count = snapshot.get("stars")
+ if isinstance(count, bool) or not isinstance(count, int) or count < 0:
+ raise ValueError("legacy Star History snapshot is invalid")
+ return payload
+
+
+def read_json_file(path: Path) -> Dict[str, Any]:
+ return json.loads(path.read_text(encoding="utf-8"))
+
+
+def download_json(url: str) -> Dict[str, Any]:
+ parsed = urllib.parse.urlsplit(url)
+ if parsed.scheme != "https" or not parsed.netloc:
+ raise ValueError("deployed data URL must use HTTPS")
+ request = urllib.request.Request(
+ url,
+ headers={"User-Agent": USER_AGENT},
+ )
+ with urllib.request.urlopen(request, timeout=30) as response:
+ body = response.read(MAX_JSON_BYTES + 1)
+ if len(body) > MAX_JSON_BYTES:
+ raise ValueError("deployed Star History data is unexpectedly large")
+ return json.loads(body.decode("utf-8"))
+
+
+def load_best_data(
+ *,
+ repositories: Tuple[str, ...],
+ seed_file: Path,
+ deployed_url: Optional[str],
+) -> Tuple[Dict[str, Any], str]:
+ if deployed_url:
+ try:
+ deployed = validate_data(download_json(deployed_url), repositories)
+ return deployed, "deployed Pages data"
+ except urllib.error.HTTPError as exc:
+ try:
+ status = exc.code
+ finally:
+ exc.close()
+ if status != 404:
+ raise RuntimeError(
+ "deployed Pages cache request failed; preserving the last "
+ "deployment: HTTP {0}".format(status)
+ ) from exc
+ print("[data] deployed cache not found; using repository seed")
+ except (OSError, ValueError, json.JSONDecodeError, urllib.error.URLError) as exc:
+ raise RuntimeError(
+ "deployed Pages cache is unavailable or invalid; preserving "
+ "the last deployment: {0}".format(exc)
+ ) from exc
+ seed = read_json_file(seed_file)
+ try:
+ return validate_data(seed, repositories), "repository seed"
+ except ValueError:
+ return validate_legacy_seed(seed, repositories), "legacy repository seed"
+
+
+def fetch_repository_metadata(repository: str, token: str) -> Dict[str, Any]:
+ url = "https://api.github.com/repos/{0}".format(repository)
+ request = urllib.request.Request(
+ url,
+ headers={
+ "Accept": "application/vnd.github+json",
+ "Authorization": "Bearer {0}".format(token),
+ "User-Agent": USER_AGENT,
+ "X-GitHub-Api-Version": "2022-11-28",
+ },
+ )
+ last_error: Optional[Exception] = None
+ for attempt in range(1, 4):
+ try:
+ with urllib.request.urlopen(request, timeout=30) as response:
+ payload = json.loads(response.read().decode("utf-8"))
+ count = payload.get("stargazers_count")
+ if isinstance(count, bool) or not isinstance(count, int) or count < 0:
+ raise ValueError("GitHub metadata omitted stargazers_count")
+ created_at = payload.get("created_at")
+ owner = payload.get("owner")
+ avatar_url = owner.get("avatar_url") if isinstance(owner, dict) else None
+ if not isinstance(created_at, str) or not created_at:
+ raise ValueError("GitHub metadata omitted created_at")
+ if not isinstance(avatar_url, str) or not avatar_url:
+ raise ValueError("GitHub metadata omitted owner.avatar_url")
+ return {
+ "star_count": count,
+ "created_at": created_at,
+ "avatar_url": avatar_url,
+ }
+ except (OSError, ValueError, json.JSONDecodeError, urllib.error.URLError) as exc:
+ last_error = exc
+ if attempt < 3:
+ time.sleep(attempt * 2)
+ raise RuntimeError("GitHub repository metadata request failed: {0}".format(last_error))
+
+
+def download_avatar_data_url(url: str) -> str:
+ parsed = urllib.parse.urlsplit(url)
+ if parsed.scheme != "https" or parsed.hostname != "avatars.githubusercontent.com":
+ raise ValueError("GitHub metadata returned an unexpected avatar URL")
+ request = urllib.request.Request(
+ url + ("&" if parsed.query else "?") + "size=22",
+ headers={"User-Agent": USER_AGENT},
+ )
+ with urllib.request.urlopen(request, timeout=30) as response:
+ content_type = response.headers.get_content_type()
+ body = response.read(2_000_001)
+ if not content_type.startswith("image/") or len(body) > 2_000_000:
+ raise ValueError("GitHub avatar response is not a supported image")
+ return "data:{0};base64,{1}".format(
+ content_type,
+ base64.b64encode(body).decode("ascii"),
+ )
+
+
+def update_current_record(
+ payload: Dict[str, Any],
+ *,
+ repository: str,
+ star_count: int,
+ current_time: datetime,
+) -> Tuple[Dict[str, Any], bool]:
+ updated = copy.deepcopy(payload)
+ _validate_runtime_series(updated, repository)
+ current = current_time.astimezone(timezone.utc)
+ record = {"date": format_record_date(current), "count": star_count}
+ records = updated["star_records"]
+ latest = parse_record_date(records[-1]["date"])
+ if current < latest:
+ raise ValueError("current time predates the latest Star History record")
+ if latest.date() == current.date():
+ if records[-1]["count"] == star_count:
+ return updated, False
+ records[-1] = record
+ else:
+ records.append(record)
+ _validate_runtime_series(updated, repository)
+ return updated, True
+
+
+def migrate_legacy_seed(
+ payload: Dict[str, Any],
+ *,
+ repositories: Tuple[str, ...],
+ metadata: Dict[str, Dict[str, Any]],
+ current_time: datetime,
+) -> Dict[str, Any]:
+ validate_legacy_seed(payload, repositories)
+ avatar_cache: Dict[str, str] = {}
+ runtime_series: List[Dict[str, Any]] = []
+ for index, repository in enumerate(repositories):
+ legacy = payload["series"][index]
+ records_by_time: Dict[datetime, int] = {}
+ for point in legacy["reconstruction"]["daily"]:
+ point_day = datetime.fromisoformat(point["date"]).date()
+ end_of_day = datetime(
+ point_day.year,
+ point_day.month,
+ point_day.day,
+ tzinfo=timezone.utc,
+ ) + timedelta(days=1)
+ records_by_time[end_of_day] = point["stars"]
+ for snapshot in legacy["snapshots"]:
+ at = datetime.fromisoformat(snapshot["at"].replace("Z", "+00:00"))
+ records_by_time[at.astimezone(timezone.utc)] = snapshot["stars"]
+ records = [
+ {"date": format_record_date(at), "count": records_by_time[at]}
+ for at in sorted(records_by_time)
+ ]
+ avatar_url = metadata[repository]["avatar_url"]
+ if avatar_url not in avatar_cache:
+ avatar_cache[avatar_url] = download_avatar_data_url(avatar_url)
+ series = {
+ "repository": repository,
+ "logo_url": avatar_cache[avatar_url],
+ "star_records": records,
+ }
+ series, _ = update_current_record(
+ series,
+ repository=repository,
+ star_count=metadata[repository]["star_count"],
+ current_time=current_time,
+ )
+ runtime_series.append(series)
+ migrated = {
+ "format": DATA_FORMAT,
+ "schema_version": SCHEMA_VERSION,
+ "updated_at": current_time.isoformat().replace("+00:00", "Z"),
+ "series": runtime_series,
+ }
+ return validate_data(migrated, repositories)
+
+
+def atomic_write_json(path: Path, payload: Dict[str, Any]) -> None:
+ path.parent.mkdir(parents=True, exist_ok=True)
+ content = json.dumps(payload, ensure_ascii=False, indent=2) + "\n"
+ with tempfile.NamedTemporaryFile(
+ mode="w",
+ encoding="utf-8",
+ prefix=path.name + ".",
+ suffix=".tmp",
+ dir=path.parent,
+ delete=False,
+ ) as temporary:
+ temporary.write(content)
+ temporary_path = Path(temporary.name)
+ temporary_path.replace(path)
+ path.chmod(0o644)
+
+
+def refresh_data_command(args: argparse.Namespace) -> int:
+ try:
+ repositories = validate_repositories(args.repository)
+ except ValueError as exc:
+ print("[error] {0}".format(exc))
+ return 1
+ token = os.environ.get(args.token_env, "").strip()
+ if not token:
+ print("[error] GitHub token environment variable is empty")
+ return 1
+ try:
+ payload, source = load_best_data(
+ repositories=repositories,
+ seed_file=args.seed_file,
+ deployed_url=args.deployed_url,
+ )
+ metadata = {
+ repository: fetch_repository_metadata(repository, token)
+ for repository in repositories
+ }
+ current_time = datetime.now(timezone.utc)
+ if payload.get("format") != DATA_FORMAT:
+ updated = migrate_legacy_seed(
+ payload,
+ repositories=repositories,
+ metadata=metadata,
+ current_time=current_time,
+ )
+ source = "legacy repository seed"
+ else:
+ updated = copy.deepcopy(payload)
+ changed = False
+ for index, repository in enumerate(repositories):
+ series, series_changed = update_current_record(
+ updated["series"][index],
+ repository=repository,
+ star_count=metadata[repository]["star_count"],
+ current_time=current_time,
+ )
+ updated["series"][index] = series
+ changed = changed or series_changed
+ if changed:
+ updated["updated_at"] = current_time.isoformat().replace("+00:00", "Z")
+ validate_data(updated, repositories)
+ atomic_write_json(args.output, updated)
+ except (
+ KeyError,
+ TypeError,
+ OSError,
+ RuntimeError,
+ ValueError,
+ json.JSONDecodeError,
+ ) as exc:
+ print("[error] Star History data refresh failed: {0}".format(exc))
+ return 1
+ for index, repository in enumerate(repositories):
+ print(
+ "[data] {0}: {1} stars from {2} ({3} records)".format(
+ repository,
+ metadata[repository]["star_count"],
+ source,
+ len(updated["series"][index]["star_records"]),
+ )
+ )
+ return 0
+
+
+# ---------------------------------------------------------------------------
+# Guarded official-backend patch
+# ---------------------------------------------------------------------------
+
+
+def replace_exactly_once(
+ path: Path,
+ original: str,
+ replacement: str,
+ description: str,
+) -> bool:
+ """Replace one pinned-upstream fragment and reject silent source drift."""
+ source = path.read_text(encoding="utf-8")
+ original_count = source.count(original)
+ replacement_count = source.count(replacement)
+
+ if original_count == 0 and replacement_count == 1:
+ return False
+ if original_count != 1 or replacement_count != 0:
+ raise RuntimeError(
+ "unexpected upstream implementation for {0} in {1} "
+ "(original={2}, patched={3})".format(
+ description,
+ path,
+ original_count,
+ replacement_count,
+ )
+ )
+
+ path.write_text(source.replace(original, replacement), encoding="utf-8")
+ return True
+
+
+def patch_upstream(source_dir: Path) -> List[Path]:
+ """Patch the pinned checkout and return the files changed in this call."""
+ path = source_dir / MAIN_PATH
+ if not path.is_file():
+ raise RuntimeError("missing pinned upstream file: {0}".format(path))
+ changed = replace_exactly_once(
+ path,
+ STARTUP_ORIGINAL,
+ STARTUP_PATCHED,
+ "local data cache seed",
+ )
+ return [MAIN_PATH] if changed else []
+
+
+def patch_upstream_command(args: argparse.Namespace) -> int:
+ try:
+ changed = patch_upstream(args.source_dir)
+ except (OSError, RuntimeError) as exc:
+ print("[error] Star History upstream patch failed: {0}".format(exc))
+ return 1
+
+ if changed:
+ for path in changed:
+ print("[patched] {0}".format(path))
+ else:
+ print("[current] Star History upstream patch is already applied")
+ return 0
+
+
+# ---------------------------------------------------------------------------
+# Official localhost SVG rendering and pair fallback
+# ---------------------------------------------------------------------------
+
+
+def validate_local_backend_url(value: str) -> str:
+ parsed = urllib.parse.urlsplit(value.rstrip("/"))
+ if parsed.scheme != "http" or parsed.hostname not in {"127.0.0.1", "localhost"}:
+ raise ValueError("backend URL must point to a local HTTP server")
+ if parsed.path not in {"", "/"} or parsed.query or parsed.fragment:
+ raise ValueError("backend URL must be an origin without a path or query")
+ return value.rstrip("/")
+
+
+def validate_fallback_base_url(value: str) -> str:
+ normalized = value.rstrip("/")
+ parsed = urllib.parse.urlsplit(normalized)
+ is_local_http = parsed.scheme == "http" and parsed.hostname in {
+ "127.0.0.1",
+ "localhost",
+ }
+ if parsed.scheme != "https" and not is_local_http:
+ raise ValueError("fallback URL must use HTTPS or local HTTP")
+ if not parsed.netloc or parsed.username or parsed.password:
+ raise ValueError("fallback URL must contain a host without credentials")
+ if parsed.query or parsed.fragment:
+ raise ValueError("fallback URL must not contain a query or fragment")
+ return normalized
+
+
+def chart_url(
+ backend_url: str, repositories: Tuple[str, ...], theme: str
+) -> str:
+ params = {
+ "repos": ",".join(repositories),
+ "type": "date",
+ "legend": "top-left",
+ }
+ if theme == "dark":
+ params["theme"] = "dark"
+ return "{0}/svg?{1}".format(backend_url, urllib.parse.urlencode(params))
+
+
+def fallback_chart_url(base_url: str, theme: str) -> str:
+ return "{0}/star-history-{1}.svg".format(base_url, theme)
+
+
+def emit_github_warning(message: str) -> None:
+ """Surface a degraded refresh as an Actions annotation."""
+ if os.environ.get("GITHUB_ACTIONS") != "true":
+ return
+ single_line = message.replace("\r", " ").replace("\n", " ")
+ print("::warning title=Star History::{0}".format(single_line))
+
+
+def download_svg(url: str, attempts: int = 4) -> bytes:
+ request = urllib.request.Request(
+ url,
+ headers={
+ "Accept": "image/svg+xml",
+ "User-Agent": "deploy-star-history-pages local renderer",
+ },
+ )
+ last_error: Optional[Exception] = None
+ for attempt in range(1, attempts + 1):
+ try:
+ with urllib.request.urlopen(request, timeout=120) as response:
+ content_type = response.headers.get("Content-Type", "").lower()
+ body = response.read(MAX_SVG_BYTES + 1)
+ if "svg" not in content_type:
+ raise ValueError(
+ "unexpected Content-Type: {0}".format(
+ content_type or "(missing)"
+ )
+ )
+ if len(body) > MAX_SVG_BYTES:
+ raise ValueError("SVG response is unexpectedly large")
+ return body
+ except urllib.error.HTTPError as exc:
+ try:
+ response_body = exc.read().decode("utf-8", errors="replace").strip()
+ finally:
+ exc.close()
+ detail = "HTTP {0} {1}".format(exc.code, exc.reason)
+ if response_body:
+ detail += ": {0}".format(response_body)
+ last_error = RuntimeError(detail)
+ if attempt == attempts:
+ break
+ time.sleep(attempt * 2)
+ except (OSError, ValueError, urllib.error.URLError) as exc:
+ last_error = exc
+ if attempt == attempts:
+ break
+ time.sleep(attempt * 2)
+ raise RuntimeError("local Star History backend request failed: {0}".format(last_error))
+
+
+def validate_svg(
+ content: bytes, repositories: Tuple[str, ...], theme: str
+) -> None:
+ if len(content) < 10_000:
+ raise ValueError("SVG is unexpectedly small: {0} bytes".format(len(content)))
+ root = ET.fromstring(content)
+ if root.tag.rsplit("}", 1)[-1] != "svg":
+ raise ValueError("document root is not SVG: {0}".format(root.tag))
+ if root.attrib.get("width") != "800" or root.attrib.get("height") != "533.333":
+ raise ValueError(
+ "official laptop chart dimensions changed: {0}x{1}".format(
+ root.attrib.get("width"),
+ root.attrib.get("height"),
+ )
+ )
+
+ text = content.decode("utf-8")
+ required_fragments = (
+ "Star History",
+ "GitHub Stars",
+ "xkcdify",
+ "font-family:xkcd",
+ )
+ required_fragments += repositories
+ missing = [fragment for fragment in required_fragments if fragment not in text]
+ if missing:
+ raise ValueError(
+ "SVG is missing official chart markers: {0}".format(", ".join(missing))
+ )
+
+ expected_background = "background:#0d1117" if theme == "dark" else "background:#fff"
+ if expected_background not in text:
+ raise ValueError("SVG does not contain the expected {0} theme".format(theme))
+
+
+def atomic_write_svg(destination: Path, content: bytes) -> None:
+ destination.parent.mkdir(parents=True, exist_ok=True)
+ with tempfile.NamedTemporaryFile(
+ mode="wb",
+ prefix=destination.name + ".",
+ suffix=".tmp",
+ dir=destination.parent,
+ delete=False,
+ ) as temporary:
+ temporary.write(content)
+ temporary_path = Path(temporary.name)
+ temporary_path.replace(destination)
+ destination.chmod(0o644)
+ print("[rendered] {0} ({1} bytes)".format(destination, len(content)))
+
+
+def fetch_chart_pair(
+ *,
+ repositories: Tuple[str, ...],
+ url_for_theme,
+ attempts: int,
+) -> Dict[str, bytes]:
+ charts = {}
+ for theme in THEMES:
+ content = download_svg(url_for_theme(theme), attempts=attempts)
+ validate_svg(content, repositories, theme)
+ charts[theme] = content
+ return charts
+
+
+def render_command(args: argparse.Namespace) -> int:
+ try:
+ backend_url = validate_local_backend_url(args.backend_url)
+ repositories = validate_repositories(args.repository)
+
+ try:
+ charts = fetch_chart_pair(
+ repositories=repositories,
+ url_for_theme=lambda theme: chart_url(
+ backend_url,
+ repositories,
+ theme,
+ ),
+ attempts=LOCAL_RENDER_ATTEMPTS,
+ )
+ except (RuntimeError, OSError, ValueError, ET.ParseError) as local_error:
+ if not args.fallback_base_url:
+ raise
+ fallback_base_url = validate_fallback_base_url(args.fallback_base_url)
+ warning = (
+ "Local Star History refresh failed; reusing the last deployed "
+ "chart pair: {0}".format(local_error)
+ )
+ print("[warning] {0}".format(warning), file=sys.stderr)
+ emit_github_warning(warning)
+ charts = fetch_chart_pair(
+ repositories=repositories,
+ url_for_theme=lambda theme: fallback_chart_url(
+ fallback_base_url,
+ theme,
+ ),
+ attempts=FALLBACK_RENDER_ATTEMPTS,
+ )
+
+ # Write only after both themes validate, preventing a mixed fresh/stale pair.
+ for theme in THEMES:
+ atomic_write_svg(
+ args.output_dir / "star-history-{0}.svg".format(theme),
+ charts[theme],
+ )
+ except (RuntimeError, OSError, ValueError, ET.ParseError) as exc:
+ print("[error] Star History rendering failed: {0}".format(exc), file=sys.stderr)
+ return 1
+ return 0
+
+
+def main(argv: Optional[List[str]] = None) -> int:
+ args = build_parser().parse_args(argv)
+ if args.command == "refresh-data":
+ return refresh_data_command(args)
+ if args.command == "patch-upstream":
+ return patch_upstream_command(args)
+ if args.command == "render":
+ return render_command(args)
+ raise RuntimeError("unknown Star History command: {0}".format(args.command))
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/.github/workflows/sync-star-history.yml b/.github/workflows/sync-star-history.yml
new file mode 100644
index 00000000..46001bc3
--- /dev/null
+++ b/.github/workflows/sync-star-history.yml
@@ -0,0 +1,230 @@
+# Render with pinned official source and persist date/count history in Pages.
+# The workflow deploys Pages artifacts only; it never creates repository commits.
+name: Render Star History
+
+on:
+ schedule:
+ - cron: "17 3 * * 1"
+ workflow_dispatch:
+
+permissions:
+ contents: read
+ deployments: write
+ pages: write
+ id-token: write
+
+concurrency:
+ group: star-history-pages
+ cancel-in-progress: true
+
+env:
+ STAR_HISTORY_REPOSITORIES: marseventh/cloudflare-imgbed,marseventh/sanyue-imghub
+ STAR_HISTORY_SOURCE_REF: bcddc9d532b10bac7e0187a741288bf9cab17616
+ STAR_HISTORY_SOURCE_DIR: .star-history-upstream
+ STAR_HISTORY_SITE_DIR: .star-history-site
+ STAR_HISTORY_PAGES_URL: https://marseventh.github.io/CloudFlare-ImgBed
+
+jobs:
+ render-and-deploy:
+ name: Render with official source and deploy to Pages
+ runs-on: ubuntu-latest
+ timeout-minutes: 15
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
+ steps:
+ - name: Check out this repository
+ uses: actions/checkout@v7
+ with:
+ persist-credentials: false
+
+ - name: Check out official Star History source
+ uses: actions/checkout@v7
+ with:
+ repository: star-history/star-history
+ ref: ${{ env.STAR_HISTORY_SOURCE_REF }}
+ path: ${{ env.STAR_HISTORY_SOURCE_DIR }}
+ persist-credentials: false
+
+ - name: Set up pnpm
+ uses: pnpm/action-setup@v6
+ with:
+ version: 9.15.9
+ run_install: false
+
+ - name: Set up Node.js
+ uses: actions/setup-node@v7
+ with:
+ node-version: "24"
+ cache: pnpm
+ cache-dependency-path: |
+ .star-history-upstream/pnpm-lock.yaml
+ .star-history-upstream/backend/pnpm-lock.yaml
+
+ - name: Install official renderer dependencies
+ shell: bash
+ run: |
+ set -euo pipefail
+ (cd "${STAR_HISTORY_SOURCE_DIR}" && pnpm install --frozen-lockfile)
+ (cd "${STAR_HISTORY_SOURCE_DIR}/backend" && pnpm install --frozen-lockfile)
+
+ - name: Refresh data and render official light/dark charts locally
+ id: render
+ shell: bash
+ env:
+ # One repository-metadata request per configured repository reads the
+ # current totals. Individual
+ # stargazers and the hosted Star History chart service are not queried.
+ GITHUB_JOB_TOKEN: ${{ github.token }}
+ run: |
+ set -euo pipefail
+
+ if [[ -z "${GITHUB_JOB_TOKEN}" ]]; then
+ echo "::error::The workflow GITHUB_TOKEN is unavailable."
+ exit 1
+ fi
+
+ data_file="${RUNNER_TEMP}/star-history-data.json"
+ server_log="${RUNNER_TEMP}/star-history-backend.log"
+
+ cleanup() {
+ if [[ -n "${server_pid:-}" ]]; then
+ kill "${server_pid}" 2>/dev/null || true
+ wait "${server_pid}" 2>/dev/null || true
+ fi
+ }
+ trap cleanup EXIT
+
+ # Pages JSON is the runtime source of truth. The committed bootstrap
+ # seed is used only when the first deployment path returns HTTP 404.
+ python3 .github/star-history/star_history.py refresh-data \
+ --repository "${STAR_HISTORY_REPOSITORIES}" \
+ --seed-file .github/star-history/history.json \
+ --deployed-url "${STAR_HISTORY_PAGES_URL}/star-history-data.json" \
+ --output "${data_file}" \
+ --token-env GITHUB_JOB_TOKEN
+
+ # Seed the pinned official backend cache while leaving its JSDOM,
+ # XYChart, xkcd styling, theme handling, and SVGO renderer unchanged.
+ python3 .github/star-history/star_history.py patch-upstream \
+ --source-dir "${STAR_HISTORY_SOURCE_DIR}"
+ (cd "${STAR_HISTORY_SOURCE_DIR}/backend" && pnpm run build)
+
+ (
+ cd "${STAR_HISTORY_SOURCE_DIR}/backend"
+ STAR_HISTORY_DATA_PATH="${data_file}" pnpm exec tsx main.ts
+ ) >"${server_log}" 2>&1 &
+ server_pid=$!
+
+ for attempt in {1..60}; do
+ if curl --fail --silent http://127.0.0.1:8080/healthz >/dev/null; then
+ break
+ fi
+ if ! kill -0 "${server_pid}" 2>/dev/null; then
+ cat "${server_log}" >&2
+ exit 1
+ fi
+ if [[ "${attempt}" == 60 ]]; then
+ cat "${server_log}" >&2
+ exit 1
+ fi
+ sleep 1
+ done
+
+ mkdir -p "${STAR_HISTORY_SITE_DIR}"
+ cp "${data_file}" "${STAR_HISTORY_SITE_DIR}/star-history-data.json"
+ if ! python3 .github/star-history/star_history.py render \
+ --repository "${STAR_HISTORY_REPOSITORIES}" \
+ --output-dir "${STAR_HISTORY_SITE_DIR}" \
+ --fallback-base-url "${STAR_HISTORY_PAGES_URL}"; then
+ cat "${server_log}" >&2
+ exit 1
+ fi
+ touch "${STAR_HISTORY_SITE_DIR}/.nojekyll"
+
+ (
+ cd "${STAR_HISTORY_SITE_DIR}"
+ sha256sum \
+ star-history-data.json \
+ star-history-dark.svg \
+ star-history-light.svg | LC_ALL=C sort > manifest.sha256
+ )
+
+ # A byte-stable same-day count skips the Pages deployment entirely.
+ current_dir="$(mktemp -d)"
+ changed=true
+ if curl --fail --silent --location \
+ "${STAR_HISTORY_PAGES_URL}/manifest.sha256" \
+ --output "${current_dir}/manifest.sha256" && \
+ cmp --silent \
+ "${STAR_HISTORY_SITE_DIR}/manifest.sha256" \
+ "${current_dir}/manifest.sha256"; then
+ changed=false
+ echo "The deployed Pages site is already current."
+ fi
+ echo "changed=${changed}" >> "${GITHUB_OUTPUT}"
+
+ - name: Configure GitHub Pages
+ if: steps.render.outputs.changed == 'true'
+ uses: actions/configure-pages@v6
+
+ - name: Upload Pages artifact
+ if: steps.render.outputs.changed == 'true'
+ uses: actions/upload-pages-artifact@v5
+ with:
+ path: ${{ env.STAR_HISTORY_SITE_DIR }}
+
+ - name: Deploy Pages artifact
+ if: steps.render.outputs.changed == 'true'
+ id: deployment
+ uses: actions/deploy-pages@v5
+
+ # Deployment records are not Git commits. Keep only the newest record so
+ # the repository's Deployments list does not grow every scheduled run.
+ - name: Delete old GitHub Pages deployment records
+ if: ${{ success() && steps.render.outcome == 'success' }}
+ uses: actions/github-script@v9
+ with:
+ github-token: ${{ github.token }}
+ script: |
+ const { owner, repo } = context.repo;
+ const environment = "github-pages";
+ const deployments = await github.paginate(
+ github.rest.repos.listDeployments,
+ { owner, repo, environment, per_page: 100 },
+ );
+
+ deployments.sort((left, right) => {
+ const difference = Date.parse(right.created_at) - Date.parse(left.created_at);
+ return difference || right.id - left.id;
+ });
+ if (deployments.length <= 1) {
+ core.info(`Keeping ${deployments.length} ${environment} deployment record(s).`);
+ return;
+ }
+
+ const [currentDeployment, ...oldDeployments] = deployments;
+ for (const oldDeployment of oldDeployments) {
+ const { data: statuses } = await github.rest.repos.listDeploymentStatuses({
+ owner,
+ repo,
+ deployment_id: oldDeployment.id,
+ per_page: 1,
+ });
+ if (statuses[0]?.state !== "inactive") {
+ await github.rest.repos.createDeploymentStatus({
+ owner,
+ repo,
+ deployment_id: oldDeployment.id,
+ state: "inactive",
+ environment,
+ auto_inactive: false,
+ description: `Superseded by deployment ${currentDeployment.id}.`,
+ });
+ }
+ await github.rest.repos.deleteDeployment({
+ owner,
+ repo,
+ deployment_id: oldDeployment.id,
+ });
+ }
diff --git a/README.md b/README.md
index 481cea44..dadd7fd9 100644
--- a/README.md
+++ b/README.md
@@ -167,11 +167,11 @@ Thank you to everyone who has contributed code, documentation, ideas, and feedba
**If you find the project useful, please consider giving it a Star ⭐. Thank you for your support!**
-
+
-
-
-
+
+
+
diff --git a/README_zh.md b/README_zh.md
index 257ee74e..246f729c 100644
--- a/README_zh.md
+++ b/README_zh.md
@@ -167,11 +167,11 @@ CloudFlare ImgBed 是支持 Docker 与 Serverless 部署的自建图床和文件
**如果这个项目对您有所帮助,欢迎点亮一个 Star ⭐,感谢您的支持!**
-
+
-
-
-
+
+
+