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!** - + - - - Star History Chart + + + CloudFlare-ImgBed and Sanyue-ImgHub Star History Chart 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 ⭐,感谢您的支持!** - + - - - Star History Chart + + + CloudFlare-ImgBed 和 Sanyue-ImgHub Star 趋势图