Fix manager ranking and cache defaults

This commit is contained in:
2026-07-02 22:09:50 +02:00
parent 28186698d0
commit 33690683b7
2 changed files with 92 additions and 29 deletions
+32 -3
View File
@@ -8,6 +8,7 @@ from pathlib import Path
from unittest import mock
from tools.generate_popular_node_signatures import (
DEFAULT_CACHE_DIR,
build_artifact,
clone_or_update_repo,
extract_repo_signatures,
@@ -5602,19 +5603,19 @@ class ManagerIngestionTests(unittest.TestCase):
"id": "tie-b",
"title": "Tie B",
"repository": "https://github.com/example/tie-b",
"metrics": {"downloads": 5},
"metrics": {"downloads": 5, "stars": 2},
},
{
"id": "most",
"title": "Most",
"repository": "https://github.com/example/most",
"metrics": {"stars": 10},
"metrics": {"downloads": 10},
},
{
"id": "tie-a",
"title": "Tie A",
"repository": "https://github.com/example/tie-a",
"metrics": {"favorites": 5},
"metrics": {"downloads": 5, "stars": 2},
},
{
"id": "none",
@@ -5629,8 +5630,36 @@ class ManagerIngestionTests(unittest.TestCase):
self.assertEqual(["most", "tie-a", "tie-b", "none"], [pack["id"] for pack in ranked])
self.assertEqual([1, 2, 3, 4], [pack["rank"] for pack in ranked])
def test_rank_packs_prioritizes_downloads_before_stars(self):
packs = [
{
"id": "many-stars",
"title": "Many Stars",
"repository": "https://github.com/example/many-stars",
"metrics": {"downloads": 1, "stars": 1000},
},
{
"id": "many-downloads",
"title": "Many Downloads",
"repository": "https://github.com/example/many-downloads",
"metrics": {"downloads": 100, "stars": 0},
},
]
ranked = rank_packs(packs)
self.assertEqual(["many-downloads", "many-stars"], [pack["id"] for pack in ranked])
class RepoCacheTests(unittest.TestCase):
def test_default_cache_dir_is_under_system_temp_dir(self):
temp_root = Path(tempfile.gettempdir()).resolve()
default_cache = DEFAULT_CACHE_DIR.resolve()
self.assertTrue(default_cache.is_absolute())
self.assertTrue(default_cache == temp_root or temp_root in default_cache.parents)
self.assertNotEqual(Path(".cache/utfcn-popular-node-repos"), DEFAULT_CACHE_DIR)
def test_repo_cache_path_is_safe_stable_and_collision_resistant(self):
with tempfile.TemporaryDirectory() as tmp:
cache_dir = Path(tmp)
+60 -26
View File
@@ -8,6 +8,7 @@ import json
import os
import re
import subprocess
import tempfile
import urllib.request
from datetime import datetime, timezone
from pathlib import Path
@@ -17,7 +18,7 @@ SCHEMA_VERSION = 1
MANAGER_LIST_URL = "https://raw.githubusercontent.com/ltdrdata/ComfyUI-Manager/main/custom-node-list.json"
REGISTRY_NODES_URL = "https://api.comfy.org/nodes"
DEFAULT_GENERATED_AT = "1970-01-01T00:00:00Z"
DEFAULT_CACHE_DIR = Path(".cache/utfcn-popular-node-repos")
DEFAULT_CACHE_DIR = Path(tempfile.gettempdir()) / "utfcn-popular-node-repos"
DEFAULT_OUTPUT = Path("popular_node_signatures.json")
USER_AGENT = "ComfyUI-UTFCN popular node signature generator"
@@ -56,6 +57,9 @@ _METRIC_FIELDS = (
"stars",
"github_stars",
"stargazers_count",
"search_ranking",
"search_rank",
"search_order",
"favorites",
"favourites",
"installed",
@@ -63,6 +67,7 @@ _METRIC_FIELDS = (
"install_count",
"count",
)
_SEARCH_RANKING_FIELDS = {"search_ranking", "search_rank", "search_order"}
def fetch_json(url):
@@ -100,6 +105,20 @@ def _coerce_int(value):
return 0
def _coerce_float(value):
if isinstance(value, bool):
return None
if isinstance(value, (int, float)):
return float(value)
if isinstance(value, str):
text = value.strip().replace(",", "")
try:
return float(text)
except ValueError:
return None
return None
def _slug(value, default="unnamed-pack"):
text = str(value or "").strip().lower()
text = re.sub(r"[^a-z0-9]+", "-", text).strip("-")
@@ -216,12 +235,31 @@ def _entry_metrics(item):
sources.append(value)
for source in sources:
for field in _METRIC_FIELDS:
value = _coerce_int(source.get(field))
if value:
metrics[field] = value
if field in _SEARCH_RANKING_FIELDS:
value = _coerce_float(source.get(field))
if value is not None:
metrics[field] = value
else:
value = _coerce_int(source.get(field))
if value:
metrics[field] = value
return metrics
def _metric_max(metrics, names):
values = [_coerce_int(metrics.get(name)) for name in names]
return max(values, default=0)
def _metric_min_float(metrics, names):
values = []
for name in names:
value = _coerce_float(metrics.get(name))
if value is not None:
values.append(value)
return min(values) if values else None
def _pack_id_from_repository(repository):
parsed = urlparse(repository)
if parsed.netloc:
@@ -261,8 +299,22 @@ def normalise_manager_entries(raw):
return entries
def _popularity_score(pack):
return sum(_coerce_int(value) for value in pack.get("metrics", {}).values())
def _rank_sort_key(pack):
metrics = pack.get("metrics", {})
downloads = _metric_max(metrics, ("downloads", "download_count"))
stars = _metric_max(metrics, ("stars", "github_stars", "stargazers_count"))
search_ranking = _metric_min_float(metrics, ("search_ranking", "search_rank", "search_order"))
manager_order = int(pack.get("manager_order", 0))
return (
-downloads,
-stars,
1 if search_ranking is None else 0,
search_ranking if search_ranking is not None else 0.0,
manager_order,
str(pack.get("title", "")).lower(),
str(pack.get("id", "")),
str(pack.get("repository", "")),
)
def rank_packs(packs, limit=None):
@@ -276,28 +328,10 @@ def rank_packs(packs, limit=None):
if previous is None:
best_by_repository[repository] = candidate
continue
candidate_key = (
_popularity_score(candidate),
-int(candidate.get("manager_order", 0)),
str(candidate.get("id", "")),
)
previous_key = (
_popularity_score(previous),
-int(previous.get("manager_order", 0)),
str(previous.get("id", "")),
)
if candidate_key > previous_key:
if _rank_sort_key(candidate) < _rank_sort_key(previous):
best_by_repository[repository] = candidate
ranked = sorted(
best_by_repository.values(),
key=lambda pack: (
-_popularity_score(pack),
str(pack.get("title", "")).lower(),
str(pack.get("id", "")),
str(pack.get("repository", "")),
),
)
ranked = sorted(best_by_repository.values(), key=_rank_sort_key)
if limit is not None:
ranked = ranked[:limit]
result = []