# This file is part of Senator Sperma # Licensed under the GPL-3.0 License # Copyright (C) 2026 Salmonik @ salmonik@tuta.io # https://codeberg.org/salmonik/senator-sperma import json import logging import aiohttp import os from dotenv import load_dotenv from cogs.utils.settings import get_baseurl load_dotenv() logger = logging.getLogger(__name__) class UniversalSearcher: """Handles searching for files via copyparty API.""" @staticmethod async def search_copyparty(query: str, max_results: int = 25): """Search for files using copyparty API (jPOST method). Args: query: Search query string max_results: Maximum number of results to return Returns: List of matching file URLs """ if not query: return [] query = query.strip() logger.info(f"Starting copyparty search for query: '{query}'") query = f'name like "*{query}*"' try: baseurl = get_baseurl() password = os.getenv("COPYPARTY_PASSWORD") async with aiohttp.ClientSession() as session: headers = {"Content-Type": "application/json"} if password: headers["PW"] = password async with session.post(f"{baseurl}/?srch", headers=headers, data=json.dumps({"n": max_results, "q": query})) as response: if response.status == 200: data = await response.json() logger.info(f"Copyparty response: {data}") files = UniversalSearcher._extract_files_from_response(data) results = UniversalSearcher._convert_files_to_urls(files, baseurl, max_results) logger.info(f"Copyparty search complete. Found {len(results)} matches.") return results else: logger.error(f"Copyparty API returned status {response.status}") return [] except aiohttp.ClientError as e: logger.error(f"HTTP error during copyparty search: {e}") return [] except Exception as e: logger.error(f"Error during copyparty search: {str(e)}", exc_info=True) return [] @staticmethod def _extract_files_from_response(data): """Extract files list from copyparty API response.""" if isinstance(data, dict): if 'hits' in data: return data['hits'] elif 'files' in data: return data['files'] elif 'results' in data: return data['results'] else: return data if isinstance(data, list) else [] elif isinstance(data, list): return data else: return [] @staticmethod def _convert_files_to_urls(files, baseurl, max_results): """Convert file info objects to URLs.""" results = [] for file_info in files[:max_results]: if isinstance(file_info, dict): file_url = UniversalSearcher._extract_url_from_dict(file_info, baseurl) if file_url: results.append(file_url) elif isinstance(file_info, str): if file_info.startswith('http'): results.append(file_info) else: results.append(f"{baseurl}/{file_info.lstrip('/')}") return results @staticmethod def _extract_url_from_dict(file_info, baseurl): """Extract URL from file info dictionary.""" if 'url' in file_info: return file_info['url'] elif 'rp' in file_info: return f"{baseurl}/{file_info['rp']}" elif 'path' in file_info: return f"{baseurl}/{file_info['path'].lstrip('/')}" elif 'name' in file_info: return f"{baseurl}/{file_info['name'].lstrip('/')}" else: return None @staticmethod async def search_copyparty_autocomplete(query: str, max_results: int = 10): """Wrapper for autocomplete searches using copyparty. Args: query: Search query string max_results: Maximum number of results to return Returns: List of matching file URLs """ if not query: return [] return await UniversalSearcher.search_copyparty(query, max_results)