From ec2aa2bb170b4bef70dee0ce452f94ab53d44f94 Mon Sep 17 00:00:00 2001 From: g0ldyy <153996346+g0ldyy@users.noreply.github.com> Date: Sat, 27 Dec 2025 19:18:16 +0100 Subject: [PATCH] feat: add ChillLink API endpoints and refactor stream description formatting logic --- comet/api/app.py | 12 ++- comet/api/endpoints/chilllink.py | 108 ++++++++++++++++++++++++ comet/api/endpoints/manifest.py | 4 +- comet/api/endpoints/playback.py | 2 +- comet/api/endpoints/stream.py | 31 ++++--- comet/utils/formatting.py | 138 +++++++++++++++++-------------- 6 files changed, 217 insertions(+), 78 deletions(-) create mode 100644 comet/api/endpoints/chilllink.py diff --git a/comet/api/app.py b/comet/api/app.py index 1ad38d3..1f20dc5 100644 --- a/comet/api/app.py +++ b/comet/api/app.py @@ -9,7 +9,8 @@ from fastapi.staticfiles import StaticFiles from starlette.middleware.base import BaseHTTPMiddleware from starlette.requests import Request -from comet.api.endpoints import admin, base, config, manifest, playback +from comet.api.endpoints import (admin, base, chilllink, config, manifest, + playback) from comet.api.endpoints import stream as streams_router from comet.background_scraper.worker import background_scraper from comet.core.database import (cleanup_expired_locks, @@ -118,8 +119,12 @@ tags_metadata = [ "description": "Endpoints for configuring Comet.", }, { - "name": "Stremio Add-on", - "description": "Standard Stremio add-on endpoints.", + "name": "Stremio", + "description": "Standard Stremio endpoints.", + }, + { + "name": "ChillLink", + "description": "Chillio specific endpoints.", }, { "name": "Admin", @@ -153,3 +158,4 @@ app.include_router(manifest.router) app.include_router(admin.router) app.include_router(playback.router) app.include_router(streams_router.streams) +app.include_router(chilllink.router) diff --git a/comet/api/endpoints/chilllink.py b/comet/api/endpoints/chilllink.py new file mode 100644 index 0000000..729fbc5 --- /dev/null +++ b/comet/api/endpoints/chilllink.py @@ -0,0 +1,108 @@ +import random +import string +from typing import Optional + +from fastapi import APIRouter, BackgroundTasks, Query, Request + +from comet.api.endpoints.stream import stream as get_streams +from comet.core.config_validation import config_check +from comet.core.models import settings +from comet.debrid.manager import get_debrid_extension + +router = APIRouter() + + +@router.get( + "/manifest", + tags=["ChillLink"], + summary="Add-on Manifest", + description="Returns the add-on manifest.", +) +@router.get( + "/{b64config}/manifest", + tags=["ChillLink"], + summary="Add-on Manifest", + description="Returns the add-on manifest with existing configuration.", +) +async def chilllink_manifest(request: Request, b64config: str = None): + config = config_check(b64config) + + manifest = { + "id": f"{settings.ADDON_ID}.{''.join(random.choice(string.ascii_letters) for _ in range(4))}", + "version": "2.0.0", + "description": "Chillio's fastest debrid search add-on.", + "supported_endpoints": {"feeds": None, "streams": "/streams"}, + } + + debrid_extension = get_debrid_extension(config["debridService"]) + manifest["name"] = ( + f"{settings.ADDON_NAME}{(' | ' + debrid_extension) if debrid_extension != 'TORRENT' else ''}" + ) + + return manifest + + +@router.get( + "/streams", + tags=["ChillLink"], + summary="Stream Provider", + description="Returns a list of streams for the specified media.", +) +@router.get( + "/{b64config}/streams", + tags=["ChillLink"], + summary="Stream Provider", + description="Returns a list of streams for the specified media with existing configuration.", +) +async def chilllink_streams( + request: Request, + background_tasks: BackgroundTasks, + imdbID: str = Query(...), + type: str = Query(...), + season: Optional[int] = Query(None), + episode: Optional[int] = Query(None), + b64config: Optional[str] = None, +): + config = config_check(b64config) + if config["debridService"] == "torrent": + return { + "sources": [ + { + "id": "comet.fast", + "title": "You need to configure a debrid service to use Comet in Chillio.", + "url": "https://comet.fast", + "metadata": [], + } + ] + } + + if type == "movie": + media_id = imdbID + elif type == "series": + media_id = f"{imdbID}:{season}:{episode}" + else: + return {"sources": []} + + stremio_response = await get_streams( + request=request, + media_type=type, + media_id=media_id, + background_tasks=background_tasks, + b64config=b64config, + chilllink=True, + ) + + stremio_streams = stremio_response.get("streams", []) + + sources = [] + for stream in stremio_streams: + sources.append( + { + "id": stream["behaviorHints"]["bingeGroup"], + "title": stream["behaviorHints"]["filename"], + "url": stream["url"], + "metadata": stream["_chilllink"], + } + ) + + return {"sources": sources} diff --git a/comet/api/endpoints/manifest.py b/comet/api/endpoints/manifest.py index acf2f76..bb8063a 100644 --- a/comet/api/endpoints/manifest.py +++ b/comet/api/endpoints/manifest.py @@ -12,13 +12,13 @@ router = APIRouter() @router.get( "/manifest.json", - tags=["Stremio Add-on"], + tags=["Stremio"], summary="Add-on Manifest", description="Returns the add-on manifest.", ) @router.get( "/{b64config}/manifest.json", - tags=["Stremio Add-on"], + tags=["Stremio"], summary="Add-on Manifest", description="Returns the add-on manifest with existing configuration.", ) diff --git a/comet/api/endpoints/playback.py b/comet/api/endpoints/playback.py index 4515a24..eef73b8 100644 --- a/comet/api/endpoints/playback.py +++ b/comet/api/endpoints/playback.py @@ -18,7 +18,7 @@ router = APIRouter() @router.get( "/{b64config}/playback/{hash}/{index}/{season}/{episode}/{torrent_name:path}", - tags=["Stremio Add-on"], + tags=["Stremio"], summary="Playback Proxy", description="Proxies the playback request to the Debrid service or returns a cached link.", ) diff --git a/comet/api/endpoints/stream.py b/comet/api/endpoints/stream.py index 65a745a..7c34911 100644 --- a/comet/api/endpoints/stream.py +++ b/comet/api/endpoints/stream.py @@ -13,7 +13,8 @@ from comet.metadata.manager import MetadataScraper from comet.services.debrid import DebridService from comet.services.lock import DistributedLock, is_scrape_in_progress from comet.services.orchestration import TorrentManager -from comet.utils.formatting import format_title +from comet.utils.formatting import (format_chilllink, format_title, + get_formatted_components) from comet.utils.network import get_client_ip from comet.utils.parsing import parse_media_id @@ -116,13 +117,13 @@ async def wait_for_scrape_completion(media_id: str, context: str = ""): @streams.get( "/stream/{media_type}/{media_id}.json", - tags=["Stremio Add-on"], + tags=["Stremio"], summary="Stream Provider", description="Returns a list of streams for the specified media.", ) @streams.get( "/{b64config}/stream/{media_type}/{media_id}.json", - tags=["Stremio Add-on"], + tags=["Stremio"], summary="Stream Provider", description="Returns a list of streams for the specified media with existing configuration.", ) @@ -132,6 +133,7 @@ async def stream( media_id: str, background_tasks: BackgroundTasks, b64config: str = None, + chilllink: bool = False, ): if "tmdb:" in media_id: return {"streams": []} @@ -453,16 +455,18 @@ async def stream( ) torrent_title = torrent["title"] + formatted_components = get_formatted_components( + rtn_data, + torrent_title, + torrent["seeders"], + torrent["size"], + torrent["tracker"], + config["resultFormat"], + ) + the_stream = { "name": f"[{debrid_extension}{debrid_emoji}] Comet {rtn_data.resolution}", - "description": format_title( - rtn_data, - torrent_title, - torrent["seeders"], - torrent["size"], - torrent["tracker"], - config["resultFormat"], - ), + "description": format_title(formatted_components), "behaviorHints": { "bingeGroup": "comet|" + info_hash, "videoSize": torrent["size"], @@ -470,6 +474,11 @@ async def stream( }, } + if chilllink: + the_stream["_chilllink"] = format_chilllink( + formatted_components, torrent["cached"] + ) + if debrid_service == "torrent": the_stream["infoHash"] = info_hash diff --git a/comet/utils/formatting.py b/comet/utils/formatting.py index 05c1021..fd0d966 100644 --- a/comet/utils/formatting.py +++ b/comet/utils/formatting.py @@ -180,7 +180,7 @@ def format_group_info(data: ParsedData): return " • ".join(group_parts) if group_parts else "" -def format_title( +def get_formatted_components( data: ParsedData, ttitle: str, seeders: int, @@ -189,74 +189,90 @@ def format_title( result_format: list, ): has_all = "all" in result_format + components = {} + + if has_all or "title" in result_format: + components["title"] = f"📄 {ttitle}" + + if has_all or "video_info" in result_format: + info = format_video_info(data) + if info: + components["video"] = f"📹 {info}" + + if has_all or "audio_info" in result_format: + info = format_audio_info(data) + if info: + components["audio"] = f"🔊 {info}" + + if has_all or "quality_info" in result_format: + info = format_quality_info(data) + if info: + components["quality"] = f"⭐ {info}" + + if has_all or "release_group" in result_format: + info = format_group_info(data) + if info: + components["group"] = f"🏷️ {info}" + + if (has_all or "seeders" in result_format) and seeders is not None: + components["seeders"] = f"👤 {seeders}" + + if has_all or "size" in result_format: + components["size"] = f"💾 {format_bytes(size)}" + + if has_all or "tracker" in result_format: + components["tracker"] = f"🔎 {tracker}" + + if ( + (has_all or "languages" in result_format) + and hasattr(data, "languages") + and data.languages + ): + formatted_languages = "/".join( + get_language_emoji(language) for language in data.languages + ) + components["languages"] = formatted_languages + + return components + + +def format_title(components: dict): lines = [] - show_title = has_all or "title" in result_format - if show_title: - lines.append(f"📄 {ttitle}") + if "title" in components: + lines.append(components["title"]) - show_video = has_all or "video_info" in result_format - show_audio = has_all or "audio_info" in result_format - show_quality = has_all or "quality_info" in result_format - show_group = has_all or "release_group" in result_format + video_audio = [components[k] for k in ["video", "audio"] if k in components] + if video_audio: + lines.append(" | ".join(video_audio)) - video_audio_parts = [] + quality_group = [components[k] for k in ["quality", "group"] if k in components] + if quality_group: + lines.append(" | ".join(quality_group)) - if show_video: - video_info = format_video_info(data) - if video_info: - video_audio_parts.append(f"📹 {video_info}") + info = [components[k] for k in ["seeders", "size", "tracker"] if k in components] + if info: + lines.append(" ".join(info)) - if show_audio: - audio_info = format_audio_info(data) - if audio_info: - video_audio_parts.append(f"🔊 {audio_info}") - - if video_audio_parts: - lines.append(" | ".join(video_audio_parts)) - - quality_parts = [] - - if show_quality: - quality_info = format_quality_info(data) - if quality_info: - quality_parts.append(f"⭐ {quality_info}") - - if show_group: - groups = format_group_info(data) - if groups: - quality_parts.append(f"🏷️ {groups}") - - if quality_parts: - lines.append(" | ".join(quality_parts)) - - show_seeders = has_all or "seeders" in result_format - show_size = has_all or "size" in result_format - show_tracker = has_all or "tracker" in result_format - - info_parts = [] - - if show_seeders and seeders is not None: - info_parts.append(f"👤 {seeders}") - - if show_size: - info_parts.append(f"💾 {format_bytes(size)}") - - if show_tracker: - info_parts.append(f"🔎 {tracker}") - - if info_parts: - lines.append(" ".join(info_parts)) - - show_languages = has_all or "languages" in result_format - if show_languages: - if hasattr(data, "languages") and data.languages: - formatted_languages = "/".join( - get_language_emoji(language) for language in data.languages - ) - lines.append(f"{formatted_languages}") + if "languages" in components: + lines.append(components["languages"]) if not lines: return "Empty result format configuration" return "\n".join(lines) + + +def format_chilllink(components: dict, cached: bool): + metadata = [] + + if cached: + metadata.append("⚡ Instant") + else: + metadata.append("⬇️ Not Cached") + + for key, value in components.items(): + if key != "title": + metadata.append(value) + + return metadata