Merge pull request #127 from matthewellis/fix/jlcpcb-downloader-improvements
fix: improve JLCPCB downloader robustness and disk usage
This commit is contained in:
@@ -12,6 +12,7 @@ KiCad MCP server's JLCPCBPartsManager.
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import shutil
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
@@ -25,31 +26,50 @@ CACHE_DIR = DATA_DIR / "jlcparts_cache"
|
|||||||
CACHE_DIR.mkdir(exist_ok=True)
|
CACHE_DIR.mkdir(exist_ok=True)
|
||||||
|
|
||||||
BASE_URL = "https://yaqwsx.github.io/jlcparts/data"
|
BASE_URL = "https://yaqwsx.github.io/jlcparts/data"
|
||||||
PARTS = [f"cache.z{i:02d}" for i in range(1, 20)] + ["cache.zip"]
|
MAX_PARTS = 30 # probe up to this many split volumes
|
||||||
|
|
||||||
TARGET_DB = DATA_DIR / "jlcpcb_parts.db"
|
TARGET_DB = DATA_DIR / "jlcpcb_parts.db"
|
||||||
|
|
||||||
|
|
||||||
def download_files():
|
def curl_download(url: str, dest: Path) -> bool:
|
||||||
"""Download all split archive parts."""
|
"""Download url to dest with curl. Returns False on 4xx/5xx or network error."""
|
||||||
|
result = subprocess.run(
|
||||||
|
["curl", "-L", "-f", "-o", str(dest), "--progress-bar", url], capture_output=False
|
||||||
|
)
|
||||||
|
return result.returncode == 0
|
||||||
|
|
||||||
|
|
||||||
|
def download_files() -> bool:
|
||||||
|
"""Download split archive volumes (z01..zNN) then cache.zip, stopping volumes at 404 or MAX_PARTS."""
|
||||||
print("Downloading jlcparts database (~421MB)...")
|
print("Downloading jlcparts database (~421MB)...")
|
||||||
for part in PARTS:
|
|
||||||
|
for i in range(1, MAX_PARTS + 1):
|
||||||
|
part = f"cache.z{i:02d}"
|
||||||
dest = CACHE_DIR / part
|
dest = CACHE_DIR / part
|
||||||
if dest.exists() and dest.stat().st_size > 1000:
|
if dest.exists() and dest.stat().st_size > 1000:
|
||||||
print(f" {part} already exists, skipping")
|
print(f" {part} already exists, skipping")
|
||||||
continue
|
continue
|
||||||
url = f"{BASE_URL}/{part}"
|
|
||||||
print(f" Downloading {part}...")
|
print(f" Downloading {part}...")
|
||||||
result = subprocess.run(
|
if not curl_download(f"{BASE_URL}/{part}", dest):
|
||||||
["curl", "-L", "-o", str(dest), "--progress-bar", url], capture_output=False
|
# -f causes non-zero exit on 4xx/5xx; treat as end of volumes
|
||||||
)
|
if dest.exists():
|
||||||
if result.returncode != 0:
|
dest.unlink()
|
||||||
print(f" ERROR downloading {part}")
|
print(f" {part} not found — {i - 1} volumes total")
|
||||||
|
break
|
||||||
|
|
||||||
|
dest = CACHE_DIR / "cache.zip"
|
||||||
|
if dest.exists() and dest.stat().st_size > 1000:
|
||||||
|
print(" cache.zip already exists, skipping")
|
||||||
|
else:
|
||||||
|
print(" Downloading cache.zip...")
|
||||||
|
if not curl_download(f"{BASE_URL}/cache.zip", dest):
|
||||||
|
print(" ERROR downloading cache.zip")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def extract_database():
|
def extract_database() -> bool:
|
||||||
"""Extract the split 7z archive to get cache.sqlite3."""
|
"""Extract the split 7z archive to get cache.sqlite3."""
|
||||||
cache_sqlite = CACHE_DIR / "cache.sqlite3"
|
cache_sqlite = CACHE_DIR / "cache.sqlite3"
|
||||||
if cache_sqlite.exists() and cache_sqlite.stat().st_size > 100_000_000:
|
if cache_sqlite.exists() and cache_sqlite.stat().st_size > 100_000_000:
|
||||||
@@ -77,7 +97,7 @@ def extract_database():
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def convert_to_mcp_format():
|
def convert_to_mcp_format() -> bool:
|
||||||
"""Convert jlcparts cache.sqlite3 to the MCP server's expected format."""
|
"""Convert jlcparts cache.sqlite3 to the MCP server's expected format."""
|
||||||
source = CACHE_DIR / "cache.sqlite3"
|
source = CACHE_DIR / "cache.sqlite3"
|
||||||
if not source.exists():
|
if not source.exists():
|
||||||
@@ -116,7 +136,8 @@ def convert_to_mcp_format():
|
|||||||
|
|
||||||
# Create target DB in MCP format
|
# Create target DB in MCP format
|
||||||
dst = sqlite3.connect(str(TARGET_DB))
|
dst = sqlite3.connect(str(TARGET_DB))
|
||||||
dst.execute("""
|
dst.execute(
|
||||||
|
"""
|
||||||
CREATE TABLE components (
|
CREATE TABLE components (
|
||||||
lcsc TEXT PRIMARY KEY,
|
lcsc TEXT PRIMARY KEY,
|
||||||
category TEXT,
|
category TEXT,
|
||||||
@@ -132,7 +153,8 @@ def convert_to_mcp_format():
|
|||||||
price_json TEXT,
|
price_json TEXT,
|
||||||
last_updated INTEGER
|
last_updated INTEGER
|
||||||
)
|
)
|
||||||
""")
|
"""
|
||||||
|
)
|
||||||
dst.execute("CREATE INDEX idx_category ON components(category, subcategory)")
|
dst.execute("CREATE INDEX idx_category ON components(category, subcategory)")
|
||||||
dst.execute("CREATE INDEX idx_package ON components(package)")
|
dst.execute("CREATE INDEX idx_package ON components(package)")
|
||||||
dst.execute("CREATE INDEX idx_manufacturer ON components(manufacturer)")
|
dst.execute("CREATE INDEX idx_manufacturer ON components(manufacturer)")
|
||||||
@@ -233,12 +255,14 @@ def convert_to_mcp_format():
|
|||||||
|
|
||||||
# Build FTS index
|
# Build FTS index
|
||||||
print(f" Building full-text search index...")
|
print(f" Building full-text search index...")
|
||||||
dst.execute("""
|
dst.execute(
|
||||||
|
"""
|
||||||
CREATE VIRTUAL TABLE IF NOT EXISTS components_fts USING fts5(
|
CREATE VIRTUAL TABLE IF NOT EXISTS components_fts USING fts5(
|
||||||
lcsc, description, mfr_part, manufacturer,
|
lcsc, description, mfr_part, manufacturer,
|
||||||
content=components
|
content=components
|
||||||
)
|
)
|
||||||
""")
|
"""
|
||||||
|
)
|
||||||
dst.execute("INSERT INTO components_fts(components_fts) VALUES('rebuild')")
|
dst.execute("INSERT INTO components_fts(components_fts) VALUES('rebuild')")
|
||||||
dst.commit()
|
dst.commit()
|
||||||
|
|
||||||
@@ -252,6 +276,10 @@ def convert_to_mcp_format():
|
|||||||
dst.close()
|
dst.close()
|
||||||
src.close()
|
src.close()
|
||||||
|
|
||||||
|
print("Cleaning up source database...")
|
||||||
|
shutil.rmtree(CACHE_DIR)
|
||||||
|
print(f" Removed {CACHE_DIR}")
|
||||||
|
|
||||||
db_size = TARGET_DB.stat().st_size / (1024 * 1024)
|
db_size = TARGET_DB.stat().st_size / (1024 * 1024)
|
||||||
print(f"\nDatabase ready: {TARGET_DB}")
|
print(f"\nDatabase ready: {TARGET_DB}")
|
||||||
print(f" Total parts: {total:,}")
|
print(f" Total parts: {total:,}")
|
||||||
@@ -261,7 +289,7 @@ def convert_to_mcp_format():
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main() -> None:
|
||||||
print("=" * 60)
|
print("=" * 60)
|
||||||
print("JLCPCB Parts Database Downloader (jlcparts method)")
|
print("JLCPCB Parts Database Downloader (jlcparts method)")
|
||||||
print("=" * 60)
|
print("=" * 60)
|
||||||
|
|||||||
Reference in New Issue
Block a user