vomebook commited on
Commit
d20d6b0
ยท
1 Parent(s): 6695264

Upload 3 files

Browse files
Files changed (3) hide show
  1. static/app.js +9 -1
  2. static/fetch_and_parse.py +366 -0
  3. static/index.html +1 -0
static/app.js CHANGED
@@ -146,6 +146,7 @@ function cacheDOM() {
146
  DOM.folderDeselectAll = $("#folder-deselect-all");
147
  DOM.extSelectAll = $("#ext-select-all");
148
  DOM.extDeselectAll = $("#ext-deselect-all");
 
149
  }
150
 
151
  /* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
@@ -285,6 +286,7 @@ const ROUTER = {
285
  renderSidebar();
286
  renderFilters();
287
  doSearch();
 
288
  },
289
  };
290
 
@@ -857,6 +859,12 @@ async function init() {
857
  DOM.hamburgerBtn.addEventListener("click", toggleLeftSidebar);
858
  DOM.settingsBtn.addEventListener("click", toggleRightSidebar);
859
  DOM.closeFiltersBtn.addEventListener("click", () => { STATE.rightSidebarOpen = false; updateSidebarVisibility(); });
 
 
 
 
 
 
860
  DOM.themeBtn.addEventListener("click", toggleTheme);
861
  DOM.mobileToggleBtn.addEventListener("click", toggleMobile);
862
  DOM.clearFiltersBtn.addEventListener("click", clearAllFilters);
@@ -885,7 +893,7 @@ async function init() {
885
  setupQuickScroll();
886
  setupKeyboard();
887
  setupResultDelegation();
888
-
889
  window.addEventListener("popstate", () => ROUTER.apply());
890
  window.addEventListener("resize", () => { if (!localStorage.getItem("mobileMode")) { const wm = STATE.isMobile; STATE.isMobile = autoDetectMobile(); if (wm !== STATE.isMobile) applyMobileMode(); } });
891
 
 
146
  DOM.folderDeselectAll = $("#folder-deselect-all");
147
  DOM.extSelectAll = $("#ext-select-all");
148
  DOM.extDeselectAll = $("#ext-deselect-all");
149
+ DOM.sidebarExpandBtn = $("#sidebar-expand-btn");
150
  }
151
 
152
  /* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
 
286
  renderSidebar();
287
  renderFilters();
288
  doSearch();
289
+ DOM.sidebarExpandBtn.style.display = STATE.mode === "repo" ? "" : "none";
290
  },
291
  };
292
 
 
859
  DOM.hamburgerBtn.addEventListener("click", toggleLeftSidebar);
860
  DOM.settingsBtn.addEventListener("click", toggleRightSidebar);
861
  DOM.closeFiltersBtn.addEventListener("click", () => { STATE.rightSidebarOpen = false; updateSidebarVisibility(); });
862
+
863
+ DOM.sidebarExpandBtn.addEventListener("click", () => {
864
+ DOM.leftSidebar.classList.toggle("expanded-wide");
865
+ DOM.sidebarExpandBtn.textContent = DOM.leftSidebar.classList.contains("expanded-wide") ? "โ†’" : "โ†”";
866
+ });
867
+
868
  DOM.themeBtn.addEventListener("click", toggleTheme);
869
  DOM.mobileToggleBtn.addEventListener("click", toggleMobile);
870
  DOM.clearFiltersBtn.addEventListener("click", clearAllFilters);
 
893
  setupQuickScroll();
894
  setupKeyboard();
895
  setupResultDelegation();
896
+
897
  window.addEventListener("popstate", () => ROUTER.apply());
898
  window.addEventListener("resize", () => { if (!localStorage.getItem("mobileMode")) { const wm = STATE.isMobile; STATE.isMobile = autoDetectMobile(); if (wm !== STATE.isMobile) applyMobileMode(); } });
899
 
static/fetch_and_parse.py ADDED
@@ -0,0 +1,366 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ ๆฏๅคฉๆ‹‰ๅ– 9 ไธช HF ไป“ๅบ“็š„ ็›ดๆŽฅ็›ฎๅฝ•.txt๏ผŒ็”Ÿๆˆ search_data.jsonใ€‚
4
+ ไป…ๅฝ“ๆœ‰ไป“ๅบ“ commit SHA ๅ˜ๅŒ–ๆ—ถๆ‰้‡ๆ–ฐ็”Ÿๆˆใ€‚
5
+
6
+ ็”จๆณ•:
7
+ python scripts/fetch_and_parse.py
8
+ ๅฏ้€‰็Žฏๅขƒๅ˜้‡: HF_TOKEN๏ผˆๆๅ‡ API ้ข‘็އ้™ๅˆถ๏ผ‰
9
+ """
10
+
11
+ import json
12
+ import os
13
+ import sys
14
+ import time
15
+ import urllib.parse
16
+ import urllib.request
17
+ from pathlib import Path
18
+
19
+ # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
20
+ # ้…็ฝฎ
21
+ # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
22
+
23
+ REPOS = [
24
+ "VoiceOfML/VOMEBOOK",
25
+ "VoiceOfML/SovMaterials",
26
+ "VoiceOfML/GPCREducation",
27
+ "VoiceOfML/Teachers",
28
+ "VoiceOfML/MLMRL-Library",
29
+ "VoiceOfML/Japanese-Materials",
30
+ "VoiceOfML/A-Historical-Learning-Data",
31
+ "VoiceOfML/MLMRL-Hub",
32
+ "VoiceOfML/IMPMaterial",
33
+ ]
34
+
35
+ STATE_FILE = Path("state/commits.json")
36
+ OUTPUT_DIR = Path("output")
37
+ OUTPUT_FILE = OUTPUT_DIR / "search_data.json"
38
+
39
+ API_DATASETS = "https://huggingface.co/api/datasets"
40
+ RAW_BASE = "https://huggingface.co/datasets"
41
+
42
+
43
+ # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
44
+ # ๅทฅๅ…ทๅ‡ฝๆ•ฐ
45
+ # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
46
+
47
+ def _make_request(url: str, token: str) -> urllib.request.Request:
48
+ """ๆž„้€ ๅธฆ User-Agent ๅ’Œๅฏ้€‰ Bearer token ็š„ HTTP ่ฏทๆฑ‚ใ€‚"""
49
+ req = urllib.request.Request(url)
50
+ req.add_header("User-Agent", "VoiceOfML-Search-Pipeline/1.0")
51
+ if token:
52
+ req.add_header("Authorization", f"Bearer {token}")
53
+ return req
54
+
55
+
56
+ def http_get_json(url: str, token: str = "") -> dict | list:
57
+ """GET JSON ็ซฏ็‚น๏ผŒ้‡ 429 ่‡ชๅŠจ้‡่ฏ•๏ผŒๅธฆๆŒ‡ๆ•ฐ้€€้ฟใ€‚"""
58
+ for attempt in range(3):
59
+ try:
60
+ with urllib.request.urlopen(_make_request(url, token), timeout=30) as resp:
61
+ body = resp.read().decode("utf-8")
62
+ return json.loads(body)
63
+ except urllib.error.HTTPError as e:
64
+ if e.code == 429:
65
+ wait = 2 ** attempt
66
+ print(f" โณ ้ข‘็އ้™ๅˆถ (429)๏ผŒ็ญ‰ๅพ… {wait}s ๅŽ้‡่ฏ•...")
67
+ time.sleep(wait)
68
+ continue
69
+ print(f" โš  HTTP {e.code}: {url}")
70
+ return {}
71
+ except Exception as e:
72
+ print(f" โš  HTTP GET JSON ๅคฑ่ดฅ [{url}]: {e}")
73
+ return {}
74
+ return {}
75
+
76
+
77
+ def http_get_text(url: str, token: str = "") -> str:
78
+ """GET ๆ–‡ๆœฌ็ซฏ็‚น๏ผŒๆˆๅŠŸ่ฟ”ๅ›žๅญ—็ฌฆไธฒ๏ผŒๅคฑ่ดฅ่ฟ”ๅ›ž็ฉบๅญ—็ฌฆไธฒใ€‚"""
79
+ try:
80
+ with urllib.request.urlopen(_make_request(url, token), timeout=30) as resp:
81
+ return resp.read().decode("utf-8")
82
+ except Exception as e:
83
+ print(f" โš  HTTP GET TEXT ๅคฑ่ดฅ [{url}]: {e}")
84
+ return ""
85
+
86
+
87
+ def encode_url_path(raw_path: str) -> str:
88
+ """
89
+ ๅฏน่ทฏๅพ„่ฟ›่กŒ URL ็™พๅˆ†ๅท็ผ–็  (UTF-8)๏ผŒไฟ็•™ '/' ไธ็ผ–็ ใ€‚
90
+
91
+ ็คบไพ‹:
92
+ โ€ข้‡่ฆ่ต„ๆ–™/ใ€Šๅ›ฝ้™…ๆญŒใ€‹.pdf
93
+ โ†’ %E2%80%A2%E9%87%8D%E8%A6%81%E8%B5%84%E6%96%99/%E3%80%8A%E5%9B%BD%E9%99%85%E6%AD%8C%E3%80%8B.pdf
94
+ """
95
+ return urllib.parse.quote(raw_path, safe="/")
96
+
97
+ def batch_get_sizes(repo: str, paths: list[str], token: str, max_bytes: int = 80000) -> dict:
98
+ url = f"https://huggingface.co/api/datasets/{repo}/paths-info/main"
99
+ size_map = {}
100
+ total = len(paths)
101
+ idx = 0
102
+ batch_num = 0
103
+
104
+ while idx < total:
105
+ # ๅŠจๆ€ๅ–ไธ€ๆ‰น๏ผŒๆŽงๅˆถ payload ไธ่ถ…่ฟ‡ max_bytes
106
+ batch = []
107
+ payload_size = 0
108
+ overhead = 30 # {"paths":[]} ็š„ๅ›บๅฎšๅผ€้”€
109
+
110
+ while idx < total and payload_size + overhead + len(paths[idx].encode('utf-8')) + 5 < max_bytes:
111
+ batch.append(paths[idx])
112
+ payload_size += len(paths[idx].encode('utf-8')) + 3 # ๆฏๆกๅŠ ๅผ•ๅทๅ’Œ้€—ๅท
113
+ idx += 1
114
+
115
+ if not batch:
116
+ # ๅ•ๆก่ทฏๅพ„ๅฐฑ่ถ…้•ฟ๏ผˆ็ฝ•่ง๏ผ‰๏ผŒๅผบๅˆถๅ•็‹ฌๅ‘้€
117
+ batch = [paths[idx]]
118
+ idx += 1
119
+
120
+ batch_num += 1
121
+ payload = json.dumps({"paths": batch}).encode("utf-8")
122
+
123
+ req = urllib.request.Request(url, data=payload, method="POST")
124
+ req.add_header("User-Agent", "VoiceOfML-Search-Pipeline/1.0")
125
+ req.add_header("Content-Type", "application/json")
126
+ if token:
127
+ req.add_header("Authorization", f"Bearer {token}")
128
+
129
+ try:
130
+ with urllib.request.urlopen(req, timeout=60) as resp:
131
+ results = json.loads(resp.read().decode("utf-8"))
132
+ for item in results:
133
+ p = item.get("path", "")
134
+ s = item.get("size", 0)
135
+ if p and s:
136
+ size_map[p] = s
137
+ except Exception as e:
138
+ print(f" โš  paths-info ๅคฑ่ดฅ (batch {batch_num}, {len(batch)}ๆก): {e}")
139
+
140
+ if idx < total:
141
+ time.sleep(0.3)
142
+
143
+ return size_map
144
+
145
+ def parse_one_line(line: str, repo: str, size_map: dict) -> dict | None:
146
+ """
147
+ ่งฃๆž txt ไธญ็š„ไธ€่กŒ่ทฏๅพ„๏ผŒ่ฟ”ๅ›žไธ€ๆก JSON ่ฎฐๅฝ•ใ€‚ไธๅˆๆณ•ๅˆ™่ฟ”ๅ›ž Noneใ€‚
148
+
149
+ ่พ“ๅ…ฅ็คบไพ‹:
150
+ ./โ€ข้‡่ฆ่ต„ๆ–™/ใ€Šๅ›ฝ้™…ๆญŒใ€‹ๅœจไธญๅ›ฝโ€”โ€”ๅ›ฝ้™…ๆญŒ็š„่ฏ‘ๆœฌใ€ๅบ•ๆœฌไธŽไผ ๆ’ญ.pdf
151
+
152
+ ่ฟ”ๅ›ž:
153
+ {
154
+ "Repo": "VoiceOfML/MLMRL-Hub",
155
+ "File": "ใ€Šๅ›ฝ้™…ๆญŒใ€‹ๅœจไธญๅ›ฝโ€”โ€”ๅ›ฝ้™…ๆญŒ็š„่ฏ‘ๆœฌใ€ๅบ•ๆœฌไธŽไผ ๆ’ญ",
156
+ "Extension": "pdf",
157
+ "Link": "https://huggingface.co/datasets/VoiceOfML/MLMRL-Hub/resolve/main/%E2%80%A2...",
158
+ "Path": "https://huggingface.co/datasets/VoiceOfML/MLMRL-Hub/blob/main/%E2%80%A2...",
159
+ "Folder": ["โ€ข้‡่ฆ่ต„ๆ–™"],
160
+ "Size": 12345,
161
+ "HasTxt": false
162
+ }
163
+ """
164
+ line = line.strip()
165
+ if not line or line.startswith("#"):
166
+ return None
167
+
168
+ # ่ฟ‡ๆปค Git LFS ๆŒ‡้’ˆ่กŒ
169
+ if line.startswith("version https://git-lfs") or line.startswith("oid sha256:") or line.startswith("size "):
170
+ return None
171
+ # ๅŽปๆމๅผ€ๅคด็š„ ./
172
+ if line.startswith("./"):
173
+ rel_path = line[2:]
174
+ elif line.startswith("/"):
175
+ rel_path = line[1:]
176
+ else:
177
+ rel_path = line
178
+
179
+ if not rel_path:
180
+ return None
181
+
182
+ # ๆ‹†ๅˆ†ๆ–‡ไปถๅคน้ƒจๅˆ†ๅ’Œๆ–‡ไปถๅ้ƒจๅˆ†
183
+ parts = rel_path.split("/")
184
+ filename_part = parts[-1] if parts else ""
185
+ folder_parts = parts[:-1] if len(parts) > 1 else []
186
+
187
+ # ๆๅ–ๆ–‡ไปถๅๅ’Œๆ‰ฉๅฑ•ๅ
188
+ if "." in filename_part and not filename_part.startswith("."):
189
+ # ๆ™ฎ้€šๆ–‡ไปถ: name.ext
190
+ last_dot = filename_part.rfind(".")
191
+ file_name = filename_part[:last_dot]
192
+ extension = filename_part[last_dot + 1:]
193
+ elif filename_part.startswith(".") and filename_part.count(".") >= 2:
194
+ # ้š่—ๆ–‡ไปถๅธฆๆ‰ฉๅฑ•ๅ: .gitignore
195
+ last_dot = filename_part.rfind(".")
196
+ file_name = filename_part[:last_dot]
197
+ extension = filename_part[last_dot + 1:]
198
+ else:
199
+ # ๆ— ๅŽ็ผ€ๆ–‡ไปถ๏ผˆ็บฏๆ–‡ไปถๅ ๆˆ– ๅชไปฅ็‚นๅผ€ๅคด็š„้š่—ๆ–‡ไปถๅฆ‚ .hidden๏ผ‰
200
+ file_name = filename_part
201
+ extension = ""
202
+
203
+ # ๆž„้€ ไธคไธช้“พๆŽฅ๏ผˆ่ทฏๅพ„ๅš URL ็ผ–็ ๏ผ‰
204
+ encoded_rel = encode_url_path(rel_path)
205
+
206
+ link = f"https://huggingface.co/datasets/{repo}/resolve/main/{encoded_rel}"
207
+ path_url = f"https://huggingface.co/datasets/{repo}/blob/main/{encoded_rel}"
208
+
209
+ # ๆ–‡ไปถๅคงๅฐ๏ผˆๅฏ่ƒฝไธบ็ฉบๅญ—็ฌฆไธฒ๏ผ‰
210
+ size = size_map.get(rel_path, "")
211
+
212
+ return {
213
+ "Repo": repo,
214
+ "File": file_name,
215
+ "Extension": extension,
216
+ "Link": link,
217
+ "Path": path_url,
218
+ "Folder": folder_parts,
219
+ "Size": size,
220
+ "HasTxt": False,
221
+ }
222
+
223
+
224
+ def get_repo_sha(repo: str, token: str) -> str:
225
+ """
226
+ ้€š่ฟ‡ HF API ่Žทๅ–ไป“ๅบ“ๆœ€ๆ–ฐ commit SHAใ€‚
227
+ ่ฟ”ๅ›ž็ฉบๅญ—็ฌฆไธฒ่กจ็คบ่Žทๅ–ๅคฑ่ดฅใ€‚
228
+ """
229
+ url = f"{API_DATASETS}/{repo}"
230
+ data = http_get_json(url, token)
231
+ if isinstance(data, dict):
232
+ return data.get("sha", "")
233
+ return ""
234
+
235
+ # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
236
+ # ไธปๆต็จ‹
237
+ # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
238
+
239
+ def main():
240
+ token = os.environ.get("HF_TOKEN", "")
241
+
242
+ print("=" * 60)
243
+ print("๐Ÿ“‚ VoiceOfML Search Pipeline โ€” fetch_and_parse")
244
+ print("=" * 60)
245
+
246
+ # โ”€โ”€ 1. ่ฏปๅ–ไธŠๆฌก commit ็Šถๆ€ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
247
+ old_state = {}
248
+ if STATE_FILE.exists():
249
+ try:
250
+ old_state = json.loads(STATE_FILE.read_text(encoding="utf-8"))
251
+ print(f"๐Ÿ“– ๅทฒ่ฏปๅ–ไธŠๆฌก็Šถๆ€: {len(old_state)} ไธชไป“ๅบ“")
252
+ except json.JSONDecodeError:
253
+ print("โš  ็Šถๆ€ๆ–‡ไปถๆŸๅ๏ผŒๅฐ†ๅ…จ้‡้‡ๆ–ฐ็”Ÿๆˆ")
254
+ else:
255
+ print("๐Ÿ“– ๆ— ๅކๅฒ็Šถๆ€ๆ–‡ไปถ๏ผŒๅฐ†ๅ…จ้‡็”Ÿๆˆ")
256
+
257
+ # โ”€โ”€ 2. ๆฃ€ๆŸฅๆฏไธชไป“ๅบ“็š„ๆœ€ๆ–ฐ commit SHA โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
258
+ new_state = {}
259
+ changed_repos = []
260
+
261
+ for repo in REPOS:
262
+ print(f"\n๐Ÿ” ๆฃ€ๆŸฅไป“ๅบ“: {repo}")
263
+ sha = get_repo_sha(repo, token)
264
+ new_state[repo] = sha
265
+
266
+ if not sha:
267
+ print(f" โš  ๆ— ๆณ•่Žทๅ– SHA๏ผŒ่ทณ่ฟ‡")
268
+ continue
269
+
270
+ old_sha = old_state.get(repo, "")
271
+ if sha != old_sha or not old_sha:
272
+ print(f" ๐Ÿ”„ ๆœ‰ๅ˜ๆ›ด: {old_sha[:8] if old_sha else '(ๆ–ฐ)'} โ†’ {sha[:8]}")
273
+ changed_repos.append(repo)
274
+ else:
275
+ print(f" โœ… ๆ— ๅ˜ๆ›ด: {sha[:8]}")
276
+
277
+ # โ”€โ”€ 3. ๅˆคๆ–ญๆ˜ฏๅฆ้œ€่ฆ้‡ๆ–ฐ๏ฟฝ๏ฟฝ๏ฟฝๆˆ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
278
+ # ๆฃ€ๆŸฅ output ๆ˜ฏๅฆๆœ‰ๆ•ˆ
279
+ output_is_valid = OUTPUT_FILE.exists()
280
+ if output_is_valid:
281
+ try:
282
+ existing = json.loads(OUTPUT_FILE.read_text(encoding="utf-8"))
283
+ if not isinstance(existing, list) or len(existing) == 0:
284
+ output_is_valid = False
285
+ except Exception:
286
+ output_is_valid = False
287
+
288
+ if not changed_repos and output_is_valid:
289
+ print("\nโœ… ๆ‰€ๆœ‰ไป“ๅบ“ๆ— ๅ˜ๆ›ด๏ผŒ่ทณ่ฟ‡็”Ÿๆˆใ€‚")
290
+ STATE_FILE.parent.mkdir(parents=True, exist_ok=True)
291
+ STATE_FILE.write_text(
292
+ json.dumps(new_state, ensure_ascii=False, indent=2),
293
+ encoding="utf-8",
294
+ )
295
+ return 0
296
+
297
+ if not changed_repos and not output_is_valid:
298
+ print("\nโš  ไป“ๅบ“ๆ— ๅ˜ๆ›ด๏ผŒไฝ† output ไธบ็ฉบ๏ผŒๅผบๅˆถ้‡ๆ–ฐ็”Ÿๆˆ...")
299
+
300
+ print(f"\n๐Ÿ”„ {len(changed_repos)} ไธชไป“ๅบ“ๆœ‰ๅ˜ๆ›ด๏ผŒๅผ€ๅง‹้‡ๆ–ฐ็”Ÿๆˆ...")
301
+
302
+ # โ”€โ”€ 4. ๆ‹‰ๅ– txt ๅนถ่งฃๆž โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
303
+ all_records = []
304
+
305
+ for repo in REPOS:
306
+ print(f"\n๐Ÿ“ฅ ๅค„็†ไป“ๅบ“: {repo}")
307
+
308
+ # 1. ไธ‹่ฝฝ txt
309
+ txt_url = f"{RAW_BASE}/{repo}/resolve/main/{urllib.parse.quote('็›ดๆŽฅ็›ฎๅฝ•.txt')}"
310
+ print(f" ๐Ÿ“„ ไธ‹่ฝฝ: {txt_url}")
311
+ txt_content = http_get_text(txt_url, token)
312
+ if not txt_content:
313
+ print(f" โš  ๆ— ๆณ•ไธ‹่ฝฝ๏ผŒ่ทณ่ฟ‡")
314
+ continue
315
+
316
+ # 2. ๅ…ˆ่งฃๆž๏ผˆSize ๆš‚ๆ—ถไธบ็ฉบ๏ผ‰
317
+ lines = txt_content.split("\n")
318
+ repo_records = []
319
+ all_paths = []
320
+ for line in lines:
321
+ rec = parse_one_line(line, repo, {}) # ็ฉบ size_map
322
+ if rec:
323
+ repo_records.append(rec)
324
+ # ๆๅ–็›ธๅฏน่ทฏๅพ„็”จไบŽ paths-info ่ฏทๆฑ‚
325
+ rel_path = "/".join(rec["Folder"] + [rec["File"] + ("." + rec["Extension"] if rec["Extension"] else "")])
326
+ all_paths.append(rel_path)
327
+
328
+ print(f" โœ… ่งฃๆžๅˆฐ {len(repo_records)} ๆก่ฎฐๅฝ•")
329
+
330
+ # 3. ๆ‰น้‡่Žทๅ–ๅคงๅฐ
331
+ print(f" ๐Ÿ“ ๆ‰น้‡่Žทๅ–ๆ–‡ไปถๅคงๅฐ ({len(all_paths)} ไธชๆ–‡ไปถ)...")
332
+ size_map = batch_get_sizes(repo, all_paths, token)
333
+
334
+ # 4. ่กฅๅ…จ Size
335
+ filled = 0
336
+ for i, rec in enumerate(repo_records):
337
+ path_key = all_paths[i]
338
+ sz = size_map.get(path_key, "")
339
+ if sz:
340
+ rec["Size"] = sz
341
+ filled += 1
342
+ print(f" โœ… ่กฅๅ…จไบ† {filled} ไธชๆ–‡ไปถ็š„ๅคงๅฐ")
343
+
344
+ all_records.extend(repo_records)
345
+ time.sleep(0.5)
346
+ # โ”€โ”€ 5. ๅ†™ๅ…ฅ output/search_data.json โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
347
+ OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
348
+ json_text = json.dumps(all_records, ensure_ascii=False, indent=2)
349
+ OUTPUT_FILE.write_text(json_text, encoding="utf-8")
350
+ file_size_mb = len(json_text.encode("utf-8")) / 1024 / 1024
351
+ print(f"๐Ÿ’พ ๅทฒๅ†™ๅ…ฅ {OUTPUT_FILE} ({file_size_mb:.1f} MB)")
352
+
353
+ # โ”€โ”€ 6. ๆ›ดๆ–ฐ state/commits.json โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
354
+ STATE_FILE.parent.mkdir(parents=True, exist_ok=True)
355
+ STATE_FILE.write_text(
356
+ json.dumps(new_state, ensure_ascii=False, indent=2),
357
+ encoding="utf-8",
358
+ )
359
+ print(f"๐Ÿ’พ ๅทฒๆ›ดๆ–ฐ {STATE_FILE}")
360
+
361
+ print("\nโœ… fetch_and_parse ๅฎŒๆˆ๏ผ")
362
+ return 0
363
+
364
+
365
+ if __name__ == "__main__":
366
+ sys.exit(main())
static/index.html CHANGED
@@ -81,6 +81,7 @@
81
  <aside id="left-sidebar" class="sidebar left-sidebar">
82
  <div class="sidebar-header">
83
  <span id="sidebar-title">ไป“ๅบ“ๅˆ—่กจ</span>
 
84
  </div>
85
  <div id="sidebar-content" class="sidebar-content">
86
  <div class="sidebar-loading">ๅŠ ่ฝฝไธญ...</div>
 
81
  <aside id="left-sidebar" class="sidebar left-sidebar">
82
  <div class="sidebar-header">
83
  <span id="sidebar-title">ไป“ๅบ“ๅˆ—่กจ</span>
84
+ <button id="sidebar-expand-btn" class="icon-btn-sm" title="ๅฑ•ๅผ€/ๆ”ถ่ตทไพง่พนๆ " style="display:none">โ†”</button>
85
  </div>
86
  <div id="sidebar-content" class="sidebar-content">
87
  <div class="sidebar-loading">ๅŠ ่ฝฝไธญ...</div>