diff --git a/addons/l0o0@jasminum b/addons/l0o0@jasminum index 4aad403..c164b52 100644 --- a/addons/l0o0@jasminum +++ b/addons/l0o0@jasminum @@ -1 +1 @@ -{"tags": ["metadata"]} \ No newline at end of file +{"tags": ["metadata"], "recommended": true} \ No newline at end of file diff --git a/addons/retorquere@zotero-better-bibtex b/addons/retorquere@zotero-better-bibtex index 8048056..0bb71cf 100644 --- a/addons/retorquere@zotero-better-bibtex +++ b/addons/retorquere@zotero-better-bibtex @@ -1 +1 @@ -{"tags": ["integration"]} \ No newline at end of file +{"tags": ["integration"], "recommended": true} \ No newline at end of file diff --git a/addons/syt2@paper-chat-for-zotero b/addons/syt2@paper-chat-for-zotero index 93da3fb..d402bc1 100644 --- a/addons/syt2@paper-chat-for-zotero +++ b/addons/syt2@paper-chat-for-zotero @@ -1 +1 @@ -{"tags": ["ai"]} \ No newline at end of file +{"tags": ["ai"], "recommended": true} \ No newline at end of file diff --git a/addons/windingwind@zotero-better-notes b/addons/windingwind@zotero-better-notes index fae9524..2bd872c 100644 --- a/addons/windingwind@zotero-better-notes +++ b/addons/windingwind@zotero-better-notes @@ -1 +1 @@ -{"tags": ["notes"]} \ No newline at end of file +{"tags": ["notes"], "recommended": true} \ No newline at end of file diff --git a/addons/windingwind@zotero-pdf-translate b/addons/windingwind@zotero-pdf-translate index 0ab15a9..2651d1d 100644 --- a/addons/windingwind@zotero-pdf-translate +++ b/addons/windingwind@zotero-pdf-translate @@ -1 +1 @@ -{"tags": ["reader"]} \ No newline at end of file +{"tags": ["reader"], "recommended": true} \ No newline at end of file diff --git a/src/zotero_scraper/models/addon.py b/src/zotero_scraper/models/addon.py index a0149d5..ed4344b 100644 --- a/src/zotero_scraper/models/addon.py +++ b/src/zotero_scraper/models/addon.py @@ -156,6 +156,7 @@ class AddonInfo: stars: Optional[int] = None author: Author = field(default_factory=Author) tags: list[str] = field(default_factory=list) + recommended: bool = False @property def owner(self) -> Optional[str]: @@ -193,6 +194,8 @@ class AddonInfo: result["author"] = author_dict if self.tags: result["tags"] = self.tags + if self.recommended: + result["recommended"] = True return result @classmethod @@ -219,4 +222,5 @@ class AddonInfo: stars=data.get("stars"), author=author, tags=data.get("tags", []), + recommended=data.get("recommended", False), ) diff --git a/src/zotero_scraper/services/cache_scraper.py b/src/zotero_scraper/services/cache_scraper.py index 103efbe..e2bd1d9 100644 --- a/src/zotero_scraper/services/cache_scraper.py +++ b/src/zotero_scraper/services/cache_scraper.py @@ -72,8 +72,10 @@ class CacheScraper: for url in self.config.previous_info_urls: result = self.fallback.apply_fallback(result, url) - # Sort by stars (descending) - result.sort(key=lambda x: x.get("stars") or 0, reverse=True) + # Sort: recommended first, then by stars (descending) + result.sort( + key=lambda x: (not x.get("recommended", False), -(x.get("stars") or 0)) + ) # Save output self._save_output(result) @@ -102,25 +104,24 @@ class CacheScraper: return repos - def _load_repo_tags(self, repo: str) -> list[str]: - """Load tags for a repository from its input file. + def _load_repo_config(self, repo: str) -> dict: + """Load metadata for a repository from its input file. - The file may contain JSON like {"tags": ["ai", "notes"]} or be empty. - Returns empty list if no tags are defined. + The file may contain JSON like {"tags": ["ai"], "recommended": true} or be empty. + Returns empty dict if no metadata is defined. """ filename = repo.replace("/", "@", 1) config_file = self.config.input_dir / filename if not config_file.exists(): - return [] + return {} try: content = config_file.read_text(encoding="utf-8").strip() if not content: - return [] - data = json.loads(content) - return data.get("tags", []) + return {} + return json.loads(content) except Exception as e: - logger.debug(f"Failed to parse tags from {config_file}: {e}") - return [] + logger.debug(f"Failed to parse config from {config_file}: {e}") + return {} def _process_repo(self, repo: str) -> Optional[AddonInfo]: """Process a single repository using cache. @@ -153,11 +154,13 @@ class CacheScraper: return None # Create AddonInfo + repo_config = self._load_repo_config(repo) addon_info = AddonInfo( repo=repo, releases=releases, name=name, - tags=self._load_repo_tags(repo), + tags=repo_config.get("tags", []), + recommended=repo_config.get("recommended", False), ) # Fetch author info diff --git a/tests/unit/test_models.py b/tests/unit/test_models.py index 9860ba9..0333df2 100644 --- a/tests/unit/test_models.py +++ b/tests/unit/test_models.py @@ -211,3 +211,27 @@ class TestAddonInfo: data = {"repo": "owner/repo"} addon = AddonInfo.from_dict(data) assert addon.tags == [] + + def test_to_dict_includes_recommended_when_true(self): + """Test to_dict includes recommended field only when True.""" + addon = AddonInfo(repo="owner/repo", recommended=True) + result = addon.to_dict() + assert result["recommended"] is True + + def test_to_dict_omits_recommended_when_false(self): + """Test to_dict omits recommended key when False (backward compat).""" + addon = AddonInfo(repo="owner/repo", recommended=False) + result = addon.to_dict() + assert "recommended" not in result + + def test_from_dict_parses_recommended(self): + """Test from_dict parses recommended field.""" + data = {"repo": "owner/repo", "recommended": True} + addon = AddonInfo.from_dict(data) + assert addon.recommended is True + + def test_from_dict_recommended_defaults_to_false(self): + """Test from_dict defaults recommended to False when key absent.""" + data = {"repo": "owner/repo"} + addon = AddonInfo.from_dict(data) + assert addon.recommended is False