From 23ab2f44b187f72048376bd3637395d781344276 Mon Sep 17 00:00:00 2001 From: g0ldyy <153996346+g0ldyy@users.noreply.github.com> Date: Sun, 11 Jan 2026 13:45:05 +0100 Subject: [PATCH 1/5] feat: enhance caching policies and improve manifest handling --- comet/api/endpoints/chilllink.py | 4 +- comet/api/endpoints/manifest.py | 9 +- comet/api/endpoints/stream.py | 106 +- comet/core/config_validation.py | 55 + comet/templates/admin_dashboard.html | 85 +- comet/templates/admin_login.html | 23 +- comet/templates/index.html | 1879 +++++++++++++++----------- comet/utils/cache.py | 14 + 8 files changed, 1280 insertions(+), 895 deletions(-) diff --git a/comet/api/endpoints/chilllink.py b/comet/api/endpoints/chilllink.py index bd0c852..b6b75a3 100644 --- a/comet/api/endpoints/chilllink.py +++ b/comet/api/endpoints/chilllink.py @@ -1,5 +1,3 @@ -import random -import string from typing import Optional from fastapi import APIRouter, BackgroundTasks, Query, Request @@ -28,7 +26,7 @@ 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))}", + "id": settings.ADDON_ID, "version": "2.0.0", "description": "Chillio's fastest debrid search add-on.", "supported_endpoints": {"feeds": None, "streams": "/streams"}, diff --git a/comet/api/endpoints/manifest.py b/comet/api/endpoints/manifest.py index 5a0e3b6..e53a8a0 100644 --- a/comet/api/endpoints/manifest.py +++ b/comet/api/endpoints/manifest.py @@ -1,6 +1,3 @@ -import random -import string - from fastapi import APIRouter, Request from comet.core.config_validation import config_check @@ -27,7 +24,7 @@ router = APIRouter() ) async def manifest(request: Request, b64config: str = None): base_manifest = { - "id": f"{settings.ADDON_ID}.{''.join(random.choice(string.ascii_letters) for _ in range(4))}", + "id": settings.ADDON_ID, "description": "Stremio's fastest torrent/debrid search add-on.", "version": "2.0.0", "catalogs": [], @@ -58,9 +55,7 @@ async def manifest(request: Request, b64config: str = None): ) if settings.HTTP_CACHE_ENABLED: - base_manifest_etag_data = base_manifest.copy() - base_manifest_etag_data.pop("id", None) - etag = generate_etag(base_manifest_etag_data) + etag = generate_etag(base_manifest) if check_etag_match(request, etag): return not_modified_response(etag) diff --git a/comet/api/endpoints/stream.py b/comet/api/endpoints/stream.py index eaef725..fd33f1f 100644 --- a/comet/api/endpoints/stream.py +++ b/comet/api/endpoints/stream.py @@ -5,7 +5,7 @@ from urllib.parse import quote import aiohttp from fastapi import APIRouter, BackgroundTasks, Request -from comet.core.config_validation import config_check +from comet.core.config_validation import config_check, is_default_config from comet.core.logger import logger from comet.core.models import database, settings, trackers from comet.debrid.exceptions import DebridAuthError @@ -31,6 +31,7 @@ def _build_stream_response( request: Request, content: dict, is_public: bool = False, + is_empty: bool = False, vary_headers: list = None, ): if not settings.HTTP_CACHE_ENABLED: @@ -41,7 +42,10 @@ def _build_stream_response( if check_etag_match(request, etag): return not_modified_response(etag) - if is_public: + if is_empty: + cache_policy = CachePolicies.empty_results() + vary = ["Accept", "Accept-Encoding"] + elif is_public: cache_policy = CachePolicies.public_torrents() vary = ["Accept", "Accept-Encoding"] else: @@ -177,12 +181,12 @@ async def stream( if media_type not in ["movie", "series"]: return _build_stream_response( - request, {"streams": []}, is_public=is_public_request + request, {"streams": []}, is_public=True, is_empty=True ) if "tmdb:" in media_id: return _build_stream_response( - request, {"streams": []}, is_public=is_public_request + request, {"streams": []}, is_public=True, is_empty=True ) media_id = media_id.replace("imdb_id:", "") @@ -198,7 +202,12 @@ async def stream( } ] } - return error_response + return _build_stream_response( + request, error_response, is_public=True, is_empty=True + ) + + if is_default_config(config): + is_public_request = True is_torrent = config["debridService"] == "torrent" if settings.DISABLE_TORRENT_STREAMS and is_torrent: @@ -238,7 +247,8 @@ async def stream( } ] }, - is_public=is_public_request, + is_public=True, + is_empty=True, ) # Check if metadata is already cached @@ -335,15 +345,20 @@ async def stream( if lock_acquired and scrape_lock: await scrape_lock.release() logger.log("SCRAPER", f"❌ Failed to fetch metadata for {media_id}") - return { - "streams": [ - { - "name": "[⚠️] Comet", - "description": "Unable to get metadata.", - "url": "https://comet.feels.legal", - } - ] - } + return _build_stream_response( + request, + { + "streams": [ + { + "name": "[⚠️] Comet", + "description": "Unable to get metadata.", + "url": "https://comet.feels.legal", + } + ] + }, + is_public=True, + is_empty=True, + ) title = metadata["title"] year = metadata["year"] @@ -449,15 +464,20 @@ async def stream( ) if len(torrent_manager.torrents) == 0: - return { - "streams": [ - { - "name": "[🔄] Comet", - "description": "Scraping in progress by another instance, please try again in a few seconds...", - "url": "https://comet.feels.legal", - } - ] - } + return _build_stream_response( + request, + { + "streams": [ + { + "name": "[🔄] Comet", + "description": "Scraping in progress by another instance, please try again in a few seconds...", + "url": "https://comet.feels.legal", + } + ] + }, + is_public=True, + is_empty=True, + ) elif is_first or cache_is_stale: # Background scrape if first search OR if cache is stale (needs refresh) @@ -527,15 +547,20 @@ async def stream( episode, ) except DebridAuthError as e: - return { - "streams": [ - { - "name": "[❌] Comet", - "description": e.display_message, - "url": "https://comet.feels.legal", - } - ] - } + return _build_stream_response( + request, + { + "streams": [ + { + "name": "[❌] Comet", + "description": e.display_message, + "url": "https://comet.feels.legal", + } + ] + }, + is_public=False, + is_empty=True, + ) if debrid_service != "torrent": cached_count = sum( @@ -641,14 +666,15 @@ async def stream( non_cached_results.append(the_stream) if sort_mixed: - return _build_stream_response( - request, - {"streams": cached_results}, - is_public=is_public_request, - ) + final_streams = cached_results + else: + final_streams = cached_results + non_cached_results + + has_results = len(final_streams) > 0 return _build_stream_response( request, - {"streams": cached_results + non_cached_results}, - is_public=is_public_request, + {"streams": final_streams}, + is_public=is_public_request or not has_results, + is_empty=not has_results, ) diff --git a/comet/core/config_validation.py b/comet/core/config_validation.py index dce9ca3..e2032a7 100644 --- a/comet/core/config_validation.py +++ b/comet/core/config_validation.py @@ -59,3 +59,58 @@ def config_check(b64config: str): return validated_config except Exception: return default_config # if it doesn't pass, return default config + + +def is_default_config(config: dict) -> bool: + if config is None: + return True + + ignored_fields = { + "debridApiKey", + "debridStreamProxyPassword", + "rtnSettings", + "rtnRanking", + "debridService", + } + + if config.get("debridService") != "torrent": + return False + + for field, default_value in default_config.items(): + if field in ignored_fields: + continue + + config_value = config.get(field) + + if field == "resolutions": + if isinstance(config_value, dict): + for res, enabled in config_value.items(): + if enabled is False: + return False + continue + + if field == "languages": + config_lang = config_value or {} + default_lang = default_value or {} + if config_lang.get("exclude", []) != default_lang.get("exclude", []): + return False + if config_lang.get("preferred", []) != default_lang.get("preferred", []): + return False + continue + + if field == "options": + config_opts = config_value or {} + default_opts = default_value or {} + for key in [ + "remove_ranks_under", + "allow_english_in_languages", + "remove_unknown_languages", + ]: + if config_opts.get(key) != default_opts.get(key): + return False + continue + + if config_value != default_value: + return False + + return True diff --git a/comet/templates/admin_dashboard.html b/comet/templates/admin_dashboard.html index 6868eae..3299924 100644 --- a/comet/templates/admin_dashboard.html +++ b/comet/templates/admin_dashboard.html @@ -1,4 +1,4 @@ - + @@ -44,9 +44,19 @@ #25292c 0%, #0c0d13 100% ); - font-family: system-ui, -apple-system, "Segoe UI", Roboto, - "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, - "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", + font-family: + system-ui, + -apple-system, + "Segoe UI", + Roboto, + "Helvetica Neue", + "Noto Sans", + "Liberation Sans", + Arial, + sans-serif, + "Apple Color Emoji", + "Segoe UI Emoji", + "Segoe UI Symbol", "Noto Color Emoji"; font-size: 1rem; font-weight: 400; @@ -308,7 +318,8 @@ border-radius: 50%; filter: drop-shadow(0 0 6px currentColor); transform: translate3d(104em, 0, 0); - animation: fall var(--fall-duration) var(--fall-delay) linear infinite, + animation: + fall var(--fall-duration) var(--fall-delay) linear infinite, tail-fade var(--tail-fade-duration) var(--fall-delay) ease-out infinite; } @@ -780,7 +791,7 @@ star.style.setProperty("--top-offset", `${randomTopOffset}vh`); star.style.setProperty( "--star-tail-length", - `${randomTailLength}em` + `${randomTailLength}em`, ); star.style.setProperty("--fall-duration", `${randomFallDuration}s`); star.style.setProperty("--fall-delay", "0s"); @@ -956,9 +967,21 @@

Torrents Analysis

-
-

Top Trackers

- +
+

Top Trackers

+ Top 5 Top 10 Top 20 @@ -1133,7 +1156,7 @@ // Logs refresh based on selected interval const interval = parseInt( - document.getElementById("refresh-interval").value + document.getElementById("refresh-interval").value, ); logsInterval = setInterval(loadLogs, interval); } @@ -1151,7 +1174,7 @@ if (document.getElementById("auto-refresh").checked) { clearInterval(logsInterval); const interval = parseInt( - document.getElementById("refresh-interval").value + document.getElementById("refresh-interval").value, ); logsInterval = setInterval(loadLogs, interval); } @@ -1189,7 +1212,7 @@ connections.length > 0 ? Math.round( connections.reduce((sum, c) => sum + c.duration, 0) / - connections.length + connections.length, ) : 0; @@ -1239,7 +1262,7 @@ ${conn.id.substring( 0, - 8 + 8, )}... @@ -1247,14 +1270,14 @@ ${ - conn.content.length > 25 - ? escapeHtml( - conn.content.substring(0, 25) - ) + "..." - : escapeHtml(conn.content) - } + conn.content.length > 25 + ? escapeHtml( + conn.content.substring(0, 25), + ) + "..." + : escapeHtml(conn.content) + } ${ @@ -1263,7 +1286,7 @@ ${Math.round( - conn.duration + conn.duration, )}s @@ -1284,7 +1307,7 @@ } - ` + `, ) .join("")} @@ -1297,7 +1320,7 @@ async function loadLogs() { try { const response = await fetch( - `/admin/api/logs?since=${lastLogTimestamp}` + `/admin/api/logs?since=${lastLogTimestamp}`, ); const data = await response.json(); @@ -1341,7 +1364,7 @@ } ${log.level} ${log.module}.${ log.function } - ${escapeHtml( - log.message + log.message, )}`; container.appendChild(logEntry); @@ -1388,7 +1411,7 @@ const hideApiLogs = document.getElementById("hide-api-logs").checked; const container = document.getElementById("logs-container"); const apiLogEntries = container.querySelectorAll( - '[data-api-log="true"]' + '[data-api-log="true"]', ); apiLogEntries.forEach((entry) => { @@ -1434,7 +1457,7 @@ const trackerContainer = document.getElementById("tracker-stats"); const limitSelect = document.getElementById("tracker-limit"); const limit = limitSelect ? limitSelect.value : "5"; - + let displayTrackers = torrentsData.by_tracker; if (limit !== "all") { displayTrackers = displayTrackers.slice(0, parseInt(limit)); @@ -1449,7 +1472,7 @@ (tracker) => `
${escapeHtml( - tracker.tracker || "Unknown" + tracker.tracker || "Unknown", )}
${tracker.count.toLocaleString()} torrents @@ -1461,7 +1484,7 @@ } avg size
- ` + `, ) .join(""); } @@ -1552,7 +1575,7 @@ function showMetricsError() { const sections = document.querySelectorAll( - ".metric-section .loading-state" + ".metric-section .loading-state", ); sections.forEach((section) => { section.innerHTML = @@ -1576,7 +1599,7 @@ (service) => `
${escapeHtml( - service.service + service.service, )}
${service.count.toLocaleString()} files @@ -1588,7 +1611,7 @@ }
- ` + `, ) .join(""); } diff --git a/comet/templates/admin_login.html b/comet/templates/admin_login.html index eeb12a2..f0c73b8 100644 --- a/comet/templates/admin_login.html +++ b/comet/templates/admin_login.html @@ -1,4 +1,4 @@ - + @@ -44,9 +44,19 @@ #25292c 0%, #0c0d13 100% ); - font-family: system-ui, -apple-system, "Segoe UI", Roboto, - "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, - "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", + font-family: + system-ui, + -apple-system, + "Segoe UI", + Roboto, + "Helvetica Neue", + "Noto Sans", + "Liberation Sans", + Arial, + sans-serif, + "Apple Color Emoji", + "Segoe UI Emoji", + "Segoe UI Symbol", "Noto Color Emoji"; font-size: 1rem; font-weight: 400; @@ -125,7 +135,8 @@ border-radius: 50%; filter: drop-shadow(0 0 6px currentColor); transform: translate3d(104em, 0, 0); - animation: fall var(--fall-duration) var(--fall-delay) linear infinite, + animation: + fall var(--fall-duration) var(--fall-delay) linear infinite, tail-fade var(--tail-fade-duration) var(--fall-delay) ease-out infinite; } @@ -235,7 +246,7 @@ star.style.setProperty("--top-offset", `${randomTopOffset}vh`); star.style.setProperty( "--star-tail-length", - `${randomTailLength}em` + `${randomTailLength}em`, ); star.style.setProperty("--fall-duration", `${randomFallDuration}s`); star.style.setProperty("--fall-delay", "0s"); diff --git a/comet/templates/index.html b/comet/templates/index.html index d5c04ae..7d33ed4 100644 --- a/comet/templates/index.html +++ b/comet/templates/index.html @@ -1,369 +1,426 @@ - + + + + + + + + + - - - - - - - - + Comet - Stremio's fastest torrent/debrid search add-on. + - Comet - Stremio's fastest torrent/debrid search add-on. - + + - - - - - - - -
- - -
-
-

- - Comet -

- - - Discord - -
- {{CUSTOM_HEADER_HTML|safe}} -
+ } -
- -
- -
- -
- -
- -
- - 4K - 2160p - Full HD - 1080p - HD - 720p - SD - 480p - LD - 360p - Unknown - -
- -
- -
- -
- -
- -
- -
- - {% set default_debrid_service = 'realdebrid' if disableTorrentStreams else 'torrent' %} -
- - {% if not disableTorrentStreams %} - Torrent - {% endif %} - TorBox - Real-Debrid - All-Debrid - Debrid-Link - Premiumize - Debrider - EasyDebrid - Offcloud - PikPak - -
- -
- - -
- -
- - Show Cached - Only - Sort Cached and Uncached Together - Allow - English in Languages - Remove Unknown Languages - Remove - Trash - + +
+ +
+ +
+ +
+ + 4K - 2160p + Full HD - 1080p + HD - 720p + SD - 480p + LD - 360p + Unknown
-
- - -
- Install - - - Attempting to add the addon to Stremio... - - - Copy Link - - - The Stremio addon link has been automatically copied. - - - -
-
-
- -
+
+ Install + + + Attempting to add the addon to Stremio... + - -
-
❤️
-

Donate to Me

-

- Comet is a passion project fueled by late nights and coffee. Your support helps keep the lights on and the - updates rolling! -

+ Copy Link + + + The Stremio addon link has been automatically copied. + -
- - - GitHub Sponsors - - - - Ko-fi - +
-
- - +
+ +
- \ No newline at end of file + +
+
+ ❤️ +
+

Donate to Me

+

+ Comet is a passion project fueled by late nights and coffee. Your + support helps keep the lights on and the updates rolling! +

+ +
+ + + GitHub Sponsors + + + + Ko-fi + +
+
+
+ + + + diff --git a/comet/utils/cache.py b/comet/utils/cache.py index 2db9160..e3a03b2 100644 --- a/comet/utils/cache.py +++ b/comet/utils/cache.py @@ -203,6 +203,20 @@ class CachePolicies: return CacheControl().public().max_age(300).s_maxage(3600) + @staticmethod + def empty_results(): + """ + For empty/temporary responses (no torrents found, processing, errors). + Short public cache to prevent spam while allowing quick retries. + """ + return ( + CacheControl() + .public() + .max_age(15) # Browser cache 15 seconds + .s_maxage(30) # CDN cache 30 seconds + .stale_if_error(60) # Serve stale on error for 1 minute + ) + @staticmethod def no_cache(): """ From 60c90bc2e842808c7fbf91d861bcf1a5916fc054 Mon Sep 17 00:00:00 2001 From: g0ldyy <153996346+g0ldyy@users.noreply.github.com> Date: Sun, 11 Jan 2026 14:40:09 +0100 Subject: [PATCH 2/5] feat: add manifest and configure page caching settings --- .env-sample | 8 ++++++++ comet/core/logger.py | 2 +- comet/core/models.py | 2 ++ comet/utils/cache.py | 16 +++++++++------- deployment/cloudflare-cache-rules.md | 15 +++++++++++++-- 5 files changed, 33 insertions(+), 10 deletions(-) diff --git a/.env-sample b/.env-sample index 83aa61f..66afc91 100644 --- a/.env-sample +++ b/.env-sample @@ -290,3 +290,11 @@ HTTP_CACHE_PRIVATE_STREAMS_TTL=60 # 1 minute (recommended: 30-120) # Stale-While-Revalidate: serve stale content while fetching fresh in background # Improves perceived performance - users get instant response with cached data HTTP_CACHE_STALE_WHILE_REVALIDATE=60 # 1 minute + +# MANIFEST TTL (for /manifest.json and /{config}/manifest.json) +# Manifest rarely changes, can be cached longer +HTTP_CACHE_MANIFEST_TTL=86400 # 24 hours (recommended: 3600-86400) + +# CONFIGURE page TTL (for /configure and /{config}/configure) +# Static page, can be cached for a long time +HTTP_CACHE_CONFIGURE_TTL=86400 # 24 hours (recommended: 3600-86400) diff --git a/comet/core/logger.py b/comet/core/logger.py index 30be3d4..f42e301 100644 --- a/comet/core/logger.py +++ b/comet/core/logger.py @@ -436,7 +436,7 @@ def log_startup_info(settings): logger.log("COMET", f"Custom Header HTML: {bool(settings.CUSTOM_HEADER_HTML)}") http_cache_info = ( - f" - Public Streams TTL: {settings.HTTP_CACHE_PUBLIC_STREAMS_TTL}s - Private Streams TTL: {settings.HTTP_CACHE_PRIVATE_STREAMS_TTL}s - Stale While Revalidate: {settings.HTTP_CACHE_STALE_WHILE_REVALIDATE}s" + f" - Public Streams TTL: {settings.HTTP_CACHE_PUBLIC_STREAMS_TTL}s - Private Streams TTL: {settings.HTTP_CACHE_PRIVATE_STREAMS_TTL}s - Manifest TTL: {settings.HTTP_CACHE_MANIFEST_TTL}s - Configure TTL: {settings.HTTP_CACHE_CONFIGURE_TTL}s - SWR: {settings.HTTP_CACHE_STALE_WHILE_REVALIDATE}s" if settings.HTTP_CACHE_ENABLED else "" ) diff --git a/comet/core/models.py b/comet/core/models.py index 25abd80..18e6d1a 100644 --- a/comet/core/models.py +++ b/comet/core/models.py @@ -141,6 +141,8 @@ class AppSettings(BaseSettings): HTTP_CACHE_PUBLIC_STREAMS_TTL: Optional[int] = 300 HTTP_CACHE_PRIVATE_STREAMS_TTL: Optional[int] = 60 HTTP_CACHE_STALE_WHILE_REVALIDATE: Optional[int] = 60 + HTTP_CACHE_MANIFEST_TTL: Optional[int] = 86400 + HTTP_CACHE_CONFIGURE_TTL: Optional[int] = 86400 @field_validator("INDEXER_MANAGER_TYPE") def set_indexer_manager_type(cls, v, values): diff --git a/comet/utils/cache.py b/comet/utils/cache.py index e3a03b2..46f509f 100644 --- a/comet/utils/cache.py +++ b/comet/utils/cache.py @@ -187,21 +187,23 @@ class CachePolicies: def manifest(): """ For manifest.json responses. - Very short cache as it can change based on config. + Long cache as manifest rarely changes. """ - return CacheControl().private().max_age(60).must_revalidate() + ttl = settings.HTTP_CACHE_MANIFEST_TTL + swr = settings.HTTP_CACHE_STALE_WHILE_REVALIDATE + + return CacheControl().public().max_age(ttl).stale_while_revalidate(swr) @staticmethod def configure_page(): """ For the /configure page. - Cacheable if no custom HTML, otherwise private. + Long cache as the page is mostly static. """ + ttl = settings.HTTP_CACHE_CONFIGURE_TTL + swr = settings.HTTP_CACHE_STALE_WHILE_REVALIDATE - if settings.CUSTOM_HEADER_HTML: - return CacheControl().private().max_age(300) - - return CacheControl().public().max_age(300).s_maxage(3600) + return CacheControl().public().max_age(ttl).stale_while_revalidate(swr) @staticmethod def empty_results(): diff --git a/deployment/cloudflare-cache-rules.md b/deployment/cloudflare-cache-rules.md index f260f30..f7c35df 100644 --- a/deployment/cloudflare-cache-rules.md +++ b/deployment/cloudflare-cache-rules.md @@ -20,12 +20,23 @@ Cache the configuration page. * **Action**: Eligible for Cache * **Edge TTL**: Use cache-control header if present (first option) * **Browser TTL**: Respect origin +* **Serve stale content while revalidating**: On -## 3. Tiered Cache +## 3. Manifest (Cache Rule) +Cache the add-on manifest. + +* **Rule Name**: Manifest +* **Expression**: `(http.request.uri.path contains "/manifest.json")` +* **Action**: Eligible for Cache +* **Edge TTL**: Use cache-control header if present (first option) +* **Browser TTL**: Respect origin +* **Serve stale content while revalidating**: On + +## 4. Tiered Cache Enable **Tiered Cache** in **Caching > Tiered Cache**. This minimizes requests to your origin by checking other Cloudflare datacenters first. -## 4. Network Optimizations +## 5. Network Optimizations In **Speed > Protocol**: * **HTTP/3 (QUIC)**: On (faster connections, especially on mobile) From 0fb306310729180a32b52cb063852aad4b2e62fb Mon Sep 17 00:00:00 2001 From: g0ldyy <153996346+g0ldyy@users.noreply.github.com> Date: Sun, 11 Jan 2026 14:57:19 +0100 Subject: [PATCH 3/5] refactor: unify stream caching settings and update related policies - Consolidated public and private stream TTL settings into a single HTTP_CACHE_STREAMS_TTL. - Updated cache policies to reflect the new unified stream caching approach. - Adjusted logging to display the new stream TTL setting. - Revised documentation to clarify caching behavior for all stream results. --- .env-sample | 13 ++++------ comet/api/endpoints/stream.py | 36 ++++++---------------------- comet/core/logger.py | 2 +- comet/core/models.py | 3 +-- comet/utils/cache.py | 17 +++---------- deployment/cloudflare-cache-rules.md | 2 +- 6 files changed, 17 insertions(+), 56 deletions(-) diff --git a/.env-sample b/.env-sample index 66afc91..862dba4 100644 --- a/.env-sample +++ b/.env-sample @@ -277,15 +277,10 @@ STREMTHRU_URL=https://stremthru.13377001.xyz # needed to use debrid services HTTP_CACHE_ENABLED=False # Master switch for HTTP cache headers -# PUBLIC streams TTL (for /stream/movie/tt1234567.json without user config) -# These can be cached at edge and shared between all users. -# Higher = less origin traffic, lower = fresher results -HTTP_CACHE_PUBLIC_STREAMS_TTL=300 # 5 minutes (recommended: 120-600) - -# PRIVATE streams TTL (for /{config}/stream/... with user config) -# Only cached in user's browser (private cache). -# These contain user-specific debrid availability info. -HTTP_CACHE_PRIVATE_STREAMS_TTL=60 # 1 minute (recommended: 30-120) +# STREAMS streams TTL (for /stream/movie/tt1234567.json) +# These results are cached at edge and shared between all users. +# Higher = less traffic to origin, Lower = faster updates for new content +HTTP_CACHE_STREAMS_TTL=300 # 5 minutes # Stale-While-Revalidate: serve stale content while fetching fresh in background # Improves perceived performance - users get instant response with cached data diff --git a/comet/api/endpoints/stream.py b/comet/api/endpoints/stream.py index fd33f1f..a53d328 100644 --- a/comet/api/endpoints/stream.py +++ b/comet/api/endpoints/stream.py @@ -5,7 +5,7 @@ from urllib.parse import quote import aiohttp from fastapi import APIRouter, BackgroundTasks, Request -from comet.core.config_validation import config_check, is_default_config +from comet.core.config_validation import config_check from comet.core.logger import logger from comet.core.models import database, settings, trackers from comet.debrid.exceptions import DebridAuthError @@ -30,7 +30,6 @@ streams = APIRouter() def _build_stream_response( request: Request, content: dict, - is_public: bool = False, is_empty: bool = False, vary_headers: list = None, ): @@ -45,12 +44,9 @@ def _build_stream_response( if is_empty: cache_policy = CachePolicies.empty_results() vary = ["Accept", "Accept-Encoding"] - elif is_public: - cache_policy = CachePolicies.public_torrents() - vary = ["Accept", "Accept-Encoding"] else: - cache_policy = CachePolicies.private_streams() - vary = ["Accept", "Accept-Encoding", "Authorization"] + cache_policy = CachePolicies.streams() + vary = ["Accept", "Accept-Encoding"] if vary_headers: vary.extend(vary_headers) @@ -177,17 +173,11 @@ async def stream( b64config: str = None, chilllink: bool = False, ): - is_public_request = b64config is None - if media_type not in ["movie", "series"]: - return _build_stream_response( - request, {"streams": []}, is_public=True, is_empty=True - ) + return _build_stream_response(request, {"streams": []}, is_empty=True) if "tmdb:" in media_id: - return _build_stream_response( - request, {"streams": []}, is_public=True, is_empty=True - ) + return _build_stream_response(request, {"streams": []}, is_empty=True) media_id = media_id.replace("imdb_id:", "") @@ -202,12 +192,7 @@ async def stream( } ] } - return _build_stream_response( - request, error_response, is_public=True, is_empty=True - ) - - if is_default_config(config): - is_public_request = True + return _build_stream_response(request, error_response, is_empty=True) is_torrent = config["debridService"] == "torrent" if settings.DISABLE_TORRENT_STREAMS and is_torrent: @@ -218,9 +203,7 @@ async def stream( if settings.TORRENT_DISABLED_STREAM_URL: placeholder_stream["url"] = settings.TORRENT_DISABLED_STREAM_URL - return _build_stream_response( - request, {"streams": [placeholder_stream]}, is_public=is_public_request - ) + return _build_stream_response(request, {"streams": [placeholder_stream]}) connector = aiohttp.TCPConnector(limit=0) async with aiohttp.ClientSession(connector=connector) as session: @@ -247,7 +230,6 @@ async def stream( } ] }, - is_public=True, is_empty=True, ) @@ -356,7 +338,6 @@ async def stream( } ] }, - is_public=True, is_empty=True, ) @@ -475,7 +456,6 @@ async def stream( } ] }, - is_public=True, is_empty=True, ) @@ -558,7 +538,6 @@ async def stream( } ] }, - is_public=False, is_empty=True, ) @@ -675,6 +654,5 @@ async def stream( return _build_stream_response( request, {"streams": final_streams}, - is_public=is_public_request or not has_results, is_empty=not has_results, ) diff --git a/comet/core/logger.py b/comet/core/logger.py index f42e301..60f94c8 100644 --- a/comet/core/logger.py +++ b/comet/core/logger.py @@ -436,7 +436,7 @@ def log_startup_info(settings): logger.log("COMET", f"Custom Header HTML: {bool(settings.CUSTOM_HEADER_HTML)}") http_cache_info = ( - f" - Public Streams TTL: {settings.HTTP_CACHE_PUBLIC_STREAMS_TTL}s - Private Streams TTL: {settings.HTTP_CACHE_PRIVATE_STREAMS_TTL}s - Manifest TTL: {settings.HTTP_CACHE_MANIFEST_TTL}s - Configure TTL: {settings.HTTP_CACHE_CONFIGURE_TTL}s - SWR: {settings.HTTP_CACHE_STALE_WHILE_REVALIDATE}s" + f" - Streams TTL: {settings.HTTP_CACHE_STREAMS_TTL}s - Manifest TTL: {settings.HTTP_CACHE_MANIFEST_TTL}s - Configure TTL: {settings.HTTP_CACHE_CONFIGURE_TTL}s - SWR: {settings.HTTP_CACHE_STALE_WHILE_REVALIDATE}s" if settings.HTTP_CACHE_ENABLED else "" ) diff --git a/comet/core/models.py b/comet/core/models.py index 18e6d1a..a073fa6 100644 --- a/comet/core/models.py +++ b/comet/core/models.py @@ -138,8 +138,7 @@ class AppSettings(BaseSettings): RATELIMIT_RETRY_BASE_DELAY: Optional[float] = 1.0 RTN_FILTER_DEBUG: Optional[bool] = False HTTP_CACHE_ENABLED: Optional[bool] = False - HTTP_CACHE_PUBLIC_STREAMS_TTL: Optional[int] = 300 - HTTP_CACHE_PRIVATE_STREAMS_TTL: Optional[int] = 60 + HTTP_CACHE_STREAMS_TTL: Optional[int] = 300 HTTP_CACHE_STALE_WHILE_REVALIDATE: Optional[int] = 60 HTTP_CACHE_MANIFEST_TTL: Optional[int] = 86400 HTTP_CACHE_CONFIGURE_TTL: Optional[int] = 86400 diff --git a/comet/utils/cache.py b/comet/utils/cache.py index 46f509f..5c9550f 100644 --- a/comet/utils/cache.py +++ b/comet/utils/cache.py @@ -154,13 +154,13 @@ def not_modified_response(etag: str): class CachePolicies: @staticmethod - def public_torrents(): + def streams(): """ - For public torrent lists (without user config). + For all stream results. Cache for a short time at CDN, revalidate often. """ - ttl = settings.HTTP_CACHE_PUBLIC_STREAMS_TTL + ttl = settings.HTTP_CACHE_STREAMS_TTL swr = settings.HTTP_CACHE_STALE_WHILE_REVALIDATE return ( @@ -172,17 +172,6 @@ class CachePolicies: .stale_if_error(300) ) - @staticmethod - def private_streams(): - """ - For user-specific stream results (with b64config). - Private cache only, short TTL. - """ - - ttl = settings.HTTP_CACHE_PRIVATE_STREAMS_TTL - - return CacheControl().private().max_age(ttl).must_revalidate() - @staticmethod def manifest(): """ diff --git a/deployment/cloudflare-cache-rules.md b/deployment/cloudflare-cache-rules.md index f7c35df..0fa2def 100644 --- a/deployment/cloudflare-cache-rules.md +++ b/deployment/cloudflare-cache-rules.md @@ -3,7 +3,7 @@ To optimize performance, configure Cloudflare to offload traffic from your server. We use **Cache Rules** to cache responses while respecting the application's cache headers. ## 1. Streams (Cache Rule) -Cache all stream results (public and private). Comet controls the TTL via headers. +Cache all stream results. Comet controls the TTL via headers. * **Rule Name**: Streams * **Expression**: `(http.request.uri.path contains "/stream/")` From feca4e03aa84468f575491311dd2de517541d06e Mon Sep 17 00:00:00 2001 From: g0ldyy <153996346+g0ldyy@users.noreply.github.com> Date: Sun, 11 Jan 2026 15:04:04 +0100 Subject: [PATCH 4/5] refactor: remove unused default configuration validation logic --- comet/core/config_validation.py | 55 --------------------------------- 1 file changed, 55 deletions(-) diff --git a/comet/core/config_validation.py b/comet/core/config_validation.py index e2032a7..dce9ca3 100644 --- a/comet/core/config_validation.py +++ b/comet/core/config_validation.py @@ -59,58 +59,3 @@ def config_check(b64config: str): return validated_config except Exception: return default_config # if it doesn't pass, return default config - - -def is_default_config(config: dict) -> bool: - if config is None: - return True - - ignored_fields = { - "debridApiKey", - "debridStreamProxyPassword", - "rtnSettings", - "rtnRanking", - "debridService", - } - - if config.get("debridService") != "torrent": - return False - - for field, default_value in default_config.items(): - if field in ignored_fields: - continue - - config_value = config.get(field) - - if field == "resolutions": - if isinstance(config_value, dict): - for res, enabled in config_value.items(): - if enabled is False: - return False - continue - - if field == "languages": - config_lang = config_value or {} - default_lang = default_value or {} - if config_lang.get("exclude", []) != default_lang.get("exclude", []): - return False - if config_lang.get("preferred", []) != default_lang.get("preferred", []): - return False - continue - - if field == "options": - config_opts = config_value or {} - default_opts = default_value or {} - for key in [ - "remove_ranks_under", - "allow_english_in_languages", - "remove_unknown_languages", - ]: - if config_opts.get(key) != default_opts.get(key): - return False - continue - - if config_value != default_value: - return False - - return True From c6f0790d683bf9b00c6f6ef5f06f3b7e12c7503f Mon Sep 17 00:00:00 2001 From: g0ldyy <153996346+g0ldyy@users.noreply.github.com> Date: Sun, 11 Jan 2026 15:56:36 +0100 Subject: [PATCH 5/5] refactor: enhance filtering logic with media type and year range handling - Updated the filter_worker function to include media_type as a parameter. - Introduced min_year and max_year calculations to improve year filtering logic. - Adjusted year mismatch logging to reflect new expected formats based on media type. --- comet/services/filtering.py | 41 ++++++++++++++++++++++----------- comet/services/orchestration.py | 1 + 2 files changed, 29 insertions(+), 13 deletions(-) diff --git a/comet/services/filtering.py b/comet/services/filtering.py index 7aca56c..20b1deb 100644 --- a/comet/services/filtering.py +++ b/comet/services/filtering.py @@ -17,13 +17,28 @@ def quick_alias_match(text_normalized: str, ez_aliases_normalized: list[str]): return any(alias in text_normalized for alias in ez_aliases_normalized) -def filter_worker(torrents, title, year, year_end, aliases, remove_adult_content): +def filter_worker( + torrents, title, year, year_end, media_type, aliases, remove_adult_content +): results = [] ez_aliases = aliases.get("ez", []) if ez_aliases: ez_aliases_normalized = [normalize_title(a) for a in ez_aliases] + min_year = 0 + max_year = float("inf") + + if year: + if year_end: + min_year = year + max_year = year_end + elif media_type == "series": + min_year = year - 1 + else: + min_year = year - 1 + max_year = year + 1 + for torrent in torrents: torrent_title = torrent["title"] torrent_title_lower = torrent_title.lower() @@ -53,18 +68,18 @@ def filter_worker(torrents, title, year, year_end, aliases, remove_adult_content continue if year and parsed.year: - if year_end is not None: - if not (year <= parsed.year <= year_end): - _log_exclusion( - f"📅 Rejected (Year Mismatch) | {torrent_title} | Year: {parsed.year} | Expected: {year}-{year_end}" - ) - continue - else: - if year < (parsed.year - 1) or year > (parsed.year + 1): - _log_exclusion( - f"📅 Rejected (Year Mismatch) | {torrent_title} | Year: {parsed.year} | Expected: ~{year}" - ) - continue + if not (min_year <= parsed.year <= max_year): + if year_end: + expected = f"{year}-{year_end}" + elif media_type == "series": + expected = f">{year}" + else: + expected = f"~{year}" + + _log_exclusion( + f"📅 Rejected (Year Mismatch) | {torrent_title} | Year: {parsed.year} | Expected: {expected}" + ) + continue torrent["parsed"] = parsed results.append(torrent) diff --git a/comet/services/orchestration.py b/comet/services/orchestration.py index 6c4bfe2..0e3452e 100644 --- a/comet/services/orchestration.py +++ b/comet/services/orchestration.py @@ -225,6 +225,7 @@ class TorrentManager: self.title, self.year, self.year_end, + self.media_type, self.aliases, self.remove_adult_content, )