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 unittest import mock
from tools.generate_popular_node_signatures import ( from tools.generate_popular_node_signatures import (
DEFAULT_CACHE_DIR,
build_artifact, build_artifact,
clone_or_update_repo, clone_or_update_repo,
extract_repo_signatures, extract_repo_signatures,
@@ -5602,19 +5603,19 @@ class ManagerIngestionTests(unittest.TestCase):
"id": "tie-b", "id": "tie-b",
"title": "Tie B", "title": "Tie B",
"repository": "https://github.com/example/tie-b", "repository": "https://github.com/example/tie-b",
"metrics": {"downloads": 5}, "metrics": {"downloads": 5, "stars": 2},
}, },
{ {
"id": "most", "id": "most",
"title": "Most", "title": "Most",
"repository": "https://github.com/example/most", "repository": "https://github.com/example/most",
"metrics": {"stars": 10}, "metrics": {"downloads": 10},
}, },
{ {
"id": "tie-a", "id": "tie-a",
"title": "Tie A", "title": "Tie A",
"repository": "https://github.com/example/tie-a", "repository": "https://github.com/example/tie-a",
"metrics": {"favorites": 5}, "metrics": {"downloads": 5, "stars": 2},
}, },
{ {
"id": "none", "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(["most", "tie-a", "tie-b", "none"], [pack["id"] for pack in ranked])
self.assertEqual([1, 2, 3, 4], [pack["rank"] 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): 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): def test_repo_cache_path_is_safe_stable_and_collision_resistant(self):
with tempfile.TemporaryDirectory() as tmp: with tempfile.TemporaryDirectory() as tmp:
cache_dir = Path(tmp) cache_dir = Path(tmp)
+57 -23
View File
@@ -8,6 +8,7 @@ import json
import os import os
import re import re
import subprocess import subprocess
import tempfile
import urllib.request import urllib.request
from datetime import datetime, timezone from datetime import datetime, timezone
from pathlib import Path 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" MANAGER_LIST_URL = "https://raw.githubusercontent.com/ltdrdata/ComfyUI-Manager/main/custom-node-list.json"
REGISTRY_NODES_URL = "https://api.comfy.org/nodes" REGISTRY_NODES_URL = "https://api.comfy.org/nodes"
DEFAULT_GENERATED_AT = "1970-01-01T00:00:00Z" 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") DEFAULT_OUTPUT = Path("popular_node_signatures.json")
USER_AGENT = "ComfyUI-UTFCN popular node signature generator" USER_AGENT = "ComfyUI-UTFCN popular node signature generator"
@@ -56,6 +57,9 @@ _METRIC_FIELDS = (
"stars", "stars",
"github_stars", "github_stars",
"stargazers_count", "stargazers_count",
"search_ranking",
"search_rank",
"search_order",
"favorites", "favorites",
"favourites", "favourites",
"installed", "installed",
@@ -63,6 +67,7 @@ _METRIC_FIELDS = (
"install_count", "install_count",
"count", "count",
) )
_SEARCH_RANKING_FIELDS = {"search_ranking", "search_rank", "search_order"}
def fetch_json(url): def fetch_json(url):
@@ -100,6 +105,20 @@ def _coerce_int(value):
return 0 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"): def _slug(value, default="unnamed-pack"):
text = str(value or "").strip().lower() text = str(value or "").strip().lower()
text = re.sub(r"[^a-z0-9]+", "-", text).strip("-") text = re.sub(r"[^a-z0-9]+", "-", text).strip("-")
@@ -216,12 +235,31 @@ def _entry_metrics(item):
sources.append(value) sources.append(value)
for source in sources: for source in sources:
for field in _METRIC_FIELDS: for field in _METRIC_FIELDS:
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)) value = _coerce_int(source.get(field))
if value: if value:
metrics[field] = value metrics[field] = value
return metrics 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): def _pack_id_from_repository(repository):
parsed = urlparse(repository) parsed = urlparse(repository)
if parsed.netloc: if parsed.netloc:
@@ -261,8 +299,22 @@ def normalise_manager_entries(raw):
return entries return entries
def _popularity_score(pack): def _rank_sort_key(pack):
return sum(_coerce_int(value) for value in pack.get("metrics", {}).values()) 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): def rank_packs(packs, limit=None):
@@ -276,28 +328,10 @@ def rank_packs(packs, limit=None):
if previous is None: if previous is None:
best_by_repository[repository] = candidate best_by_repository[repository] = candidate
continue continue
candidate_key = ( if _rank_sort_key(candidate) < _rank_sort_key(previous):
_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:
best_by_repository[repository] = candidate best_by_repository[repository] = candidate
ranked = sorted( ranked = sorted(best_by_repository.values(), key=_rank_sort_key)
best_by_repository.values(),
key=lambda pack: (
-_popularity_score(pack),
str(pack.get("title", "")).lower(),
str(pack.get("id", "")),
str(pack.get("repository", "")),
),
)
if limit is not None: if limit is not None:
ranked = ranked[:limit] ranked = ranked[:limit]
result = [] result = []