From 33690683b70b6fcac954ad1c60c653a2d2046e0c Mon Sep 17 00:00:00 2001 From: Ethanfel Date: Thu, 2 Jul 2026 22:09:50 +0200 Subject: [PATCH] Fix manager ranking and cache defaults --- .../test_generate_popular_node_signatures.py | 35 +++++++- tools/generate_popular_node_signatures.py | 86 +++++++++++++------ 2 files changed, 92 insertions(+), 29 deletions(-) diff --git a/tests/test_generate_popular_node_signatures.py b/tests/test_generate_popular_node_signatures.py index ff448e0..4e5ac53 100644 --- a/tests/test_generate_popular_node_signatures.py +++ b/tests/test_generate_popular_node_signatures.py @@ -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) diff --git a/tools/generate_popular_node_signatures.py b/tools/generate_popular_node_signatures.py index ec38a8a..db63ecd 100644 --- a/tools/generate_popular_node_signatures.py +++ b/tools/generate_popular_node_signatures.py @@ -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 = []