diff --git a/.env-sample b/.env-sample index d3f7470..1848473 100644 --- a/.env-sample +++ b/.env-sample @@ -82,16 +82,17 @@ BYPASS_PROXY_URL=http://warp:1080 # To bypass scraper IP blacklists SCRAPE_JACKETT=False # Context mode: live, background, both, false JACKETT_URL=http://127.0.0.1:9117 JACKETT_API_KEY=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX -JACKETT_INDEXERS='["EXAMPLE1_CHANGETHIS", "EXAMPLE2_CHANGETHIS"]' # Get names from https://github.com/Jackett/Jackett/tree/master/src/Jackett.Common/Definitions +JACKETT_INDEXERS=[] # Leave empty to automatically use all configured/healthy indexers. Or specify a list of indexer IDs to use (e.g. '["oxtorrent", "torrent9"]'). # Prowlarr Configuration SCRAPE_PROWLARR=False # Context mode: live, background, both, false PROWLARR_URL=http://127.0.0.1:9696 PROWLARR_API_KEY=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX -PROWLARR_INDEXERS='["EXAMPLE1_CHANGETHIS", "EXAMPLE2_CHANGETHIS"]' +PROWLARR_INDEXERS=[] # Leave empty to automatically use all configured/healthy indexers. Or specify a list of indexer IDs. # Shared Settings -INDEXER_MANAGER_TIMEOUT=60 # Max time to get search results (seconds) - Shared by both +INDEXER_MANAGER_TIMEOUT=30 # Max time to get search results (seconds) - Shared by both +INDEXER_MANAGER_UPDATE_INTERVAL=900 # Time in seconds between indexer updates (default: 900s / 15m) # ============================== # # Torrent Settings # @@ -189,6 +190,7 @@ TORBOX_API_KEY=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX SCRAPE_YGGTORRENT=False YGGTORRENT_USERNAME=username YGGTORRENT_PASSWORD=password +YGGTORRENT_PASSKEY=passkey YGGTORRENT_MAX_CONCURRENT_PAGES=5 # ============================== # diff --git a/comet/api/app.py b/comet/api/app.py index f7ebd48..45cab68 100644 --- a/comet/api/app.py +++ b/comet/api/app.py @@ -19,6 +19,7 @@ from comet.core.logger import logger from comet.core.models import settings from comet.services.anime import anime_mapper from comet.services.bandwidth import bandwidth_monitor +from comet.services.indexer_manager import indexer_manager from comet.services.torrent_manager import (add_torrent_queue, torrent_update_queue) from comet.services.trackers import download_best_trackers @@ -66,9 +67,18 @@ async def lifespan(app: FastAPI): if settings.BACKGROUND_SCRAPER_ENABLED: background_scraper_task = asyncio.create_task(background_scraper.start()) + # Start indexer manager + indexer_manager_task = asyncio.create_task(indexer_manager.run()) + try: yield finally: + indexer_manager_task.cancel() + try: + await indexer_manager_task + except asyncio.CancelledError: + pass + if background_scraper_task: await background_scraper.stop() background_scraper_task.cancel() diff --git a/comet/core/logger.py b/comet/core/logger.py index 6737273..a434750 100644 --- a/comet/core/logger.py +++ b/comet/core/logger.py @@ -196,7 +196,12 @@ def log_startup_info(settings): jackett_info = "" if settings.is_any_context_enabled(settings.SCRAPE_JACKETT): - jackett_info = f" - {settings.JACKETT_URL} - Indexers: {', '.join(settings.JACKETT_INDEXERS)}" + indexers = ( + ", ".join(settings.JACKETT_INDEXERS) + if settings.JACKETT_INDEXERS + else "All Configured/Healthy" + ) + jackett_info = f" - {settings.JACKETT_URL} - Indexers: {indexers}" logger.log( "COMET", f"Jackett Scraper: {settings.format_scraper_mode(settings.SCRAPE_JACKETT)}{jackett_info}", @@ -204,13 +209,22 @@ def log_startup_info(settings): prowlarr_info = "" if settings.is_any_context_enabled(settings.SCRAPE_PROWLARR): - prowlarr_info = f" - {settings.PROWLARR_URL} - Indexers: {', '.join(settings.PROWLARR_INDEXERS)}" + indexers = ( + ", ".join(settings.PROWLARR_INDEXERS) + if settings.PROWLARR_INDEXERS + else "All Configured/Healthy" + ) + prowlarr_info = f" - {settings.PROWLARR_URL} - Indexers: {indexers}" logger.log( "COMET", f"Prowlarr Scraper: {settings.format_scraper_mode(settings.SCRAPE_PROWLARR)}{prowlarr_info}", ) logger.log("COMET", f"Indexer Manager Timeout: {settings.INDEXER_MANAGER_TIMEOUT}s") + logger.log( + "COMET", + f"Indexer Manager Update Interval: {settings.INDEXER_MANAGER_UPDATE_INTERVAL}s", + ) logger.log("COMET", f"Get Torrent Timeout: {settings.GET_TORRENT_TIMEOUT}s") logger.log("COMET", f"Magnet Resolve Timeout: {settings.MAGNET_RESOLVE_TIMEOUT}s") logger.log( @@ -328,7 +342,7 @@ def log_startup_info(settings): ) yggtorrent_info = ( - f" - Username: {settings.YGGTORRENT_USERNAME} - Password: {settings.YGGTORRENT_PASSWORD}" + f" - Username: {settings.YGGTORRENT_USERNAME} - Password: {settings.YGGTORRENT_PASSWORD} - Passkey: {settings.YGGTORRENT_PASSKEY}" if settings.is_any_context_enabled(settings.SCRAPE_YGGTORRENT) else "" ) diff --git a/comet/core/models.py b/comet/core/models.py index 4fd20a1..461d2b2 100644 --- a/comet/core/models.py +++ b/comet/core/models.py @@ -55,6 +55,7 @@ class AppSettings(BaseSettings): INDEXER_MANAGER_MODE: Union[bool, str] = "both" INDEXER_MANAGER_TIMEOUT: Optional[int] = 30 INDEXER_MANAGER_INDEXERS: List[str] = [] + INDEXER_MANAGER_UPDATE_INTERVAL: Optional[int] = 900 SCRAPE_JACKETT: Union[bool, str] = False JACKETT_URL: Optional[str] = "http://127.0.0.1:9117" JACKETT_API_KEY: Optional[str] = None @@ -102,6 +103,7 @@ class AppSettings(BaseSettings): SCRAPE_YGGTORRENT: Union[bool, str] = False YGGTORRENT_USERNAME: Optional[str] = None YGGTORRENT_PASSWORD: Optional[str] = None + YGGTORRENT_PASSKEY: Optional[str] = None YGGTORRENT_MAX_CONCURRENT_PAGES: Optional[int] = 5 CUSTOM_HEADER_HTML: Optional[str] = None PROXY_DEBRID_STREAM: Optional[bool] = False diff --git a/comet/scrapers/prowlarr.py b/comet/scrapers/prowlarr.py index fc5dfb3..c82c5d1 100644 --- a/comet/scrapers/prowlarr.py +++ b/comet/scrapers/prowlarr.py @@ -95,28 +95,11 @@ class ProwlarrScraper(BaseScraper): ) try: - indexers = [indexer.lower() for indexer in settings.PROWLARR_INDEXERS] - - get_indexers = await self.session.get( - f"{self.url}/api/v1/indexer", - headers={"X-Api-Key": settings.PROWLARR_API_KEY}, - timeout=INDEXER_TIMEOUT, - ) - get_indexers = await get_indexers.json() - - indexers_id = [] - for indexer in get_indexers: - if ( - indexer["name"].lower() in indexers - or indexer["definitionName"].lower() in indexers - ): - indexers_id.append(indexer["id"]) - tasks = [] for query in queries: tasks.append( self.session.get( - f"{self.url}/api/v1/search?query={query}&indexerIds={'&indexerIds='.join(str(indexer_id) for indexer_id in indexers_id)}&type=search", + f"{self.url}/api/v1/search?query={query}&indexerIds={'&indexerIds='.join(str(indexer_id) for indexer_id in settings.PROWLARR_INDEXERS)}&type=search", headers={"X-Api-Key": settings.PROWLARR_API_KEY}, timeout=INDEXER_TIMEOUT, ) diff --git a/comet/scrapers/yggtorrent.py b/comet/scrapers/yggtorrent.py index 6bb8d98..32259dd 100644 --- a/comet/scrapers/yggtorrent.py +++ b/comet/scrapers/yggtorrent.py @@ -9,9 +9,11 @@ from comet.core.logger import logger from comet.core.models import settings from comet.scrapers.base import BaseScraper from comet.scrapers.models import ScrapeRequest -from comet.services.torrent_manager import extract_torrent_metadata from comet.utils.formatting import size_to_bytes +YGG_URL = "https://www.yggtorrent.org" +TRACKER_URL = "http://tracker.p2p-world.net:8080" + LOGIN_PAGE = "/auth/login" LOGIN_PROCESS_PAGE = "/auth/process_login" @@ -22,45 +24,18 @@ NAME_PATTERN = re.compile(r']+href="([^"]+)"[^>]*>(.*)', re.DOTALL) class YGGTorrentScraper(BaseScraper): - _domain = None _session = None _lock = asyncio.Lock() def __init__(self, manager, session: aiohttp.ClientSession): super().__init__(manager, session) - @classmethod - async def _get_domain(cls): - if cls._domain: - return cls._domain - - try: - async with requests.AsyncSession(impersonate="chrome") as session: - response = await session.get("https://ygg.re", allow_redirects=False) - if "location" in response.headers: - location = response.headers["location"] - domain = urlparse(location).netloc - logger.info(f"Resolved YGG domain to: {domain}") - cls._domain = domain - return domain - elif response.status_code == 200: - cls._domain = urlparse(response.url).netloc - return cls._domain - except Exception as e: - logger.error(f"Error resolving YGG domain: {e}") - return None - return None - @classmethod async def _ensure_session(cls): async with cls._lock: if cls._session: - domain = await cls._get_domain() - if not domain: - return False - try: - response = await cls._session.get(f"https://{domain}/") + response = await cls._session.get(f"{YGG_URL}/") if response.status_code == 200 and "Déconnexion" in response.text: return True except Exception: @@ -70,15 +45,13 @@ class YGGTorrentScraper(BaseScraper): await cls._session.close() cls._session = None - domain = await cls._get_domain() - if not domain: - return False + domain = urlparse(YGG_URL).netloc session = requests.AsyncSession(impersonate="chrome") session.cookies.set("account_created", "true", domain=domain) try: - response = await session.get(f"https://{domain}{LOGIN_PAGE}") + response = await session.get(f"{YGG_URL}{LOGIN_PAGE}") if response.status_code != 200: logger.error(f"Failed to get login page: {response.status_code}") await session.close() @@ -90,7 +63,7 @@ class YGGTorrentScraper(BaseScraper): } response = await session.post( - f"https://{domain}{LOGIN_PROCESS_PAGE}", data=payload + f"{YGG_URL}{LOGIN_PROCESS_PAGE}", data=payload ) if response.status_code != 200: @@ -98,7 +71,7 @@ class YGGTorrentScraper(BaseScraper): await session.close() return False - response = await session.get(f"https://{domain}/") + response = await session.get(f"{YGG_URL}/") if "Déconnexion" in response.text or "logout" in response.text.lower(): logger.info("Successfully logged in to YGGTorrent.") cls._session = session @@ -112,53 +85,63 @@ class YGGTorrentScraper(BaseScraper): await session.close() return False - async def _download_torrent(self, url): + async def _process_torrent(self, url, title, seeders, size): try: response = await self._session.get(url) - if response.status_code == 200: - return response.content - logger.warning(f"Failed to download torrent file: {response.status_code}") - except Exception as e: - logger.warning(f"Exception downloading torrent file: {e}") - return None - - async def _process_torrent(self, domain, torrent_id, title, seeders, size): - download_url = f"https://{domain}/engine/download_torrent?id={torrent_id}" - - torrent_content = await self._download_torrent(download_url) - - results = [] - if torrent_content: - try: - metadata = extract_torrent_metadata(torrent_content) - if metadata: - for file in metadata["files"]: - results.append( - { - "title": file["name"], - "infoHash": metadata["info_hash"].lower(), - "fileIndex": file["index"], - "seeders": seeders, - "size": file["size"], - "tracker": "YGGTorrent", - "sources": metadata["announce_list"], - } - ) - else: - logger.warning( - f"Failed to extract metadata for torrent {torrent_id}" - ) - except Exception as e: + if response.status_code != 200: logger.warning( - f"Exception extracting metadata for torrent {torrent_id}: {e}" + f"Failed to fetch torrent page {url}: {response.status_code}" ) - else: - logger.warning(f"Failed to download torrent content for {torrent_id}") + return [] - return results + html_content = response.text - async def _scrape_page(self, domain, query, offset, semaphore): - url = f"https://{domain}/engine/search?name={query}&do=search&page={offset}&category=2145" + hash_match = re.search( + r"Hash\s*\s*