Files

229 lines
6.6 KiB
Python

"""
Unified package service for ArchStore.
Merges pacman and AUR results, handles deduplication and ranking.
"""
import asyncio
from typing import Optional
from services import pacman_service, aur_service
from database.db import db
from utils.sanitize import sanitize_search_query, sanitize_package_name, validate_source_filter
# Category definitions — maps friendly names to pacman groups
CATEGORIES = {
"Development": {
"icon": "code",
"description": "Programming tools, compilers, and IDEs",
"groups": ["base-devel"],
"keywords": ["gcc", "git", "python", "nodejs", "rust", "go", "vim", "neovim", "code"],
},
"System": {
"icon": "monitor",
"description": "Core system utilities and tools",
"groups": ["base", "sys-utils"],
"keywords": ["systemd", "kernel", "grub", "filesystem", "coreutils"],
},
"Network": {
"icon": "wifi",
"description": "Networking tools, browsers, and servers",
"groups": ["network"],
"keywords": ["firefox", "chromium", "curl", "wget", "nginx", "ssh"],
},
"Multimedia": {
"icon": "music",
"description": "Audio, video, and image tools",
"groups": ["multimedia"],
"keywords": ["vlc", "mpv", "ffmpeg", "gimp", "audacity", "obs"],
},
"Games": {
"icon": "gamepad-2",
"description": "Games and gaming tools",
"groups": ["games"],
"keywords": ["steam", "lutris", "wine", "gamemode"],
},
"Desktop": {
"icon": "layout-dashboard",
"description": "Desktop environments and window managers",
"groups": ["gnome", "kde-applications", "xfce4"],
"keywords": ["gnome", "kde", "xfce", "i3", "sway", "hyprland"],
},
"Fonts": {
"icon": "type",
"description": "Fonts and typography",
"groups": ["fonts"],
"keywords": ["ttf", "otf", "nerd-fonts", "noto"],
},
"Security": {
"icon": "shield",
"description": "Security and privacy tools",
"groups": [],
"keywords": ["firewall", "gpg", "openssl", "wireguard", "tor"],
},
}
async def search_packages(query: str, source: str = "all") -> list[dict]:
"""
Search packages from pacman, AUR, or both.
Results are deduplicated, merged, and ranked.
"""
query = sanitize_search_query(query)
source = validate_source_filter(source)
# Check cache first
cached = await db.get_cached_search(query, source)
if cached:
return cached
results = []
if source in ("all", "pacman"):
pacman_task = pacman_service.search_packages(query)
else:
pacman_task = asyncio.coroutine(lambda: [])()
if source in ("all", "aur"):
aur_task = aur_service.search_packages(query)
else:
aur_task = asyncio.coroutine(lambda: [])()
# Run both searches concurrently
if source == "all":
pacman_results, aur_results = await asyncio.gather(
pacman_service.search_packages(query),
aur_service.search_packages(query),
return_exceptions=True,
)
if isinstance(pacman_results, Exception):
pacman_results = []
if isinstance(aur_results, Exception):
aur_results = []
results = _merge_results(pacman_results, aur_results)
elif source == "pacman":
results = await pacman_service.search_packages(query)
elif source == "aur":
results = await aur_service.search_packages(query)
# Cache results
await db.set_cached_search(query, source, results)
return results
async def get_package_info(name: str) -> Optional[dict]:
"""Get detailed package info, trying pacman first then AUR."""
name = sanitize_package_name(name)
# Check cache
cached = await db.get_cached_package(name)
if cached:
return cached
# Try pacman first
info = await pacman_service.get_package_info(name)
if not info:
# Try AUR
info = await aur_service.get_package_info(name)
if not info:
# Check if installed locally
info = await pacman_service.get_installed_info(name)
if info:
await db.set_cached_package(name, info)
return info
async def install_package(name: str) -> dict:
"""Install a package, auto-detecting source."""
name = sanitize_package_name(name)
# Try pacman first
info = await pacman_service.get_package_info(name)
if info:
return await pacman_service.install_package(name)
# Fall back to AUR via yay
return await aur_service.install_package(name)
async def remove_package(name: str) -> dict:
"""Remove an installed package."""
name = sanitize_package_name(name)
return await pacman_service.remove_package(name)
async def list_installed() -> list[dict]:
"""List all installed packages."""
return await pacman_service.list_installed()
async def check_updates() -> list[dict]:
"""Check for all available updates (pacman + AUR)."""
pacman_updates, aur_updates = await asyncio.gather(
pacman_service.check_updates(),
aur_service.check_updates(),
return_exceptions=True,
)
if isinstance(pacman_updates, Exception):
pacman_updates = []
if isinstance(aur_updates, Exception):
aur_updates = []
return pacman_updates + aur_updates
async def get_categories() -> list[dict]:
"""Get list of package categories."""
return [
{"name": name, **data}
for name, data in CATEGORIES.items()
]
async def get_category_packages(category: str) -> list[dict]:
"""Get packages for a specific category using search."""
cat = CATEGORIES.get(category)
if not cat:
return []
# Search using category keywords
all_results = []
for keyword in cat.get("keywords", [])[:5]:
try:
results = await search_packages(keyword, "all")
all_results.extend(results)
except Exception:
continue
# Deduplicate by name
seen = set()
unique = []
for pkg in all_results:
if pkg["name"] not in seen:
seen.add(pkg["name"])
unique.append(pkg)
return unique[:50]
def _merge_results(pacman: list[dict], aur: list[dict]) -> list[dict]:
"""Merge and deduplicate pacman + AUR results."""
seen = {}
# Pacman results take priority
for pkg in pacman:
seen[pkg["name"]] = pkg
# Add AUR results if not already from pacman
for pkg in aur:
if pkg["name"] not in seen:
seen[pkg["name"]] = pkg
# Sort: installed first, then by name
results = list(seen.values())
results.sort(key=lambda p: (not p.get("installed", False), p.get("name", "")))
return results