Mythus commited on
Commit
01d9265
·
verified ·
1 Parent(s): ff51211

Upload 26 files

Browse files
.env-sample ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ADDON_ID=stremio.comet.fast # for Stremio
2
+ ADDON_NAME=Comet # for Stremio
3
+ FASTAPI_HOST=0.0.0.0
4
+ FASTAPI_PORT=8000
5
+ FASTAPI_WORKERS=1 # remove to destroy CPU -> max performances :)
6
+ DASHBOARD_ADMIN_PASSWORD=CHANGE_ME # The password to access the dashboard with active connections and soon more...
7
+ DATABASE_TYPE=sqlite # or postgresql if you know what you're doing
8
+ DATABASE_URL=username:password@hostname:port # to connect to PostgreSQL
9
+ DATABASE_PATH=data/comet.db # only change it if you know what it is - folders in path must exist - ignored if PostgreSQL used
10
+ CACHE_TTL=86400 # cache duration in seconds
11
+ DEBRID_PROXY_URL=http://127.0.0.1:1080 # https://github.com/cmj2002/warp-docker to bypass Debrid Services and Torrentio server IP blacklist
12
+ INDEXER_MANAGER_TYPE=jackett # or prowlarr or None if you want to disable it completely and use Zilean or Torrentio
13
+ INDEXER_MANAGER_URL=http://127.0.0.1:9117
14
+ INDEXER_MANAGER_API_KEY=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
15
+ INDEXER_MANAGER_TIMEOUT=60 # maximum time to obtain search results from indexer manager in seconds
16
+ INDEXER_MANAGER_INDEXERS='["EXAMPLE1_CHANGETHIS", "EXAMPLE2_CHANGETHIS"]'
17
+ GET_TORRENT_TIMEOUT=5 # maximum time to obtain the torrent info hash in seconds
18
+ ZILEAN_URL=None # for DMM search - https://github.com/iPromKnight/zilean - ex: http://127.0.0.1:8181
19
+ ZILEAN_TAKE_FIRST=500 # only change it if you know what it is
20
+ SCRAPE_TORRENTIO=False # scrape Torrentio
21
+ PROXY_DEBRID_STREAM=False # Proxy Debrid Streams (very useful to use your debrid service on multiple IPs at same time)
22
+ PROXY_DEBRID_STREAM_PASSWORD=CHANGE_ME # Secret password to enter on configuration page to prevent people from abusing your debrid stream proxy
23
+ PROXY_DEBRID_STREAM_MAX_CONNECTIONS=100 # IP-Based connection limit for the Debrid Stream Proxy
24
+ PROXY_DEBRID_STREAM_DEBRID_DEFAULT_SERVICE=realdebrid # if you want your users who use the Debrid Stream Proxy not to have to specify Debrid information, but to use the default one instead
25
+ PROXY_DEBRID_STREAM_DEBRID_DEFAULT_APIKEY=CHANGE_ME # if you want your users who use the Debrid Stream Proxy not to have to specify Debrid information, but to use the default one instead
26
+ TITLE_MATCH_CHECK=True # disable if you only use Jackett / Prowlarr / Torrentio and are sure you're only scraping good titles, for example (keep it True if Zilean is enabled)
27
+ CUSTOM_HEADER_HTML=None # only set it if you know what it is
.gitignore ADDED
@@ -0,0 +1,167 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Project Based
2
+ *.db
3
+
4
+ # Byte-compiled / optimized / DLL files
5
+ __pycache__/
6
+ *.py[cod]
7
+ *$py.class
8
+ logs/
9
+ .vscode/
10
+
11
+ # C extensions
12
+ *.so
13
+
14
+ # Distribution / packaging
15
+ .Python
16
+ build/
17
+ develop-eggs/
18
+ dist/
19
+ downloads/
20
+ eggs/
21
+ .eggs/
22
+ lib/
23
+ lib64/
24
+ parts/
25
+ sdist/
26
+ var/
27
+ wheels/
28
+ share/python-wheels/
29
+ *.egg-info/
30
+ .installed.cfg
31
+ *.egg
32
+ MANIFEST
33
+
34
+ # PyInstaller
35
+ # Usually these files are written by a python script from a template
36
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
37
+ *.manifest
38
+ *.spec
39
+
40
+ # Installer logs
41
+ pip-log.txt
42
+ pip-delete-this-directory.txt
43
+
44
+ # Unit test / coverage reports
45
+ htmlcov/
46
+ .tox/
47
+ .nox/
48
+ .coverage
49
+ .coverage.*
50
+ .cache
51
+ nosetests.xml
52
+ coverage.xml
53
+ *.cover
54
+ *.py,cover
55
+ .hypothesis/
56
+ .pytest_cache/
57
+ cover/
58
+
59
+ # Translations
60
+ *.mo
61
+ *.pot
62
+
63
+ # Django stuff:
64
+ *.log
65
+ local_settings.py
66
+ db.sqlite3
67
+ db.sqlite3-journal
68
+
69
+ # Flask stuff:
70
+ instance/
71
+ .webassets-cache
72
+
73
+ # Scrapy stuff:
74
+ .scrapy
75
+
76
+ # Sphinx documentation
77
+ docs/_build/
78
+
79
+ # PyBuilder
80
+ .pybuilder/
81
+ target/
82
+
83
+ # Jupyter Notebook
84
+ .ipynb_checkpoints
85
+
86
+ # IPython
87
+ profile_default/
88
+ ipython_config.py
89
+
90
+ # pyenv
91
+ # For a library or package, you might want to ignore these files since the code is
92
+ # intended to run in multiple environments; otherwise, check them in:
93
+ # .python-version
94
+
95
+ # pipenv
96
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
97
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
98
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
99
+ # install all needed dependencies.
100
+ #Pipfile.lock
101
+
102
+ # poetry
103
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
104
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
105
+ # commonly ignored for libraries.
106
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
107
+ poetry.lock
108
+
109
+ # pdm
110
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
111
+ #pdm.lock
112
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
113
+ # in version control.
114
+ # https://pdm.fming.dev/latest/usage/project/#working-with-version-control
115
+ .pdm.toml
116
+ .pdm-python
117
+ .pdm-build/
118
+
119
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
120
+ __pypackages__/
121
+
122
+ # Celery stuff
123
+ celerybeat-schedule
124
+ celerybeat.pid
125
+
126
+ # SageMath parsed files
127
+ *.sage.py
128
+
129
+ # Environments
130
+ .env
131
+ .venv
132
+ env/
133
+ venv/
134
+ ENV/
135
+ env.bak/
136
+ venv.bak/
137
+
138
+ # Spyder project settings
139
+ .spyderproject
140
+ .spyproject
141
+
142
+ # Rope project settings
143
+ .ropeproject
144
+
145
+ # mkdocs documentation
146
+ /site
147
+
148
+ # mypy
149
+ .mypy_cache/
150
+ .dmypy.json
151
+ dmypy.json
152
+
153
+ # Pyre type checker
154
+ .pyre/
155
+
156
+ # pytype static type analyzer
157
+ .pytype/
158
+
159
+ # Cython debug symbols
160
+ cython_debug/
161
+
162
+ # PyCharm
163
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
164
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
165
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
166
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
167
+ #.idea/
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Goldy
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
comet/__init__.py ADDED
File without changes
comet/api/__init__.py ADDED
File without changes
comet/api/core.py ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import PTT
2
+ import RTN
3
+
4
+ from fastapi import APIRouter, Request
5
+ from fastapi.responses import RedirectResponse
6
+ from fastapi.templating import Jinja2Templates
7
+
8
+ from comet.utils.models import settings
9
+ from comet.utils.general import config_check, get_debrid_extension
10
+
11
+ templates = Jinja2Templates("comet/templates")
12
+ main = APIRouter()
13
+
14
+
15
+ @main.get("/", status_code=200)
16
+ async def root():
17
+ return RedirectResponse("/configure")
18
+
19
+
20
+ @main.get("/health", status_code=200)
21
+ async def health():
22
+ return {"status": "ok"}
23
+
24
+
25
+ indexers = settings.INDEXER_MANAGER_INDEXERS
26
+ languages = [language for language in PTT.parse.LANGUAGES_TRANSLATION_TABLE.values()]
27
+ languages.insert(0, "Multi")
28
+ web_config = {
29
+ "indexers": [indexer.replace(" ", "_").lower() for indexer in indexers],
30
+ "languages": languages,
31
+ "resolutions": [resolution.value for resolution in RTN.models.Resolution],
32
+ "resultFormat": ["Title", "Metadata", "Size", "Tracker", "Languages"],
33
+ }
34
+
35
+
36
+ @main.get("/configure")
37
+ @main.get("/{b64config}/configure")
38
+ async def configure(request: Request):
39
+ return templates.TemplateResponse(
40
+ "index.html",
41
+ {
42
+ "request": request,
43
+ "CUSTOM_HEADER_HTML": settings.CUSTOM_HEADER_HTML
44
+ if settings.CUSTOM_HEADER_HTML
45
+ else "",
46
+ "webConfig": web_config,
47
+ "indexerManager": settings.INDEXER_MANAGER_TYPE,
48
+ "proxyDebridStream": settings.PROXY_DEBRID_STREAM,
49
+ },
50
+ )
51
+
52
+
53
+ @main.get("/manifest.json")
54
+ @main.get("/{b64config}/manifest.json")
55
+ async def manifest(b64config: str = None):
56
+ config = config_check(b64config)
57
+ if not config:
58
+ config = {"debridService": None}
59
+
60
+ debrid_extension = get_debrid_extension(config["debridService"])
61
+
62
+ return {
63
+ "id": settings.ADDON_ID,
64
+ "name": f"{settings.ADDON_NAME}{(' | ' + debrid_extension) if debrid_extension is not None else ''}",
65
+ "description": "Stremio's fastest torrent/debrid search add-on.",
66
+ "version": "1.0.0",
67
+ "catalogs": [],
68
+ "resources": [
69
+ {
70
+ "name": "stream",
71
+ "types": ["movie", "series"],
72
+ "idPrefixes": ["tt", "kitsu"],
73
+ }
74
+ ],
75
+ "types": ["movie", "series", "anime", "other"],
76
+ "logo": "https://i.imgur.com/jmVoVMu.jpeg",
77
+ "background": "https://i.imgur.com/WwnXB3k.jpeg",
78
+ "behaviorHints": {"configurable": True, "configurationRequired": False},
79
+ }
comet/api/stream.py ADDED
@@ -0,0 +1,601 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ import hashlib
3
+ import json
4
+ import time
5
+ import aiohttp
6
+ import httpx
7
+ import uuid
8
+ import orjson
9
+
10
+ from fastapi import APIRouter, Request
11
+ from fastapi.responses import (
12
+ RedirectResponse,
13
+ StreamingResponse,
14
+ FileResponse,
15
+ Response,
16
+ )
17
+ from starlette.background import BackgroundTask
18
+ from RTN import Torrent, sort_torrents
19
+
20
+ from comet.debrid.manager import getDebrid
21
+ from comet.utils.general import (
22
+ config_check,
23
+ get_debrid_extension,
24
+ get_indexer_manager,
25
+ get_zilean,
26
+ get_torrentio,
27
+ filter,
28
+ get_torrent_hash,
29
+ translate,
30
+ get_balanced_hashes,
31
+ format_title,
32
+ get_client_ip,
33
+ )
34
+ from comet.utils.logger import logger
35
+ from comet.utils.models import database, rtn, settings
36
+
37
+ streams = APIRouter()
38
+
39
+
40
+ @streams.get("/stream/{type}/{id}.json")
41
+ @streams.get("/{b64config}/stream/{type}/{id}.json")
42
+ async def stream(request: Request, b64config: str, type: str, id: str):
43
+ config = config_check(b64config)
44
+ if not config:
45
+ return {
46
+ "streams": [
47
+ {
48
+ "name": "[⚠️] Comet",
49
+ "title": "Invalid Comet config.",
50
+ "url": "https://comet.fast",
51
+ }
52
+ ]
53
+ }
54
+
55
+ connector = aiohttp.TCPConnector(limit=0)
56
+ async with aiohttp.ClientSession(connector=connector) as session:
57
+ full_id = id
58
+ season = None
59
+ episode = None
60
+ if type == "series":
61
+ info = id.split(":")
62
+ id = info[0]
63
+ season = int(info[1])
64
+ episode = int(info[2])
65
+
66
+ try:
67
+ kitsu = False
68
+ if id == "kitsu":
69
+ kitsu = True
70
+ get_metadata = await session.get(
71
+ f"https://kitsu.io/api/edge/anime/{season}"
72
+ )
73
+ metadata = await get_metadata.json()
74
+ name = metadata["data"]["attributes"]["canonicalTitle"]
75
+ season = 1
76
+ year = None
77
+ else:
78
+ get_metadata = await session.get(
79
+ f"https://v3.sg.media-imdb.com/suggestion/a/{id}.json"
80
+ )
81
+ metadata = await get_metadata.json()
82
+ element = metadata["d"][
83
+ 0
84
+ if metadata["d"][0]["id"]
85
+ not in ["/imdbpicks/summer-watch-guide", "/emmys"]
86
+ else 1
87
+ ]
88
+
89
+ for element in metadata["d"]:
90
+ if "/" not in element["id"]:
91
+ break
92
+
93
+ name = element["l"]
94
+ year = element["y"]
95
+ except Exception as e:
96
+ logger.warning(f"Exception while getting metadata for {id}: {e}")
97
+
98
+ return {
99
+ "streams": [
100
+ {
101
+ "name": "[⚠️] Comet",
102
+ "title": f"Can't get metadata for {id}",
103
+ "url": "https://comet.fast",
104
+ }
105
+ ]
106
+ }
107
+
108
+ name = translate(name)
109
+ log_name = name
110
+ if type == "series":
111
+ log_name = f"{name} S0{season}E0{episode}"
112
+
113
+ cache_key = hashlib.md5(
114
+ json.dumps(
115
+ {
116
+ "debridService": config["debridService"],
117
+ "name": name,
118
+ "season": season,
119
+ "episode": episode,
120
+ "indexers": config["indexers"],
121
+ }
122
+ ).encode("utf-8")
123
+ ).hexdigest()
124
+ cached = await database.fetch_one(
125
+ f"SELECT EXISTS (SELECT 1 FROM cache WHERE cacheKey = '{cache_key}')"
126
+ )
127
+ if cached[0] != 0:
128
+ logger.info(f"Cache found for {log_name}")
129
+
130
+ timestamp = await database.fetch_one(
131
+ f"SELECT timestamp FROM cache WHERE cacheKey = '{cache_key}'"
132
+ )
133
+ if timestamp[0] + settings.CACHE_TTL < time.time():
134
+ await database.execute(
135
+ f"DELETE FROM cache WHERE cacheKey = '{cache_key}'"
136
+ )
137
+
138
+ logger.info(f"Cache expired for {log_name}")
139
+ else:
140
+ sorted_ranked_files = await database.fetch_one(
141
+ f"SELECT results FROM cache WHERE cacheKey = '{cache_key}'"
142
+ )
143
+ sorted_ranked_files = json.loads(sorted_ranked_files[0])
144
+
145
+ debrid_extension = get_debrid_extension(config["debridService"])
146
+
147
+ balanced_hashes = get_balanced_hashes(sorted_ranked_files, config)
148
+
149
+ results = []
150
+ if (
151
+ config["debridStreamProxyPassword"] != ""
152
+ and settings.PROXY_DEBRID_STREAM
153
+ and settings.PROXY_DEBRID_STREAM_PASSWORD
154
+ != config["debridStreamProxyPassword"]
155
+ ):
156
+ results.append(
157
+ {
158
+ "name": "[⚠️] Comet",
159
+ "title": "Debrid Stream Proxy Password incorrect.\nStreams will not be proxied.",
160
+ "url": "https://comet.fast",
161
+ }
162
+ )
163
+
164
+ for (
165
+ hash,
166
+ hash_data,
167
+ ) in sorted_ranked_files.items():
168
+ for resolution, hash_list in balanced_hashes.items():
169
+ if hash in hash_list:
170
+ data = hash_data["data"]
171
+ results.append(
172
+ {
173
+ "name": f"[{debrid_extension}⚡] Comet {data['resolution']}",
174
+ "title": format_title(data, config),
175
+ "torrentTitle": (
176
+ data["torrent_title"]
177
+ if "torrent_title" in data
178
+ else None
179
+ ),
180
+ "torrentSize": (
181
+ data["torrent_size"]
182
+ if "torrent_size" in data
183
+ else None
184
+ ),
185
+ "url": f"{request.url.scheme}://{request.url.netloc}/{b64config}/playback/{hash}/{data['index']}",
186
+ "behaviorHints": {
187
+ "filename": data["raw_title"],
188
+ "bingeGroup": "comet|" + hash,
189
+ },
190
+ }
191
+ )
192
+
193
+ continue
194
+
195
+ return {"streams": results}
196
+ else:
197
+ logger.info(f"No cache found for {log_name} with user configuration")
198
+
199
+ if (
200
+ settings.PROXY_DEBRID_STREAM
201
+ and settings.PROXY_DEBRID_STREAM_PASSWORD
202
+ == config["debridStreamProxyPassword"]
203
+ and config["debridApiKey"] == ""
204
+ ):
205
+ config["debridService"] = (
206
+ settings.PROXY_DEBRID_STREAM_DEBRID_DEFAULT_SERVICE
207
+ )
208
+ config["debridApiKey"] = settings.PROXY_DEBRID_STREAM_DEBRID_DEFAULT_APIKEY
209
+
210
+ debrid = getDebrid(session, config, get_client_ip(request))
211
+
212
+ check_premium = await debrid.check_premium()
213
+ if not check_premium:
214
+ additional_info = ""
215
+ if config["debridService"] == "alldebrid":
216
+ additional_info = "\nCheck your email!"
217
+
218
+ return {
219
+ "streams": [
220
+ {
221
+ "name": "[⚠️] Comet",
222
+ "title": f"Invalid {config['debridService']} account.{additional_info}",
223
+ "url": "https://comet.fast",
224
+ }
225
+ ]
226
+ }
227
+
228
+ indexer_manager_type = settings.INDEXER_MANAGER_TYPE
229
+
230
+ search_indexer = len(config["indexers"]) != 0
231
+ torrents = []
232
+ tasks = []
233
+ if indexer_manager_type and search_indexer:
234
+ logger.info(
235
+ f"Start of {indexer_manager_type} search for {log_name} with indexers {config['indexers']}"
236
+ )
237
+
238
+ search_terms = [name]
239
+ if type == "series":
240
+ if not kitsu:
241
+ search_terms.append(f"{name} S0{season}E0{episode}")
242
+ else:
243
+ search_terms.append(f"{name} {episode}")
244
+ tasks.extend(
245
+ get_indexer_manager(
246
+ session, indexer_manager_type, config["indexers"], term
247
+ )
248
+ for term in search_terms
249
+ )
250
+ else:
251
+ logger.info(
252
+ f"No indexer {'manager ' if not indexer_manager_type else ''}{'selected by user' if indexer_manager_type else 'defined'} for {log_name}"
253
+ )
254
+
255
+ if settings.ZILEAN_URL:
256
+ tasks.append(get_zilean(session, name, log_name, season, episode))
257
+
258
+ if settings.SCRAPE_TORRENTIO:
259
+ tasks.append(get_torrentio(log_name, type, full_id))
260
+
261
+ search_response = await asyncio.gather(*tasks)
262
+ for results in search_response:
263
+ for result in results:
264
+ torrents.append(result)
265
+
266
+ logger.info(
267
+ f"{len(torrents)} torrents found for {log_name}"
268
+ + (
269
+ " with "
270
+ + ", ".join(
271
+ part
272
+ for part in [
273
+ indexer_manager_type,
274
+ "Zilean" if settings.ZILEAN_URL else None,
275
+ "Torrentio" if settings.SCRAPE_TORRENTIO else None,
276
+ ]
277
+ if part
278
+ )
279
+ if any(
280
+ [
281
+ indexer_manager_type,
282
+ settings.ZILEAN_URL,
283
+ settings.SCRAPE_TORRENTIO,
284
+ ]
285
+ )
286
+ else ""
287
+ )
288
+ )
289
+
290
+ if len(torrents) == 0:
291
+ return {"streams": []}
292
+
293
+ if settings.TITLE_MATCH_CHECK:
294
+ indexed_torrents = [(i, torrents[i]["Title"]) for i in range(len(torrents))]
295
+ chunk_size = 50
296
+ chunks = [
297
+ indexed_torrents[i : i + chunk_size]
298
+ for i in range(0, len(indexed_torrents), chunk_size)
299
+ ]
300
+
301
+ tasks = []
302
+ for chunk in chunks:
303
+ tasks.append(filter(chunk, name, year))
304
+
305
+ filtered_torrents = await asyncio.gather(*tasks)
306
+ index_less = 0
307
+ for result in filtered_torrents:
308
+ for filtered in result:
309
+ if not filtered[1]:
310
+ del torrents[filtered[0] - index_less]
311
+ index_less += 1
312
+ continue
313
+
314
+ logger.info(
315
+ f"{len(torrents)} torrents passed title match check for {log_name}"
316
+ )
317
+
318
+ if len(torrents) == 0:
319
+ return {"streams": []}
320
+
321
+ tasks = []
322
+ for i in range(len(torrents)):
323
+ tasks.append(get_torrent_hash(session, (i, torrents[i])))
324
+
325
+ torrent_hashes = await asyncio.gather(*tasks)
326
+ index_less = 0
327
+ for hash in torrent_hashes:
328
+ if not hash[1]:
329
+ del torrents[hash[0] - index_less]
330
+ index_less += 1
331
+ continue
332
+
333
+ torrents[hash[0] - index_less]["InfoHash"] = hash[1]
334
+
335
+ logger.info(f"{len(torrents)} info hashes found for {log_name}")
336
+
337
+ if len(torrents) == 0:
338
+ return {"streams": []}
339
+
340
+ files = await debrid.get_files(
341
+ [hash[1] for hash in torrent_hashes if hash[1] is not None],
342
+ type,
343
+ season,
344
+ episode,
345
+ kitsu,
346
+ )
347
+
348
+ ranked_files = set()
349
+ for hash in files:
350
+ try:
351
+ ranked_file = rtn.rank(
352
+ files[hash]["title"],
353
+ hash, # , correct_title=name, remove_trash=True
354
+ )
355
+
356
+ ranked_files.add(ranked_file)
357
+ except:
358
+ pass
359
+
360
+ sorted_ranked_files = sort_torrents(ranked_files)
361
+
362
+ len_sorted_ranked_files = len(sorted_ranked_files)
363
+ logger.info(
364
+ f"{len_sorted_ranked_files} cached files found on {config['debridService']} for {log_name}"
365
+ )
366
+
367
+ if len_sorted_ranked_files == 0:
368
+ return {"streams": []}
369
+
370
+ sorted_ranked_files = {
371
+ key: (value.model_dump() if isinstance(value, Torrent) else value)
372
+ for key, value in sorted_ranked_files.items()
373
+ }
374
+ torrents_by_hash = {torrent["InfoHash"]: torrent for torrent in torrents}
375
+ for hash in sorted_ranked_files: # needed for caching
376
+ sorted_ranked_files[hash]["data"]["title"] = files[hash]["title"]
377
+ sorted_ranked_files[hash]["data"]["torrent_title"] = torrents_by_hash[hash][
378
+ "Title"
379
+ ]
380
+ sorted_ranked_files[hash]["data"]["tracker"] = torrents_by_hash[hash][
381
+ "Tracker"
382
+ ]
383
+ sorted_ranked_files[hash]["data"]["size"] = files[hash]["size"]
384
+ torrent_size = torrents_by_hash[hash]["Size"]
385
+ sorted_ranked_files[hash]["data"]["torrent_size"] = (
386
+ torrent_size if torrent_size else files[hash]["size"]
387
+ )
388
+ sorted_ranked_files[hash]["data"]["index"] = files[hash]["index"]
389
+
390
+ json_data = json.dumps(sorted_ranked_files).replace("'", "''")
391
+ await database.execute(
392
+ f"INSERT {'OR IGNORE ' if settings.DATABASE_TYPE == 'sqlite' else ''}INTO cache (cacheKey, results, timestamp) VALUES (:cache_key, :json_data, :timestamp){' ON CONFLICT DO NOTHING' if settings.DATABASE_TYPE == 'postgresql' else ''}",
393
+ {"cache_key": cache_key, "json_data": json_data, "timestamp": time.time()},
394
+ )
395
+ logger.info(f"Results have been cached for {log_name}")
396
+
397
+ debrid_extension = get_debrid_extension(config["debridService"])
398
+
399
+ balanced_hashes = get_balanced_hashes(sorted_ranked_files, config)
400
+
401
+ results = []
402
+ if (
403
+ config["debridStreamProxyPassword"] != ""
404
+ and settings.PROXY_DEBRID_STREAM
405
+ and settings.PROXY_DEBRID_STREAM_PASSWORD
406
+ != config["debridStreamProxyPassword"]
407
+ ):
408
+ results.append(
409
+ {
410
+ "name": "[⚠️] Comet",
411
+ "title": "Debrid Stream Proxy Password incorrect.\nStreams will not be proxied.",
412
+ "url": "https://comet.fast",
413
+ }
414
+ )
415
+
416
+ for hash, hash_data in sorted_ranked_files.items():
417
+ for resolution, hash_list in balanced_hashes.items():
418
+ if hash in hash_list:
419
+ data = hash_data["data"]
420
+ results.append(
421
+ {
422
+ "name": f"[{debrid_extension}⚡] Comet {data['resolution']}",
423
+ "title": format_title(data, config),
424
+ "torrentTitle": data["torrent_title"],
425
+ "torrentSize": data["torrent_size"],
426
+ "url": f"{request.url.scheme}://{request.url.netloc}/{b64config}/playback/{hash}/{data['index']}",
427
+ "behaviorHints": {
428
+ "filename": data["raw_title"],
429
+ "bingeGroup": "comet|" + hash,
430
+ },
431
+ }
432
+ )
433
+
434
+ continue
435
+
436
+ return {"streams": results}
437
+
438
+
439
+ @streams.head("/{b64config}/playback/{hash}/{index}")
440
+ async def playback(b64config: str, hash: str, index: str):
441
+ return RedirectResponse("https://stremio.fast", status_code=302)
442
+
443
+
444
+ class CustomORJSONResponse(Response):
445
+ media_type = "application/json"
446
+
447
+ def render(self, content) -> bytes:
448
+ assert orjson is not None, "orjson must be installed"
449
+ return orjson.dumps(content, option=orjson.OPT_INDENT_2)
450
+
451
+
452
+ @streams.get("/active-connections", response_class=CustomORJSONResponse)
453
+ async def active_connections(request: Request, password: str):
454
+ if password != settings.DASHBOARD_ADMIN_PASSWORD:
455
+ return "Invalid Password"
456
+
457
+ active_connections = await database.fetch_all("SELECT * FROM active_connections")
458
+
459
+ return {
460
+ "total_connections": len(active_connections),
461
+ "active_connections": active_connections,
462
+ }
463
+
464
+
465
+ @streams.get("/{b64config}/playback/{hash}/{index}")
466
+ async def playback(request: Request, b64config: str, hash: str, index: str):
467
+ config = config_check(b64config)
468
+ if not config:
469
+ return FileResponse("comet/assets/invalidconfig.mp4")
470
+
471
+ if (
472
+ settings.PROXY_DEBRID_STREAM
473
+ and settings.PROXY_DEBRID_STREAM_PASSWORD == config["debridStreamProxyPassword"]
474
+ and config["debridApiKey"] == ""
475
+ ):
476
+ config["debridService"] = settings.PROXY_DEBRID_STREAM_DEBRID_DEFAULT_SERVICE
477
+ config["debridApiKey"] = settings.PROXY_DEBRID_STREAM_DEBRID_DEFAULT_APIKEY
478
+
479
+ async with aiohttp.ClientSession() as session:
480
+ # Check for cached download link
481
+ cached_link = await database.fetch_one(
482
+ f"SELECT link, timestamp FROM download_links WHERE debrid_key = '{config['debridApiKey']}' AND hash = '{hash}' AND file_index = '{index}'"
483
+ )
484
+
485
+ current_time = time.time()
486
+ download_link = None
487
+ if cached_link:
488
+ link = cached_link["link"]
489
+ timestamp = cached_link["timestamp"]
490
+
491
+ if current_time - timestamp < 3600:
492
+ download_link = link
493
+ else:
494
+ # Cache expired, remove old entry
495
+ await database.execute(
496
+ f"DELETE FROM download_links WHERE debrid_key = '{config['debridApiKey']}' AND hash = '{hash}' AND file_index = '{index}'"
497
+ )
498
+
499
+ ip = get_client_ip(request)
500
+ if not download_link:
501
+ debrid = getDebrid(session, config, ip)
502
+ download_link = await debrid.generate_download_link(hash, index)
503
+ if not download_link:
504
+ return FileResponse("comet/assets/uncached.mp4")
505
+
506
+ # Cache the new download link
507
+ await database.execute(
508
+ f"INSERT {'OR IGNORE ' if settings.DATABASE_TYPE == 'sqlite' else ''}INTO download_links (debrid_key, hash, file_index, link, timestamp) VALUES (:debrid_key, :hash, :file_index, :link, :timestamp){' ON CONFLICT DO NOTHING' if settings.DATABASE_TYPE == 'postgresql' else ''}",
509
+ {
510
+ "debrid_key": config["debridApiKey"],
511
+ "hash": hash,
512
+ "file_index": index,
513
+ "link": download_link,
514
+ "timestamp": current_time,
515
+ },
516
+ )
517
+
518
+ if (
519
+ settings.PROXY_DEBRID_STREAM
520
+ and settings.PROXY_DEBRID_STREAM_PASSWORD
521
+ == config["debridStreamProxyPassword"]
522
+ ):
523
+ active_ip_connections = await database.fetch_all(
524
+ "SELECT ip, COUNT(*) as connections FROM active_connections GROUP BY ip"
525
+ )
526
+ if any(
527
+ connection["ip"] == ip
528
+ and connection["connections"]
529
+ >= settings.PROXY_DEBRID_STREAM_MAX_CONNECTIONS
530
+ for connection in active_ip_connections
531
+ ):
532
+ return FileResponse("comet/assets/proxylimit.mp4")
533
+
534
+ proxy = None
535
+
536
+ class Streamer:
537
+ def __init__(self, id: str):
538
+ self.id = id
539
+
540
+ self.client = httpx.AsyncClient(proxy=proxy)
541
+ self.response = None
542
+
543
+ async def stream_content(self, headers: dict):
544
+ async with self.client.stream(
545
+ "GET", download_link, headers=headers
546
+ ) as self.response:
547
+ async for chunk in self.response.aiter_raw():
548
+ yield chunk
549
+
550
+ async def close(self):
551
+ await database.execute(
552
+ f"DELETE FROM active_connections WHERE id = '{self.id}'"
553
+ )
554
+
555
+ if self.response is not None:
556
+ await self.response.aclose()
557
+ if self.client is not None:
558
+ await self.client.aclose()
559
+
560
+ range_header = request.headers.get("range", "bytes=0-")
561
+
562
+ response = await session.head(
563
+ download_link, headers={"Range": range_header}
564
+ )
565
+ if response.status == 503 and config["debridService"] == "alldebrid":
566
+ proxy = (
567
+ settings.DEBRID_PROXY_URL
568
+ ) # proxy is not needed to proxy realdebrid stream
569
+
570
+ response = await session.head(
571
+ download_link, headers={"Range": range_header}, proxy=proxy
572
+ )
573
+
574
+ if response.status == 206:
575
+ id = str(uuid.uuid4())
576
+ await database.execute(
577
+ f"INSERT {'OR IGNORE ' if settings.DATABASE_TYPE == 'sqlite' else ''}INTO active_connections (id, ip, content, timestamp) VALUES (:id, :ip, :content, :timestamp){' ON CONFLICT DO NOTHING' if settings.DATABASE_TYPE == 'postgresql' else ''}",
578
+ {
579
+ "id": id,
580
+ "ip": ip,
581
+ "content": str(response.url),
582
+ "timestamp": current_time,
583
+ },
584
+ )
585
+
586
+ streamer = Streamer(id)
587
+
588
+ return StreamingResponse(
589
+ streamer.stream_content({"Range": range_header}),
590
+ status_code=206,
591
+ headers={
592
+ "Content-Range": response.headers["Content-Range"],
593
+ "Content-Length": response.headers["Content-Length"],
594
+ "Accept-Ranges": "bytes",
595
+ },
596
+ background=BackgroundTask(streamer.close),
597
+ )
598
+
599
+ return FileResponse("comet/assets/uncached.mp4")
600
+
601
+ return RedirectResponse(download_link, status_code=302)
comet/assets/invalidconfig.mp4 ADDED
Binary file (137 kB). View file
 
comet/assets/proxylimit.mp4 ADDED
Binary file (55.8 kB). View file
 
comet/assets/uncached.mp4 ADDED
Binary file (303 kB). View file
 
comet/debrid/__init__.py ADDED
File without changes
comet/debrid/alldebrid.py ADDED
@@ -0,0 +1,172 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import aiohttp
2
+ import asyncio
3
+
4
+ from RTN import parse
5
+
6
+ from comet.utils.general import is_video
7
+ from comet.utils.logger import logger
8
+ from comet.utils.models import settings
9
+
10
+
11
+ class AllDebrid:
12
+ def __init__(self, session: aiohttp.ClientSession, debrid_api_key: str):
13
+ session.headers["Authorization"] = f"Bearer {debrid_api_key}"
14
+ self.session = session
15
+ self.proxy = None
16
+
17
+ self.api_url = "http://api.alldebrid.com/v4"
18
+ self.agent = "comet"
19
+
20
+ async def check_premium(self):
21
+ try:
22
+ check_premium = await self.session.get(
23
+ f"{self.api_url}/user?agent={self.agent}"
24
+ )
25
+ check_premium = await check_premium.text()
26
+ if '"isPremium":true' in check_premium:
27
+ return True
28
+ except Exception as e:
29
+ logger.warning(
30
+ f"Exception while checking premium status on All-Debrid: {e}"
31
+ )
32
+
33
+ return False
34
+
35
+ async def get_instant(self, chunk: list):
36
+ try:
37
+ get_instant = await self.session.get(
38
+ f"{self.api_url}/magnet/instant?agent={self.agent}&magnets[]={'&magnets[]='.join(chunk)}"
39
+ )
40
+ return await get_instant.json()
41
+ except Exception as e:
42
+ logger.warning(
43
+ f"Exception while checking hashes instant availability on All-Debrid: {e}"
44
+ )
45
+
46
+ async def get_files(
47
+ self, torrent_hashes: list, type: str, season: str, episode: str, kitsu: bool
48
+ ):
49
+ chunk_size = 500
50
+ chunks = [
51
+ torrent_hashes[i : i + chunk_size]
52
+ for i in range(0, len(torrent_hashes), chunk_size)
53
+ ]
54
+
55
+ tasks = []
56
+ for chunk in chunks:
57
+ tasks.append(self.get_instant(chunk))
58
+
59
+ responses = await asyncio.gather(*tasks)
60
+
61
+ availability = [response for response in responses if response]
62
+
63
+ files = {}
64
+
65
+ if type == "series":
66
+ for result in availability:
67
+ if "status" not in result or result["status"] != "success":
68
+ continue
69
+
70
+ for magnet in result["data"]["magnets"]:
71
+ if not magnet["instant"]:
72
+ continue
73
+
74
+ for file in magnet["files"]:
75
+ filename = file["n"]
76
+ pack = False
77
+ if "e" in file: # PACK
78
+ filename = file["e"][0]["n"]
79
+ pack = True
80
+
81
+ if not is_video(filename):
82
+ continue
83
+
84
+ if "sample" in filename:
85
+ continue
86
+
87
+ filename_parsed = parse(filename)
88
+ if episode not in filename_parsed.episodes:
89
+ continue
90
+
91
+ if kitsu:
92
+ if filename_parsed.seasons:
93
+ continue
94
+ else:
95
+ if season not in filename_parsed.seasons:
96
+ continue
97
+
98
+ files[magnet["hash"]] = {
99
+ "index": magnet["files"].index(file),
100
+ "title": filename,
101
+ "size": file["e"][0]["s"] if pack else file["s"],
102
+ }
103
+
104
+ break
105
+ else:
106
+ for result in availability:
107
+ if "status" not in result or result["status"] != "success":
108
+ continue
109
+
110
+ for magnet in result["data"]["magnets"]:
111
+ if not magnet["instant"]:
112
+ continue
113
+
114
+ for file in magnet["files"]:
115
+ filename = file["n"]
116
+
117
+ if not is_video(filename):
118
+ continue
119
+
120
+ if "sample" in filename:
121
+ continue
122
+
123
+ files[magnet["hash"]] = {
124
+ "index": magnet["files"].index(file),
125
+ "title": filename,
126
+ "size": file["s"],
127
+ }
128
+
129
+ break
130
+
131
+ return files
132
+
133
+ async def generate_download_link(self, hash: str, index: str):
134
+ try:
135
+ check_blacklisted = await self.session.get(
136
+ f"{self.api_url}/magnet/upload?agent=comet&magnets[]={hash}"
137
+ )
138
+ check_blacklisted = await check_blacklisted.text()
139
+ if "NO_SERVER" in check_blacklisted:
140
+ self.proxy = settings.DEBRID_PROXY_URL
141
+ if not self.proxy:
142
+ logger.warning(
143
+ "All-Debrid blacklisted server's IP. No proxy found."
144
+ )
145
+ else:
146
+ logger.warning(
147
+ f"All-Debrid blacklisted server's IP. Switching to proxy {self.proxy} for {hash}|{index}"
148
+ )
149
+
150
+ upload_magnet = await self.session.get(
151
+ f"{self.api_url}/magnet/upload?agent=comet&magnets[]={hash}",
152
+ proxy=self.proxy,
153
+ )
154
+ upload_magnet = await upload_magnet.json()
155
+
156
+ get_magnet_status = await self.session.get(
157
+ f"{self.api_url}/magnet/status?agent=comet&id={upload_magnet['data']['magnets'][0]['id']}",
158
+ proxy=self.proxy,
159
+ )
160
+ get_magnet_status = await get_magnet_status.json()
161
+
162
+ unlock_link = await self.session.get(
163
+ f"{self.api_url}/link/unlock?agent=comet&link={get_magnet_status['data']['magnets']['links'][int(index)]['link']}",
164
+ proxy=self.proxy,
165
+ )
166
+ unlock_link = await unlock_link.json()
167
+
168
+ return unlock_link["data"]["link"]
169
+ except Exception as e:
170
+ logger.warning(
171
+ f"Exception while getting download link from All-Debrid for {hash}|{index}: {e}"
172
+ )
comet/debrid/debridlink.py ADDED
@@ -0,0 +1,126 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import aiohttp
2
+ import asyncio
3
+
4
+ from RTN import parse
5
+
6
+ from comet.utils.general import is_video
7
+ from comet.utils.logger import logger
8
+
9
+
10
+ class DebridLink:
11
+ def __init__(self, session: aiohttp.ClientSession, debrid_api_key: str):
12
+ session.headers["Authorization"] = f"Bearer {debrid_api_key}"
13
+ self.session = session
14
+ self.proxy = None
15
+
16
+ self.api_url = "https://debrid-link.com/api/v2"
17
+
18
+ async def check_premium(self):
19
+ try:
20
+ check_premium = await self.session.get(f"{self.api_url}/account/infos")
21
+ check_premium = await check_premium.text()
22
+ if '"accountType":1' in check_premium:
23
+ return True
24
+ except Exception as e:
25
+ logger.warning(
26
+ f"Exception while checking premium status on Debrid-Link: {e}"
27
+ )
28
+
29
+ return False
30
+
31
+ async def get_instant(self, chunk: list):
32
+ try:
33
+ get_instant = await self.session.get(
34
+ f"{self.api_url}/seedbox/cached?url={','.join(chunk)}"
35
+ )
36
+ return await get_instant.json()
37
+ except Exception as e:
38
+ logger.warning(
39
+ f"Exception while checking hashes instant availability on Debrid-Link: {e}"
40
+ )
41
+
42
+ async def get_files(
43
+ self, torrent_hashes: list, type: str, season: str, episode: str, kitsu: bool
44
+ ):
45
+ chunk_size = 250
46
+ chunks = [
47
+ torrent_hashes[i : i + chunk_size]
48
+ for i in range(0, len(torrent_hashes), chunk_size)
49
+ ]
50
+
51
+ tasks = []
52
+ for chunk in chunks:
53
+ tasks.append(self.get_instant(chunk))
54
+
55
+ responses = await asyncio.gather(*tasks)
56
+
57
+ availability = [
58
+ response for response in responses if response and response.get("success")
59
+ ]
60
+
61
+ files = {}
62
+
63
+ if type == "series":
64
+ for result in availability:
65
+ for hash, torrent_data in result["value"].items():
66
+ for file in torrent_data["files"]:
67
+ filename = file["name"]
68
+
69
+ if not is_video(filename):
70
+ continue
71
+
72
+ if "sample" in filename:
73
+ continue
74
+
75
+ filename_parsed = parse(filename)
76
+ if episode not in filename_parsed.episodes:
77
+ continue
78
+
79
+ if kitsu:
80
+ if filename_parsed.seasons:
81
+ continue
82
+ else:
83
+ if season not in filename_parsed.seasons:
84
+ continue
85
+
86
+ files[hash] = {
87
+ "index": torrent_data["files"].index(file),
88
+ "title": filename,
89
+ "size": file["size"],
90
+ }
91
+
92
+ break
93
+ else:
94
+ for result in availability:
95
+ for hash, torrent_data in result["value"].items():
96
+ for file in torrent_data["files"]:
97
+ filename = file["name"]
98
+
99
+ if not is_video(filename):
100
+ continue
101
+
102
+ if "sample" in filename:
103
+ continue
104
+
105
+ files[hash] = {
106
+ "index": torrent_data["files"].index(file),
107
+ "title": filename,
108
+ "size": file["size"],
109
+ }
110
+
111
+ break
112
+
113
+ return files
114
+
115
+ async def generate_download_link(self, hash: str, index: str):
116
+ try:
117
+ add_torrent = await self.session.post(
118
+ f"{self.api_url}/seedbox/add", data={"url": hash, "async": True}
119
+ )
120
+ add_torrent = await add_torrent.json()
121
+
122
+ return add_torrent["value"]["files"][int(index)]["downloadUrl"]
123
+ except Exception as e:
124
+ logger.warning(
125
+ f"Exception while getting download link from Debrid-Link for {hash}|{index}: {e}"
126
+ )
comet/debrid/manager.py ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import aiohttp
2
+
3
+ from .realdebrid import RealDebrid
4
+ from .alldebrid import AllDebrid
5
+ from .premiumize import Premiumize
6
+ from .torbox import TorBox
7
+ from .debridlink import DebridLink
8
+
9
+
10
+ def getDebrid(session: aiohttp.ClientSession, config: dict, ip: str):
11
+ debrid_service = config["debridService"]
12
+ debrid_api_key = config["debridApiKey"]
13
+ if debrid_service == "realdebrid":
14
+ return RealDebrid(session, debrid_api_key, ip)
15
+ elif debrid_service == "alldebrid":
16
+ return AllDebrid(session, debrid_api_key)
17
+ elif debrid_service == "premiumize":
18
+ return Premiumize(session, debrid_api_key)
19
+ elif debrid_service == "torbox":
20
+ return TorBox(session, debrid_api_key)
21
+ elif debrid_service == "debridlink":
22
+ return DebridLink(session, debrid_api_key)
comet/debrid/premiumize.py ADDED
@@ -0,0 +1,177 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import aiohttp
2
+ import asyncio
3
+
4
+ from RTN import parse
5
+
6
+ from comet.utils.general import is_video
7
+ from comet.utils.logger import logger
8
+
9
+
10
+ class Premiumize:
11
+ def __init__(self, session: aiohttp.ClientSession, debrid_api_key: str):
12
+ self.session = session
13
+ self.proxy = None
14
+
15
+ self.api_url = "https://premiumize.me/api"
16
+ self.debrid_api_key = debrid_api_key
17
+
18
+ async def check_premium(self):
19
+ try:
20
+ check_premium = await self.session.get(
21
+ f"{self.api_url}/account/info?apikey={self.debrid_api_key}"
22
+ )
23
+ check_premium = await check_premium.text()
24
+ if (
25
+ '"status":"success"' in check_premium
26
+ and '"premium_until":null' not in check_premium
27
+ ):
28
+ return True
29
+ except Exception as e:
30
+ logger.warning(
31
+ f"Exception while checking premium status on Premiumize: {e}"
32
+ )
33
+
34
+ return False
35
+
36
+ async def get_instant(self, chunk: list):
37
+ try:
38
+ response = await self.session.get(
39
+ f"{self.api_url}/cache/check?apikey={self.debrid_api_key}&items[]={'&items[]='.join(chunk)}"
40
+ )
41
+
42
+ response = await response.json()
43
+ response["hashes"] = chunk
44
+
45
+ return response
46
+ except Exception as e:
47
+ logger.warning(
48
+ f"Exception while checking hash instant availability on Premiumize: {e}"
49
+ )
50
+
51
+ async def get_files(
52
+ self, torrent_hashes: list, type: str, season: str, episode: str, kitsu: bool
53
+ ):
54
+ chunk_size = 100
55
+ chunks = [
56
+ torrent_hashes[i : i + chunk_size]
57
+ for i in range(0, len(torrent_hashes), chunk_size)
58
+ ]
59
+
60
+ tasks = []
61
+ for chunk in chunks:
62
+ tasks.append(self.get_instant(chunk))
63
+
64
+ responses = await asyncio.gather(*tasks)
65
+
66
+ availability = []
67
+ for response in responses:
68
+ if not response:
69
+ continue
70
+
71
+ availability.append(response)
72
+
73
+ files = {}
74
+
75
+ if type == "series":
76
+ for result in availability:
77
+ if result["status"] != "success":
78
+ continue
79
+
80
+ responses = result["response"]
81
+ filenames = result["filename"]
82
+ filesizes = result["filesize"]
83
+ hashes = result["hashes"]
84
+ for index, response in enumerate(responses):
85
+ if not response:
86
+ continue
87
+
88
+ if not filesizes[index]:
89
+ continue
90
+
91
+ filename = filenames[index]
92
+
93
+ if "sample" in filename:
94
+ continue
95
+
96
+ filename_parsed = parse(filename)
97
+ if episode not in filename_parsed.episodes:
98
+ continue
99
+
100
+ if kitsu:
101
+ if filename_parsed.seasons:
102
+ continue
103
+ else:
104
+ if season not in filename_parsed.seasons:
105
+ continue
106
+
107
+ files[hashes[index]] = {
108
+ "index": f"{season}|{episode}",
109
+ "title": filename,
110
+ "size": int(filesizes[index]),
111
+ }
112
+ else:
113
+ for result in availability:
114
+ if result["status"] != "success":
115
+ continue
116
+
117
+ responses = result["response"]
118
+ filenames = result["filename"]
119
+ filesizes = result["filesize"]
120
+ hashes = result["hashes"]
121
+ for index, response in enumerate(responses):
122
+ if response is False:
123
+ continue
124
+
125
+ if not filesizes[index]:
126
+ continue
127
+
128
+ filename = filenames[index]
129
+
130
+ if "sample" in filename:
131
+ continue
132
+
133
+ files[hashes[index]] = {
134
+ "index": 0,
135
+ "title": filename,
136
+ "size": int(filesizes[index]),
137
+ }
138
+
139
+ return files
140
+
141
+ async def generate_download_link(self, hash: str, index: str):
142
+ try:
143
+ add_magnet = await self.session.post(
144
+ f"{self.api_url}/transfer/directdl?apikey={self.debrid_api_key}&src=magnet:?xt=urn:btih:{hash}",
145
+ )
146
+ add_magnet = await add_magnet.json()
147
+
148
+ season = None
149
+ if "|" in index:
150
+ index = index.split("|")
151
+ season = int(index[0])
152
+ episode = int(index[1])
153
+
154
+ content = add_magnet["content"]
155
+ for file in content:
156
+ filename = file["path"]
157
+ if "/" in filename:
158
+ filename = filename.split("/")[1]
159
+
160
+ if not is_video(filename):
161
+ content.remove(file)
162
+ continue
163
+
164
+ if season is not None:
165
+ filename_parsed = parse(filename)
166
+ if (
167
+ season in filename_parsed.seasons
168
+ and episode in filename_parsed.episodes
169
+ ):
170
+ return file["link"]
171
+
172
+ max_size_item = max(content, key=lambda x: x["size"])
173
+ return max_size_item["link"]
174
+ except Exception as e:
175
+ logger.warning(
176
+ f"Exception while getting download link from Premiumize for {hash}|{index}: {e}"
177
+ )
comet/debrid/realdebrid.py ADDED
@@ -0,0 +1,192 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import aiohttp
2
+ import asyncio
3
+
4
+ from RTN import parse
5
+
6
+ from comet.utils.general import is_video
7
+ from comet.utils.logger import logger
8
+ from comet.utils.models import settings
9
+
10
+
11
+ class RealDebrid:
12
+ def __init__(self, session: aiohttp.ClientSession, debrid_api_key: str, ip: str):
13
+ session.headers["Authorization"] = f"Bearer {debrid_api_key}"
14
+ self.session = session
15
+ self.ip = ip
16
+ self.proxy = None
17
+
18
+ self.api_url = "https://api.real-debrid.com/rest/1.0"
19
+
20
+ async def check_premium(self):
21
+ try:
22
+ check_premium = await self.session.get(f"{self.api_url}/user")
23
+ check_premium = await check_premium.text()
24
+ if '"type": "premium"' in check_premium:
25
+ return True
26
+ except Exception as e:
27
+ logger.warning(
28
+ f"Exception while checking premium status on Real-Debrid: {e}"
29
+ )
30
+
31
+ return False
32
+
33
+ async def get_instant(self, chunk: list):
34
+ try:
35
+ response = await self.session.get(
36
+ f"{self.api_url}/torrents/instantAvailability/{'/'.join(chunk)}"
37
+ )
38
+ return await response.json()
39
+ except Exception as e:
40
+ logger.warning(
41
+ f"Exception while checking hash instant availability on Real-Debrid: {e}"
42
+ )
43
+
44
+ async def get_files(
45
+ self, torrent_hashes: list, type: str, season: str, episode: str, kitsu: bool
46
+ ):
47
+ chunk_size = 50
48
+ chunks = [
49
+ torrent_hashes[i : i + chunk_size]
50
+ for i in range(0, len(torrent_hashes), chunk_size)
51
+ ]
52
+
53
+ tasks = []
54
+ for chunk in chunks:
55
+ tasks.append(self.get_instant(chunk))
56
+
57
+ responses = await asyncio.gather(*tasks)
58
+
59
+ availability = {}
60
+ for response in responses:
61
+ if response is not None:
62
+ availability.update(response)
63
+
64
+ files = {}
65
+
66
+ if type == "series":
67
+ for hash, details in availability.items():
68
+ if "rd" not in details:
69
+ continue
70
+
71
+ for variants in details["rd"]:
72
+ for index, file in variants.items():
73
+ filename = file["filename"]
74
+
75
+ if not is_video(filename):
76
+ continue
77
+
78
+ if "sample" in filename:
79
+ continue
80
+
81
+ filename_parsed = parse(filename)
82
+ if episode not in filename_parsed.episodes:
83
+ continue
84
+
85
+ if kitsu:
86
+ if filename_parsed.seasons:
87
+ continue
88
+ else:
89
+ if season not in filename_parsed.seasons:
90
+ continue
91
+
92
+ files[hash] = {
93
+ "index": index,
94
+ "title": filename,
95
+ "size": file["filesize"],
96
+ }
97
+
98
+ break
99
+ else:
100
+ for hash, details in availability.items():
101
+ if "rd" not in details:
102
+ continue
103
+
104
+ for variants in details["rd"]:
105
+ for index, file in variants.items():
106
+ filename = file["filename"]
107
+
108
+ if not is_video(filename):
109
+ continue
110
+
111
+ if "sample" in filename:
112
+ continue
113
+
114
+ files[hash] = {
115
+ "index": index,
116
+ "title": filename,
117
+ "size": file["filesize"],
118
+ }
119
+
120
+ break
121
+
122
+ return files
123
+
124
+ async def generate_download_link(self, hash: str, index: str):
125
+ try:
126
+ check_blacklisted = await self.session.get("https://real-debrid.com/vpn")
127
+ check_blacklisted = await check_blacklisted.text()
128
+ if (
129
+ "Your ISP or VPN provider IP address is currently blocked on our website"
130
+ in check_blacklisted
131
+ ):
132
+ self.proxy = settings.DEBRID_PROXY_URL
133
+ if not self.proxy:
134
+ logger.warning(
135
+ "Real-Debrid blacklisted server's IP. No proxy found."
136
+ )
137
+ else:
138
+ logger.warning(
139
+ f"Real-Debrid blacklisted server's IP. Switching to proxy {self.proxy} for {hash}|{index}"
140
+ )
141
+
142
+ add_magnet = await self.session.post(
143
+ f"{self.api_url}/torrents/addMagnet",
144
+ data={"magnet": f"magnet:?xt=urn:btih:{hash}", "ip": self.ip},
145
+ proxy=self.proxy,
146
+ )
147
+ add_magnet = await add_magnet.json()
148
+
149
+ get_magnet_info = await self.session.get(
150
+ add_magnet["uri"], proxy=self.proxy
151
+ )
152
+ get_magnet_info = await get_magnet_info.json()
153
+
154
+ await self.session.post(
155
+ f"{self.api_url}/torrents/selectFiles/{add_magnet['id']}",
156
+ data={
157
+ "files": ",".join(
158
+ str(file["id"])
159
+ for file in get_magnet_info["files"]
160
+ if is_video(file["path"])
161
+ ),
162
+ "ip": self.ip,
163
+ },
164
+ proxy=self.proxy,
165
+ )
166
+
167
+ get_magnet_info = await self.session.get(
168
+ add_magnet["uri"], proxy=self.proxy
169
+ )
170
+ get_magnet_info = await get_magnet_info.json()
171
+
172
+ index = int(index)
173
+ realIndex = index
174
+ for file in get_magnet_info["files"]:
175
+ if file["id"] == realIndex:
176
+ break
177
+
178
+ if file["selected"] != 1:
179
+ index -= 1
180
+
181
+ unrestrict_link = await self.session.post(
182
+ f"{self.api_url}/unrestrict/link",
183
+ data={"link": get_magnet_info["links"][index - 1], "ip": self.ip},
184
+ proxy=self.proxy,
185
+ )
186
+ unrestrict_link = await unrestrict_link.json()
187
+
188
+ return unrestrict_link["download"]
189
+ except Exception as e:
190
+ logger.warning(
191
+ f"Exception while getting download link from Real-Debrid for {hash}|{index}: {e}"
192
+ )
comet/debrid/torbox.py ADDED
@@ -0,0 +1,161 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import aiohttp
2
+ import asyncio
3
+
4
+ from RTN import parse
5
+
6
+ from comet.utils.general import is_video
7
+ from comet.utils.logger import logger
8
+
9
+
10
+ class TorBox:
11
+ def __init__(self, session: aiohttp.ClientSession, debrid_api_key: str):
12
+ session.headers["Authorization"] = f"Bearer {debrid_api_key}"
13
+ self.session = session
14
+ self.proxy = None
15
+
16
+ self.api_url = "https://api.torbox.app/v1/api"
17
+ self.debrid_api_key = debrid_api_key
18
+
19
+ async def check_premium(self):
20
+ try:
21
+ check_premium = await self.session.get(
22
+ f"{self.api_url}/user/me?settings=false"
23
+ )
24
+ check_premium = await check_premium.text()
25
+ if '"success":true' in check_premium and '"plan":0' not in check_premium:
26
+ return True
27
+ except Exception as e:
28
+ logger.warning(f"Exception while checking premium status on TorBox: {e}")
29
+
30
+ return False
31
+
32
+ async def get_instant(self, chunk: list):
33
+ try:
34
+ response = await self.session.get(
35
+ f"{self.api_url}/torrents/checkcached?hash={','.join(chunk)}&format=list&list_files=true"
36
+ )
37
+ return await response.json()
38
+ except Exception as e:
39
+ logger.warning(
40
+ f"Exception while checking hash instant availability on TorBox: {e}"
41
+ )
42
+
43
+ async def get_files(
44
+ self, torrent_hashes: list, type: str, season: str, episode: str, kitsu: bool
45
+ ):
46
+ chunk_size = 100
47
+ chunks = [
48
+ torrent_hashes[i : i + chunk_size]
49
+ for i in range(0, len(torrent_hashes), chunk_size)
50
+ ]
51
+
52
+ tasks = []
53
+ for chunk in chunks:
54
+ tasks.append(self.get_instant(chunk))
55
+
56
+ responses = await asyncio.gather(*tasks)
57
+
58
+ availability = [response for response in responses if response is not None]
59
+
60
+ files = {}
61
+
62
+ if type == "series":
63
+ for result in availability:
64
+ if not result["success"] or not result["data"]:
65
+ continue
66
+
67
+ for torrent in result["data"]:
68
+ torrent_files = torrent["files"]
69
+ for file in torrent_files:
70
+ filename = file["name"].split("/")[1]
71
+
72
+ if not is_video(filename):
73
+ continue
74
+
75
+ if "sample" in filename:
76
+ continue
77
+
78
+ filename_parsed = parse(filename)
79
+ if episode not in filename_parsed.episodes:
80
+ continue
81
+
82
+ if kitsu:
83
+ if filename_parsed.seasons:
84
+ continue
85
+ else:
86
+ if season not in filename_parsed.seasons:
87
+ continue
88
+
89
+ files[torrent["hash"]] = {
90
+ "index": torrent_files.index(file),
91
+ "title": filename,
92
+ "size": file["size"],
93
+ }
94
+
95
+ break
96
+ else:
97
+ for result in availability:
98
+ if not result["success"] or not result["data"]:
99
+ continue
100
+
101
+ for torrent in result["data"]:
102
+ torrent_files = torrent["files"]
103
+ for file in torrent_files:
104
+ filename = file["name"].split("/")[1]
105
+
106
+ if not is_video(filename):
107
+ continue
108
+
109
+ if "sample" in filename:
110
+ continue
111
+
112
+ files[torrent["hash"]] = {
113
+ "index": torrent_files.index(file),
114
+ "title": filename,
115
+ "size": file["size"],
116
+ }
117
+
118
+ break
119
+
120
+ return files
121
+
122
+ async def generate_download_link(self, hash: str, index: str):
123
+ try:
124
+ get_torrents = await self.session.get(
125
+ f"{self.api_url}/torrents/mylist?bypass_cache=true"
126
+ )
127
+ get_torrents = await get_torrents.json()
128
+ exists = False
129
+ for torrent in get_torrents["data"]:
130
+ if torrent["hash"] == hash:
131
+ torrent_id = torrent["id"]
132
+ exists = True
133
+ break
134
+ if not exists:
135
+ create_torrent = await self.session.post(
136
+ f"{self.api_url}/torrents/createtorrent",
137
+ data={"magnet": f"magnet:?xt=urn:btih:{hash}"},
138
+ )
139
+ create_torrent = await create_torrent.json()
140
+ torrent_id = create_torrent["data"]["torrent_id"]
141
+
142
+ # get_torrents = await self.session.get(
143
+ # f"{self.api_url}/torrents/mylist?bypass_cache=true"
144
+ # )
145
+ # get_torrents = await get_torrents.json()
146
+
147
+ # for torrent in get_torrents["data"]:
148
+ # if torrent["id"] == torrent_id:
149
+ # file_id = torrent["files"][int(index)]["id"]
150
+ # Useless, we already have file index
151
+
152
+ get_download_link = await self.session.get(
153
+ f"{self.api_url}/torrents/requestdl?token={self.debrid_api_key}&torrent_id={torrent_id}&file_id={index}&zip=false",
154
+ )
155
+ get_download_link = await get_download_link.json()
156
+
157
+ return get_download_link["data"]
158
+ except Exception as e:
159
+ logger.warning(
160
+ f"Exception while getting download link from TorBox for {hash}|{index}: {e}"
161
+ )
comet/main.py ADDED
@@ -0,0 +1,165 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import contextlib
2
+ import signal
3
+ import sys
4
+ import threading
5
+ import time
6
+ import traceback
7
+ from contextlib import asynccontextmanager
8
+
9
+ import uvicorn
10
+ from fastapi import FastAPI
11
+ from fastapi.middleware.cors import CORSMiddleware
12
+ from fastapi.staticfiles import StaticFiles
13
+ from starlette.middleware.base import BaseHTTPMiddleware
14
+ from starlette.requests import Request
15
+
16
+ from comet.api.core import main
17
+ from comet.api.stream import streams
18
+ from comet.utils.db import setup_database, teardown_database
19
+ from comet.utils.logger import logger
20
+ from comet.utils.models import settings
21
+
22
+
23
+ class LoguruMiddleware(BaseHTTPMiddleware):
24
+ async def dispatch(self, request: Request, call_next):
25
+ start_time = time.time()
26
+ try:
27
+ response = await call_next(request)
28
+ except Exception as e:
29
+ logger.exception(f"Exception during request processing: {e}")
30
+ raise
31
+ finally:
32
+ process_time = time.time() - start_time
33
+ logger.log(
34
+ "API",
35
+ f"{request.method} {request.url.path} - {response.status_code if 'response' in locals() else '500'} - {process_time:.2f}s",
36
+ )
37
+ return response
38
+
39
+
40
+ @asynccontextmanager
41
+ async def lifespan(app: FastAPI):
42
+ await setup_database()
43
+ yield
44
+ await teardown_database()
45
+
46
+
47
+ app = FastAPI(
48
+ title="Comet",
49
+ summary="Stremio's fastest torrent/debrid search add-on.",
50
+ version="1.0.0",
51
+ lifespan=lifespan,
52
+ redoc_url=None,
53
+ )
54
+
55
+ app.add_middleware(LoguruMiddleware)
56
+ app.add_middleware(
57
+ CORSMiddleware,
58
+ allow_origins=["*"],
59
+ allow_credentials=True,
60
+ allow_methods=["*"],
61
+ allow_headers=["*"],
62
+ )
63
+
64
+ app.mount("/static", StaticFiles(directory="comet/templates"), name="static")
65
+
66
+ app.include_router(main)
67
+ app.include_router(streams)
68
+
69
+
70
+ class Server(uvicorn.Server):
71
+ def install_signal_handlers(self):
72
+ pass
73
+
74
+ @contextlib.contextmanager
75
+ def run_in_thread(self):
76
+ thread = threading.Thread(target=self.run, name="Comet")
77
+ thread.start()
78
+ try:
79
+ while not self.started:
80
+ time.sleep(1e-3)
81
+ yield
82
+ except Exception as e:
83
+ logger.error(f"Error in server thread: {e}")
84
+ logger.exception(traceback.format_exc())
85
+ raise e
86
+ finally:
87
+ self.should_exit = True
88
+ sys.exit(0)
89
+
90
+
91
+ def signal_handler(sig, frame):
92
+ # This will handle kubernetes/docker shutdowns better
93
+ # Toss anything that needs to be gracefully shutdown here
94
+ logger.log("COMET", "Exiting Gracefully.")
95
+ sys.exit(0)
96
+
97
+
98
+ signal.signal(signal.SIGINT, signal_handler)
99
+ signal.signal(signal.SIGTERM, signal_handler)
100
+
101
+ config = uvicorn.Config(
102
+ app,
103
+ host=settings.FASTAPI_HOST,
104
+ port=settings.FASTAPI_PORT,
105
+ proxy_headers=True,
106
+ workers=settings.FASTAPI_WORKERS,
107
+ log_config=None,
108
+ )
109
+ server = Server(config=config)
110
+
111
+
112
+ def start_log():
113
+ logger.log(
114
+ "COMET",
115
+ f"Server started on http://{settings.FASTAPI_HOST}:{settings.FASTAPI_PORT} - {settings.FASTAPI_WORKERS} workers",
116
+ )
117
+ logger.log(
118
+ "COMET",
119
+ f"Dashboard Admin Password: {settings.DASHBOARD_ADMIN_PASSWORD} - http://{settings.FASTAPI_HOST}:{settings.FASTAPI_PORT}/active-connections?password={settings.DASHBOARD_ADMIN_PASSWORD}",
120
+ )
121
+ logger.log(
122
+ "COMET",
123
+ f"Database ({settings.DATABASE_TYPE}): {settings.DATABASE_PATH if settings.DATABASE_TYPE == 'sqlite' else settings.DATABASE_URL} - TTL: {settings.CACHE_TTL}s",
124
+ )
125
+ logger.log("COMET", f"Debrid Proxy: {settings.DEBRID_PROXY_URL}")
126
+
127
+ if settings.INDEXER_MANAGER_TYPE:
128
+ logger.log(
129
+ "COMET",
130
+ f"Indexer Manager: {settings.INDEXER_MANAGER_TYPE}|{settings.INDEXER_MANAGER_URL} - Timeout: {settings.INDEXER_MANAGER_TIMEOUT}s",
131
+ )
132
+ logger.log("COMET", f"Indexers: {', '.join(settings.INDEXER_MANAGER_INDEXERS)}")
133
+ logger.log("COMET", f"Get Torrent Timeout: {settings.GET_TORRENT_TIMEOUT}s")
134
+ else:
135
+ logger.log("COMET", "Indexer Manager: False")
136
+
137
+ if settings.ZILEAN_URL:
138
+ logger.log(
139
+ "COMET",
140
+ f"Zilean: {settings.ZILEAN_URL} - Take first: {settings.ZILEAN_TAKE_FIRST}",
141
+ )
142
+ else:
143
+ logger.log("COMET", "Zilean: False")
144
+
145
+ logger.log("COMET", f"Torrentio Scraper: {bool(settings.SCRAPE_TORRENTIO)}")
146
+ logger.log(
147
+ "COMET",
148
+ f"Debrid Stream Proxy: {bool(settings.PROXY_DEBRID_STREAM)} - Password: {settings.PROXY_DEBRID_STREAM_PASSWORD} - Max Connections: {settings.PROXY_DEBRID_STREAM_MAX_CONNECTIONS} - Default Debrid Service: {settings.PROXY_DEBRID_STREAM_DEBRID_DEFAULT_SERVICE} - Default Debrid API Key: {settings.PROXY_DEBRID_STREAM_DEBRID_DEFAULT_APIKEY}",
149
+ )
150
+ logger.log("COMET", f"Title Match Check: {bool(settings.TITLE_MATCH_CHECK)}")
151
+ logger.log("COMET", f"Custom Header HTML: {bool(settings.CUSTOM_HEADER_HTML)}")
152
+
153
+
154
+ with server.run_in_thread():
155
+ start_log()
156
+ try:
157
+ while True:
158
+ time.sleep(1) # Keep the main thread alive
159
+ except KeyboardInterrupt:
160
+ logger.log("COMET", "Server stopped by user")
161
+ except Exception as e:
162
+ logger.error(f"Unexpected error: {e}")
163
+ logger.exception(traceback.format_exc())
164
+ finally:
165
+ logger.log("COMET", "Server Shutdown")
comet/templates/index.html ADDED
@@ -0,0 +1,794 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html class="sl-theme-dark">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
6
+ <meta content="Comet" property="og:title" />
7
+ <meta content="Stremio's fastest torrent/debrid search add-on." property="og:description" />
8
+ <meta content="https://comet.fast" property="og:url" />
9
+ <meta content="https://i.imgur.com/jmVoVMu.jpeg" property="og:image" />
10
+ <meta content="#6b6ef8" data-react-helmet="true" name="theme-color" />
11
+
12
+ <title>Comet - Stremio's fastest torrent/debrid search add-on.</title>
13
+ <link id="favicon" rel="icon" type="image/x-icon" href="https://i.imgur.com/jmVoVMu.jpeg">
14
+
15
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.15.1/cdn/themes/dark.css" />
16
+ <script type="module" src="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.15.1/cdn/shoelace-autoloader.js"></script>
17
+
18
+ <style>
19
+ :not(:defined) {
20
+ visibility: hidden;
21
+ }
22
+
23
+ body {
24
+ opacity: 0;
25
+ display: flex;
26
+ flex-direction: column;
27
+ justify-content: center;
28
+ align-items: center;
29
+ min-height: 100vh;
30
+ margin: 0;
31
+ background: radial-gradient(ellipse at bottom, #25292c 0%, #0c0d13 100%);
32
+ 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";
33
+ font-size: 1rem;
34
+ font-weight: 400;
35
+ }
36
+
37
+ body.ready {
38
+ opacity: 1;
39
+ transition: 0.25s opacity;
40
+ }
41
+
42
+ ::-webkit-scrollbar {
43
+ overflow: hidden;
44
+ }
45
+
46
+ .header {
47
+ text-align: center;
48
+ width: 40%;
49
+ margin-bottom: 20px;
50
+ }
51
+
52
+ .comet-text {
53
+ font-size: calc(1.375rem + 1.5vw);
54
+ font-weight: 500;
55
+ margin-bottom: 0;
56
+ }
57
+
58
+ .emoji {
59
+ width: 1em;
60
+ height: 1em;
61
+ vertical-align: middle;
62
+ }
63
+
64
+ .form-container {
65
+ background-color: #1a1d20;
66
+ padding: 2rem;
67
+ border-radius: 0.375rem;
68
+ box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
69
+ width: 80vw;
70
+ max-width: 950px;
71
+ margin-bottom: 50px;
72
+ }
73
+
74
+ .form-item {
75
+ margin-bottom: 0.75rem;
76
+ }
77
+
78
+ sl-details {
79
+ margin-bottom: 0.75rem;
80
+ }
81
+
82
+ .centered-item {
83
+ display: flex;
84
+ justify-content: center;
85
+ }
86
+
87
+ .margin-left-install {
88
+ margin-right: 10px;
89
+ }
90
+
91
+ .stars {
92
+ position: fixed;
93
+ top: 0;
94
+ left: 0;
95
+ width: 100%;
96
+ height: 120%;
97
+ transform: rotate(-45deg);
98
+ z-index: -1;
99
+ }
100
+ .star {
101
+ --star-color: var(--primary-color);
102
+ --star-tail-length: 6em;
103
+ --star-tail-height: 2px;
104
+ --star-width: calc(var(--star-tail-length) / 6);
105
+ --fall-duration: 9s;
106
+ --tail-fade-duration: var(--fall-duration);
107
+ position: absolute;
108
+ top: var(--top-offset);
109
+ left: 0;
110
+ width: var(--star-tail-length);
111
+ height: var(--star-tail-height);
112
+ color: var(--star-color);
113
+ background: linear-gradient(45deg, currentColor, transparent);
114
+ border-radius: 50%;
115
+ filter: drop-shadow(0 0 6px currentColor);
116
+ transform: translate3d(104em, 0, 0);
117
+ animation: fall var(--fall-duration) var(--fall-delay) linear infinite, tail-fade var(--tail-fade-duration) var(--fall-delay) ease-out infinite;
118
+ }
119
+ @media screen and (max-width: 750px) {
120
+ .star {
121
+ animation: fall var(--fall-duration) var(--fall-delay) linear infinite;
122
+ }
123
+ }
124
+ .star:nth-child(1) {
125
+ --star-tail-length: 6.14em;
126
+ --top-offset: 3.64vh;
127
+ --fall-duration: 10.878s;
128
+ --fall-delay: 0.034s;
129
+ }
130
+ .star:nth-child(2) {
131
+ --star-tail-length: 5.08em;
132
+ --top-offset: 69.69vh;
133
+ --fall-duration: 11.372s;
134
+ --fall-delay: 1.679s;
135
+ }
136
+ .star:nth-child(3) {
137
+ --star-tail-length: 6.1em;
138
+ --top-offset: 64.3vh;
139
+ --fall-duration: 7.088s;
140
+ --fall-delay: 3.382s;
141
+ }
142
+ .star:nth-child(4) {
143
+ --star-tail-length: 6.66em;
144
+ --top-offset: 34.91vh;
145
+ --fall-duration: 6.184s;
146
+ --fall-delay: 9.61s;
147
+ }
148
+ .star:nth-child(5) {
149
+ --star-tail-length: 6.89em;
150
+ --top-offset: 24.92vh;
151
+ --fall-duration: 9.465s;
152
+ --fall-delay: 9.12s;
153
+ }
154
+ .star:nth-child(6) {
155
+ --star-tail-length: 6.5em;
156
+ --top-offset: 51.9vh;
157
+ --fall-duration: 10.52s;
158
+ --fall-delay: 0.214s;
159
+ }
160
+ .star:nth-child(7) {
161
+ --star-tail-length: 5.58em;
162
+ --top-offset: 24.15vh;
163
+ --fall-duration: 8.9s;
164
+ --fall-delay: 0.499s;
165
+ }
166
+ .star:nth-child(8) {
167
+ --star-tail-length: 6.26em;
168
+ --top-offset: 59.4vh;
169
+ --fall-duration: 7.671s;
170
+ --fall-delay: 1.694s;
171
+ }
172
+ .star:nth-child(9) {
173
+ --star-tail-length: 6.46em;
174
+ --top-offset: 54.52vh;
175
+ --fall-duration: 6.484s;
176
+ --fall-delay: 4.616s;
177
+ }
178
+ .star:nth-child(10) {
179
+ --star-tail-length: 5.34em;
180
+ --top-offset: 74.03vh;
181
+ --fall-duration: 8.565s;
182
+ --fall-delay: 1.159s;
183
+ }
184
+ .star:nth-child(11) {
185
+ --star-tail-length: 6.84em;
186
+ --top-offset: 90.94vh;
187
+ --fall-duration: 10.133s;
188
+ --fall-delay: 6.108s;
189
+ }
190
+ .star:nth-child(12) {
191
+ --star-tail-length: 5.48em;
192
+ --top-offset: 97.27vh;
193
+ --fall-duration: 10.248s;
194
+ --fall-delay: 4.186s;
195
+ }
196
+ .star:nth-child(13) {
197
+ --star-tail-length: 7.21em;
198
+ --top-offset: 17.75vh;
199
+ --fall-duration: 10.549s;
200
+ --fall-delay: 1.868s;
201
+ }
202
+ .star:nth-child(14) {
203
+ --star-tail-length: 6.35em;
204
+ --top-offset: 94.98vh;
205
+ --fall-duration: 9.682s;
206
+ --fall-delay: 9.327s;
207
+ }
208
+ .star:nth-child(15) {
209
+ --star-tail-length: 5.18em;
210
+ --top-offset: 52.87vh;
211
+ --fall-duration: 9.934s;
212
+ --fall-delay: 8.919s;
213
+ }
214
+ .star:nth-child(16) {
215
+ --star-tail-length: 5.07em;
216
+ --top-offset: 50.26vh;
217
+ --fall-duration: 11.826s;
218
+ --fall-delay: 8.478s;
219
+ }
220
+ .star:nth-child(17) {
221
+ --star-tail-length: 7.01em;
222
+ --top-offset: 85.73vh;
223
+ --fall-duration: 9.87s;
224
+ --fall-delay: 1.206s;
225
+ }
226
+ .star:nth-child(18) {
227
+ --star-tail-length: 6.39em;
228
+ --top-offset: 2.81vh;
229
+ --fall-duration: 10.292s;
230
+ --fall-delay: 4.394s;
231
+ }
232
+ .star:nth-child(19) {
233
+ --star-tail-length: 5.27em;
234
+ --top-offset: 56.83vh;
235
+ --fall-duration: 8.944s;
236
+ --fall-delay: 8.779s;
237
+ }
238
+ .star:nth-child(20) {
239
+ --star-tail-length: 6.07em;
240
+ --top-offset: 54.55vh;
241
+ --fall-duration: 9.324s;
242
+ --fall-delay: 7.375s;
243
+ }
244
+ .star:nth-child(21) {
245
+ --star-tail-length: 6.1em;
246
+ --top-offset: 87.67vh;
247
+ --fall-duration: 11.943s;
248
+ --fall-delay: 6.919s;
249
+ }
250
+ .star:nth-child(22) {
251
+ --star-tail-length: 5.98em;
252
+ --top-offset: 70.04vh;
253
+ --fall-duration: 9.995s;
254
+ --fall-delay: 4.472s;
255
+ }
256
+ .star:nth-child(23) {
257
+ --star-tail-length: 6.34em;
258
+ --top-offset: 77.19vh;
259
+ --fall-duration: 10.073s;
260
+ --fall-delay: 8.354s;
261
+ }
262
+ .star:nth-child(24) {
263
+ --star-tail-length: 6.95em;
264
+ --top-offset: 14.48vh;
265
+ --fall-duration: 9.028s;
266
+ --fall-delay: 7.638s;
267
+ }
268
+ .star:nth-child(25) {
269
+ --star-tail-length: 6.23em;
270
+ --top-offset: 8.48vh;
271
+ --fall-duration: 7.427s;
272
+ --fall-delay: 0.915s;
273
+ }
274
+ .star:nth-child(26) {
275
+ --star-tail-length: 5.09em;
276
+ --top-offset: 6.56vh;
277
+ --fall-duration: 7.706s;
278
+ --fall-delay: 2.841s;
279
+ }
280
+ .star:nth-child(27) {
281
+ --star-tail-length: 7.01em;
282
+ --top-offset: 92.85vh;
283
+ --fall-duration: 7.359s;
284
+ --fall-delay: 7.229s;
285
+ }
286
+ .star:nth-child(28) {
287
+ --star-tail-length: 5.49em;
288
+ --top-offset: 27.89vh;
289
+ --fall-duration: 10.344s;
290
+ --fall-delay: 2.346s;
291
+ }
292
+ .star:nth-child(29) {
293
+ --star-tail-length: 5.82em;
294
+ --top-offset: 56.08vh;
295
+ --fall-duration: 10.911s;
296
+ --fall-delay: 4.231s;
297
+ }
298
+ .star:nth-child(30) {
299
+ --star-tail-length: 7.24em;
300
+ --top-offset: 22.54vh;
301
+ --fall-duration: 9.344s;
302
+ --fall-delay: 2.112s;
303
+ }
304
+ .star:nth-child(31) {
305
+ --star-tail-length: 6.8em;
306
+ --top-offset: 59.49vh;
307
+ --fall-duration: 7.059s;
308
+ --fall-delay: 0.924s;
309
+ }
310
+ .star:nth-child(32) {
311
+ --star-tail-length: 5.22em;
312
+ --top-offset: 44.01vh;
313
+ --fall-duration: 10.121s;
314
+ --fall-delay: 0.591s;
315
+ }
316
+ .star:nth-child(33) {
317
+ --star-tail-length: 6.1em;
318
+ --top-offset: 78.61vh;
319
+ --fall-duration: 8.306s;
320
+ --fall-delay: 4.403s;
321
+ }
322
+ .star:nth-child(34) {
323
+ --star-tail-length: 7.26em;
324
+ --top-offset: 85.76vh;
325
+ --fall-duration: 7.058s;
326
+ --fall-delay: 6.772s;
327
+ }
328
+ .star:nth-child(35) {
329
+ --star-tail-length: 7.01em;
330
+ --top-offset: 77.17vh;
331
+ --fall-duration: 6.29s;
332
+ --fall-delay: 1.468s;
333
+ }
334
+ .star:nth-child(36) {
335
+ --star-tail-length: 5.17em;
336
+ --top-offset: 13.63vh;
337
+ --fall-duration: 6.739s;
338
+ --fall-delay: 0.019s;
339
+ }
340
+ .star:nth-child(37) {
341
+ --star-tail-length: 6.41em;
342
+ --top-offset: 70.18vh;
343
+ --fall-duration: 6.177s;
344
+ --fall-delay: 8.148s;
345
+ }
346
+ .star:nth-child(38) {
347
+ --star-tail-length: 5.32em;
348
+ --top-offset: 62.65vh;
349
+ --fall-duration: 10.476s;
350
+ --fall-delay: 0.98s;
351
+ }
352
+ .star:nth-child(39) {
353
+ --star-tail-length: 7.24em;
354
+ --top-offset: 66.12vh;
355
+ --fall-duration: 8.449s;
356
+ --fall-delay: 4.255s;
357
+ }
358
+ .star:nth-child(40) {
359
+ --star-tail-length: 6.73em;
360
+ --top-offset: 14.73vh;
361
+ --fall-duration: 9.857s;
362
+ --fall-delay: 6.867s;
363
+ }
364
+ .star:nth-child(41) {
365
+ --star-tail-length: 5.25em;
366
+ --top-offset: 45.23vh;
367
+ --fall-duration: 7.898s;
368
+ --fall-delay: 4.966s;
369
+ }
370
+ .star:nth-child(42) {
371
+ --star-tail-length: 6.73em;
372
+ --top-offset: 36.17vh;
373
+ --fall-duration: 7.32s;
374
+ --fall-delay: 3.93s;
375
+ }
376
+ .star:nth-child(43) {
377
+ --star-tail-length: 7.38em;
378
+ --top-offset: 83.09vh;
379
+ --fall-duration: 7.394s;
380
+ --fall-delay: 5.388s;
381
+ }
382
+ .star:nth-child(44) {
383
+ --star-tail-length: 5.18em;
384
+ --top-offset: 98.36vh;
385
+ --fall-duration: 6.905s;
386
+ --fall-delay: 2.771s;
387
+ }
388
+ .star:nth-child(45) {
389
+ --star-tail-length: 6.66em;
390
+ --top-offset: 27.99vh;
391
+ --fall-duration: 7.62s;
392
+ --fall-delay: 3.624s;
393
+ }
394
+ .star:nth-child(46) {
395
+ --star-tail-length: 5.19em;
396
+ --top-offset: 92vh;
397
+ --fall-duration: 9.158s;
398
+ --fall-delay: 1.984s;
399
+ }
400
+ .star:nth-child(47) {
401
+ --star-tail-length: 6.16em;
402
+ --top-offset: 2.87vh;
403
+ --fall-duration: 9.266s;
404
+ --fall-delay: 4.04s;
405
+ }
406
+ .star:nth-child(48) {
407
+ --star-tail-length: 6.34em;
408
+ --top-offset: 19.39vh;
409
+ --fall-duration: 7.503s;
410
+ --fall-delay: 0.045s;
411
+ }
412
+ .star:nth-child(49) {
413
+ --star-tail-length: 6.85em;
414
+ --top-offset: 79.92vh;
415
+ --fall-duration: 7.472s;
416
+ --fall-delay: 1.514s;
417
+ }
418
+ .star:nth-child(50) {
419
+ --star-tail-length: 7.35em;
420
+ --top-offset: 63.71vh;
421
+ --fall-duration: 8.117s;
422
+ --fall-delay: 4.46s;
423
+ }
424
+ .star::before, .star::after {
425
+ position: absolute;
426
+ content: "";
427
+ top: 0;
428
+ left: calc(var(--star-width) / -2);
429
+ width: var(--star-width);
430
+ height: 100%;
431
+ background: linear-gradient(45deg, transparent, currentColor, transparent);
432
+ border-radius: inherit;
433
+ animation: blink 2s linear infinite;
434
+ }
435
+ .star::before {
436
+ transform: rotate(45deg);
437
+ }
438
+ .star::after {
439
+ transform: rotate(-45deg);
440
+ }
441
+
442
+ @keyframes fall {
443
+ to {
444
+ transform: translate3d(-30em, 0, 0);
445
+ }
446
+ }
447
+ @keyframes tail-fade {
448
+ 0%, 50% {
449
+ width: var(--star-tail-length);
450
+ opacity: 1;
451
+ }
452
+ 70%, 80% {
453
+ width: 0;
454
+ opacity: 0.4;
455
+ }
456
+ 100% {
457
+ width: 0;
458
+ opacity: 0;
459
+ }
460
+ }
461
+ @keyframes blink {
462
+ 50% {
463
+ opacity: 0.6;
464
+ }
465
+ }
466
+ </style>
467
+ </head>
468
+
469
+ <body>
470
+ <div class="stars"></div>
471
+
472
+ <script>
473
+ let hasTouchScreen = false;
474
+ if ("maxTouchPoints" in navigator) {
475
+ hasTouchScreen = navigator.maxTouchPoints > 0;
476
+ } else if ("msMaxTouchPoints" in navigator) {
477
+ hasTouchScreen = navigator.msMaxTouchPoints > 0;
478
+ } else {
479
+ const mQ = matchMedia?.("(pointer:coarse)");
480
+ if (mQ?.media === "(pointer:coarse)") {
481
+ hasTouchScreen = !!mQ.matches;
482
+ } else if ("orientation" in window) {
483
+ hasTouchScreen = true;
484
+ } else {
485
+ const UA = navigator.userAgent;
486
+ hasTouchScreen =
487
+ /\b(BlackBerry|webOS|iPhone|IEMobile)\b/i.test(UA) ||
488
+ /\b(Android|Windows Phone|iPad|iPod)\b/i.test(UA);
489
+ }
490
+ }
491
+
492
+ if (!hasTouchScreen) {
493
+ document.addEventListener("DOMContentLoaded", function() {
494
+ const starsContainer = document.querySelector('.stars');
495
+ const starCount = 30
496
+
497
+ for (let i = 0; i < starCount; i++) {
498
+ const star = document.createElement("div");
499
+ star.classList.add("star");
500
+ starsContainer.appendChild(star);
501
+ }
502
+ });
503
+ }
504
+ </script>
505
+ <div class="header">
506
+ <p class="comet-text">
507
+ <img class="emoji" src="https://fonts.gstatic.com/s/e/notoemoji/latest/1f4ab/512.gif">
508
+ Comet - <a href="https://github.com/g0ldyy/comet">GitHub</a>
509
+ </p>
510
+ {{CUSTOM_HEADER_HTML|safe}}
511
+ </div>
512
+
513
+ <div class="form-container">
514
+ <div class="form-item">
515
+ <sl-select id="indexers" multiple clearable label="Indexers" placeholder="Select indexers" {% if not indexerManager %} disabled {% endif %}></sl-select>
516
+ </div>
517
+
518
+ <div class="form-item">
519
+ <sl-select id="languages" multiple clearable label="Languages" placeholder="Select languages"></sl-select>
520
+ </div>
521
+
522
+ <div class="form-item">
523
+ <sl-select id="resolutions" multiple clearable label="Resolutions" placeholder="Select resolutions"></sl-select>
524
+ </div>
525
+
526
+ <div class="form-item">
527
+ <sl-input id="maxResults" type="number" min=0 value=0 label="Max Results" placeholder="Enter max results"></sl-input>
528
+ </div>
529
+
530
+ <div class="form-item">
531
+ <sl-input id="maxSize" type="number" min=0 value=0 label="Max Size (GB)" placeholder="Enter max size in gigabytes"></sl-input>
532
+ </div>
533
+
534
+ <div class="form-item">
535
+ <sl-input id="debridStreamProxyPassword" label="Debrid Stream Proxy Password" placeholder="{% if not proxyDebridStream %}Configuration required, see docs{% else %}Enter password{% endif %}" help-text="Debrid Stream Proxying allows you to use your Debrid Service from multiple IPs at same time!" {% if not proxyDebridStream %} disabled {% endif %}></sl-input>
536
+ </div>
537
+
538
+ <div class="form-item">
539
+ <sl-select id="debridService" value="realdebrid" label="Debrid Service" placeholder="Select debrid service">
540
+ <sl-option value="realdebrid">Real-Debrid</sl-option>
541
+ <sl-option value="alldebrid">All-Debrid</sl-option>
542
+ <sl-option value="premiumize">Premiumize</sl-option>
543
+ <sl-option value="torbox">TorBox</sl-option>
544
+ <sl-option value="debridlink">Debrid-Link</sl-option>
545
+ </sl-select>
546
+ </div>
547
+
548
+ <div class="form-item">
549
+ <label for="debridService">
550
+ Debrid API Key -
551
+ <a id="apiKeyLink" href="https://real-debrid.com/apitoken" target="_blank">Get It Here</a>
552
+ </label>
553
+ <sl-input id="debridApiKey" placeholder="Enter API key"></sl-input>
554
+ </div>
555
+
556
+ <sl-details summary="Advanced Settings">
557
+ <div class="form-item">
558
+ <sl-select id="resultFormat" multiple label="Result Format" placeholder="Select what to show in result title" hoist max-options-visible=10>
559
+ </sl-select>
560
+ </div>
561
+ </sl-details>
562
+
563
+ <script>
564
+ document.getElementById("debridService").addEventListener("sl-change", function(event) {
565
+ const selectedService = event.target.value;
566
+ const apiKeyLink = document.getElementById("apiKeyLink");
567
+
568
+ if (selectedService === "realdebrid") {
569
+ apiKeyLink.href = "https://real-debrid.com/apitoken";
570
+ } else if (selectedService === "alldebrid") {
571
+ apiKeyLink.href = "https://alldebrid.com/apikeys";
572
+ } else if (selectedService === "premiumize") {
573
+ apiKeyLink.href = "https://premiumize.me/account";
574
+ } else if (selectedService === "torbox") {
575
+ apiKeyLink.href = "https://torbox.app/settings";
576
+ } else if (selectedService === "debridlink") {
577
+ apiKeyLink.href = "https://debrid-link.com/webapp/apikey";
578
+ }
579
+ });
580
+ </script>
581
+
582
+ <div class="centered-item">
583
+ <sl-button id="install" variant="neutral" class="margin-left-install">Install</sl-button>
584
+ <sl-alert id="installAlert" variant="neutral" duration="3000" closable>
585
+ <sl-icon slot="icon" name="clipboard2-check"></sl-icon>
586
+ <strong>Attempting to add the addon to Stremio...</strong>
587
+ </sl-alert>
588
+
589
+ <sl-button id="copy-link" variant="neutral">Copy Link</sl-button>
590
+ <sl-alert id="copyAlert" variant="neutral" duration="3000" closable>
591
+ <sl-icon slot="icon" name="clipboard2-check"></sl-icon>
592
+ <strong>The Stremio addon link has been automatically copied.</strong>
593
+ </sl-alert>
594
+
595
+ <script type="module">
596
+ const languagesEmojis = {
597
+ "Multi": "🌎",
598
+ "English": "🇬🇧",
599
+ "Japanese": "🇯🇵",
600
+ "Chinese": "🇨🇳",
601
+ "Russian": "🇷🇺",
602
+ "Arabic": "🇸🇦",
603
+ "Portuguese": "🇵🇹",
604
+ "Spanish": "🇪🇸",
605
+ "French": "🇫🇷",
606
+ "German": "🇩🇪",
607
+ "Italian": "🇮🇹",
608
+ "Korean": "🇰🇷",
609
+ "Hindi": "🇮🇳",
610
+ "Bengali": "🇧🇩",
611
+ "Punjabi": "🇵🇰",
612
+ "Marathi": "🇮🇳",
613
+ "Gujarati": "🇮🇳",
614
+ "Tamil": "🇮🇳",
615
+ "Telugu": "🇮🇳",
616
+ "Kannada": "🇮🇳",
617
+ "Malayalam": "🇮🇳",
618
+ "Thai": "🇹🇭",
619
+ "Vietnamese": "🇻🇳",
620
+ "Indonesian": "🇮🇩",
621
+ "Turkish": "🇹🇷",
622
+ "Hebrew": "🇮🇱",
623
+ "Persian": "🇮🇷",
624
+ "Ukrainian": "🇺🇦",
625
+ "Greek": "🇬🇷",
626
+ "Lithuanian": "🇱🇹",
627
+ "Latvian": "🇱🇻",
628
+ "Estonian": "🇪🇪",
629
+ "Polish": "🇵🇱",
630
+ "Czech": "🇨🇿",
631
+ "Slovak": "🇸🇰",
632
+ "Hungarian": "🇭🇺",
633
+ "Romanian": "🇷🇴",
634
+ "Bulgarian": "🇧🇬",
635
+ "Serbian": "🇷🇸",
636
+ "Croatian": "🇭🇷",
637
+ "Slovenian": "🇸🇮",
638
+ "Dutch": "🇳🇱",
639
+ "Danish": "🇩🇰",
640
+ "Finnish": "🇫🇮",
641
+ "Swedish": "🇸🇪",
642
+ "Norwegian": "🇳🇴",
643
+ "Malay": "🇲🇾",
644
+ "Latino": "💃🏻",
645
+ };
646
+
647
+ let defaultLanguages = [];
648
+ let defaultResolutions = [];
649
+ let defaultResultFormat = [];
650
+
651
+ document.addEventListener("DOMContentLoaded", async () => {
652
+ await Promise.allSettled([
653
+ customElements.whenDefined("sl-button"),
654
+ customElements.whenDefined("sl-alert"),
655
+ customElements.whenDefined("sl-select"),
656
+ customElements.whenDefined("sl-input"),
657
+ customElements.whenDefined("sl-option"),
658
+ customElements.whenDefined("sl-icon")
659
+ ]);
660
+
661
+ const webConfig = {{webConfig|tojson}};
662
+ populateSelect("indexers", webConfig.indexers);
663
+ populateSelect("languages", webConfig.languages);
664
+ populateSelect("resolutions", webConfig.resolutions);
665
+ populateSelect("resultFormat", webConfig.resultFormat);
666
+ document.body.classList.add("ready");
667
+ defaultLanguages = webConfig.languages;
668
+ defaultResolutions = webConfig.resolutions;
669
+ defaultResultFormat = webConfig.resultFormat;
670
+
671
+ // try populate the form from previous settings
672
+ try {
673
+ const settings = getSettingsFromUrl();
674
+ if (settings != null) { populateFormFromSettings(settings); }
675
+ } catch (error) {
676
+ console.log("Failed to retrieve or parse settings from URL:", error);
677
+ }
678
+
679
+ });
680
+
681
+ function populateSelect(selectId, options) {
682
+ const selectElement = document.getElementById(selectId);
683
+ options.forEach(option => {
684
+ const optionElement = document.createElement("sl-option");
685
+ optionElement.value = option;
686
+
687
+ if (selectId === "languages") {
688
+ // For languages, prepend the flag emoji if it exists
689
+ const flag = languagesEmojis[option] || '';
690
+ optionElement.textContent = `${flag} ${option}`;
691
+ } else {
692
+ // For other selects, just use the option text as is
693
+ optionElement.textContent = option;
694
+ }
695
+
696
+ selectElement.appendChild(optionElement);
697
+ });
698
+
699
+ // Set the default value
700
+ selectElement.value = options; // Assuming first option as default
701
+ }
702
+
703
+ const installButton = document.querySelector("#install");
704
+ const installAlert = document.querySelector('#installAlert');
705
+ const copyLinkButton = document.querySelector("#copy-link");
706
+ const copyAlert = document.querySelector('#copyAlert');
707
+
708
+ function getSettings() {
709
+ const indexers = Array.from(document.getElementById("indexers").selectedOptions).map(option => option.value);
710
+ const languages = Array.from(document.getElementById("languages").selectedOptions).map(option => option.value);
711
+ const resolutions = Array.from(document.getElementById("resolutions").selectedOptions).map(option => option.value);
712
+ const maxResults = document.getElementById("maxResults").value;
713
+ const maxSize = document.getElementById("maxSize").value;
714
+ const resultFormat = Array.from(document.getElementById("resultFormat").selectedOptions).map(option => option.value);
715
+ const debridService = document.getElementById("debridService").value;
716
+ const debridApiKey = document.getElementById("debridApiKey").value;
717
+ const debridStreamProxyPassword = document.getElementById("debridStreamProxyPassword").value;
718
+ const selectedLanguages = languages.length === defaultLanguages.length && languages.every((val, index) => val === defaultLanguages[index]) ? ["All"] : languages;
719
+ const selectedResolutions = resolutions.length === defaultResolutions.length && resolutions.every((val, index) => val === defaultResolutions[index]) ? ["All"] : resolutions;
720
+ const selectedResultFormat = resultFormat.length === defaultResultFormat.length && resultFormat.every((val, index) => val === defaultResultFormat[index]) ? ["All"] : resultFormat;
721
+
722
+ return {
723
+ indexers: indexers,
724
+ maxResults: parseInt(maxResults),
725
+ maxSize: parseFloat(maxSize * 1073741824),
726
+ resultFormat: selectedResultFormat,
727
+ resolutions: selectedResolutions,
728
+ languages: selectedLanguages,
729
+ debridService: debridService,
730
+ debridApiKey: debridApiKey,
731
+ debridStreamProxyPassword: debridStreamProxyPassword,
732
+ };
733
+ }
734
+
735
+ copyLinkButton.addEventListener("click", () => {
736
+ const settings = getSettings();
737
+ const settingsString = btoa(JSON.stringify(settings))
738
+
739
+ const textArea = document.createElement("textarea");
740
+ textArea.value = `${window.location.origin}/${settingsString}/manifest.json`;
741
+ document.body.appendChild(textArea);
742
+ textArea.focus();
743
+ textArea.select();
744
+ document.execCommand("copy");
745
+ document.body.removeChild(textArea);
746
+
747
+ console.log(settingsString);
748
+
749
+ copyAlert.toast();
750
+ });
751
+
752
+ installButton.addEventListener("click", () => {
753
+ const settings = getSettings();
754
+ const settingsString = btoa(JSON.stringify(settings))
755
+ window.location.href = `stremio://${window.location.host}/${settingsString}/manifest.json`;
756
+ console.log(settingsString);
757
+ installAlert.toast();
758
+ });
759
+
760
+ function getSettingsFromUrl() {
761
+ const url = window.location.href;
762
+ // Try parse settings from the URL
763
+ if (url.split("/").length < 5) {
764
+ console.log("no previous settings"); return null;
765
+ }
766
+ const settingsString = url.split("/")[3];
767
+ if (typeof settingsString === "undefined") {
768
+ console.log("can't fetch previous settings", settingsString)
769
+ return null;
770
+ }
771
+ const settingsJson = atob(settingsString);
772
+ return JSON.parse(settingsJson);
773
+ }
774
+
775
+ function populateFormFromSettings(settings) {
776
+ document.getElementById("maxResults").value = settings.maxResults;
777
+ document.getElementById("maxSize").value = settings.maxSize / 1073741824; // Convert back from bytes to GB
778
+ document.getElementById("debridService").value = settings.debridService;
779
+ document.getElementById("debridApiKey").value = settings.debridApiKey;
780
+ document.getElementById("debridStreamProxyPassword").value = settings.debridStreamProxyPassword;
781
+ document.getElementById("indexers").value = settings.indexers;
782
+ if (settings.languages != "All")
783
+ document.getElementById("languages").value = settings.languages;
784
+ if (settings.resolutions != "All")
785
+ document.getElementById("resolutions").value = settings.resolutions;
786
+ if (settings.resultFormat != "All")
787
+ document.getElementById("resultFormat").value = settings.resultFormat;
788
+ }
789
+
790
+ </script>
791
+ </div>
792
+ </div>
793
+ </body>
794
+ </html>
comet/utils/__init__.py ADDED
File without changes
comet/utils/db.py ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+
3
+ from comet.utils.logger import logger
4
+ from comet.utils.models import database, settings
5
+
6
+
7
+ async def setup_database():
8
+ try:
9
+ if settings.DATABASE_TYPE == "sqlite":
10
+ os.makedirs(os.path.dirname(settings.DATABASE_PATH), exist_ok=True)
11
+
12
+ if not os.path.exists(settings.DATABASE_PATH):
13
+ open(settings.DATABASE_PATH, "a").close()
14
+
15
+ await database.connect()
16
+ await database.execute(
17
+ "CREATE TABLE IF NOT EXISTS cache (cacheKey TEXT PRIMARY KEY, timestamp INTEGER, results TEXT)"
18
+ )
19
+ await database.execute(
20
+ "CREATE TABLE IF NOT EXISTS download_links (debrid_key TEXT, hash TEXT, file_index TEXT, link TEXT, timestamp INTEGER, PRIMARY KEY (debrid_key, hash, file_index))"
21
+ )
22
+ await database.execute("DROP TABLE IF EXISTS active_connections")
23
+ await database.execute(
24
+ "CREATE TABLE IF NOT EXISTS active_connections (id TEXT PRIMARY KEY, ip TEXT, content TEXT, timestamp INTEGER)"
25
+ )
26
+ except Exception as e:
27
+ logger.error(f"Error setting up the database: {e}")
28
+
29
+
30
+ async def teardown_database():
31
+ try:
32
+ await database.disconnect()
33
+ except Exception as e:
34
+ logger.error(f"Error tearing down the database: {e}")
comet/utils/general.py ADDED
@@ -0,0 +1,614 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import base64
2
+ import hashlib
3
+ import json
4
+ import re
5
+ import aiohttp
6
+ import bencodepy
7
+ import PTT
8
+ import asyncio
9
+
10
+ from RTN import parse, title_match
11
+ from curl_cffi import requests
12
+ from fastapi import Request
13
+
14
+ from comet.utils.logger import logger
15
+ from comet.utils.models import settings, ConfigModel
16
+
17
+ languages_emojis = {
18
+ "multi": "🌎", # Dubbed
19
+ "en": "🇬🇧", # English
20
+ "ja": "🇯🇵", # Japanese
21
+ "zh": "🇨🇳", # Chinese
22
+ "ru": "🇷🇺", # Russian
23
+ "ar": "🇸🇦", # Arabic
24
+ "pt": "🇵🇹", # Portuguese
25
+ "es": "🇪🇸", # Spanish
26
+ "fr": "🇫🇷", # French
27
+ "de": "🇩🇪", # German
28
+ "it": "🇮🇹", # Italian
29
+ "ko": "🇰🇷", # Korean
30
+ "hi": "🇮🇳", # Hindi
31
+ "bn": "🇧🇩", # Bengali
32
+ "pa": "🇵🇰", # Punjabi
33
+ "mr": "🇮🇳", # Marathi
34
+ "gu": "🇮🇳", # Gujarati
35
+ "ta": "🇮🇳", # Tamil
36
+ "te": "🇮🇳", # Telugu
37
+ "kn": "🇮🇳", # Kannada
38
+ "ml": "🇮🇳", # Malayalam
39
+ "th": "🇹🇭", # Thai
40
+ "vi": "🇻🇳", # Vietnamese
41
+ "id": "🇮🇩", # Indonesian
42
+ "tr": "🇹🇷", # Turkish
43
+ "he": "🇮🇱", # Hebrew
44
+ "fa": "🇮🇷", # Persian
45
+ "uk": "🇺🇦", # Ukrainian
46
+ "el": "🇬🇷", # Greek
47
+ "lt": "🇱🇹", # Lithuanian
48
+ "lv": "🇱🇻", # Latvian
49
+ "et": "🇪🇪", # Estonian
50
+ "pl": "🇵🇱", # Polish
51
+ "cs": "🇨🇿", # Czech
52
+ "sk": "🇸🇰", # Slovak
53
+ "hu": "🇭🇺", # Hungarian
54
+ "ro": "🇷🇴", # Romanian
55
+ "bg": "🇧🇬", # Bulgarian
56
+ "sr": "🇷🇸", # Serbian
57
+ "hr": "🇭🇷", # Croatian
58
+ "sl": "🇸🇮", # Slovenian
59
+ "nl": "🇳🇱", # Dutch
60
+ "da": "🇩🇰", # Danish
61
+ "fi": "🇫🇮", # Finnish
62
+ "sv": "🇸🇪", # Swedish
63
+ "no": "🇳🇴", # Norwegian
64
+ "ms": "🇲🇾", # Malay
65
+ "la": "💃🏻", # Latino
66
+ }
67
+
68
+
69
+ def get_language_emoji(language: str):
70
+ language_formatted = language.lower()
71
+ return (
72
+ languages_emojis[language_formatted]
73
+ if language_formatted in languages_emojis
74
+ else language
75
+ )
76
+
77
+
78
+ translation_table = {
79
+ "ā": "a",
80
+ "ă": "a",
81
+ "ą": "a",
82
+ "ć": "c",
83
+ "č": "c",
84
+ "ç": "c",
85
+ "ĉ": "c",
86
+ "ċ": "c",
87
+ "ď": "d",
88
+ "đ": "d",
89
+ "è": "e",
90
+ "é": "e",
91
+ "ê": "e",
92
+ "ë": "e",
93
+ "ē": "e",
94
+ "ĕ": "e",
95
+ "ę": "e",
96
+ "ě": "e",
97
+ "ĝ": "g",
98
+ "ğ": "g",
99
+ "ġ": "g",
100
+ "ģ": "g",
101
+ "ĥ": "h",
102
+ "î": "i",
103
+ "ï": "i",
104
+ "ì": "i",
105
+ "í": "i",
106
+ "ī": "i",
107
+ "ĩ": "i",
108
+ "ĭ": "i",
109
+ "ı": "i",
110
+ "ĵ": "j",
111
+ "ķ": "k",
112
+ "ĺ": "l",
113
+ "ļ": "l",
114
+ "ł": "l",
115
+ "ń": "n",
116
+ "ň": "n",
117
+ "ñ": "n",
118
+ "ņ": "n",
119
+ "ʼn": "n",
120
+ "ó": "o",
121
+ "ô": "o",
122
+ "õ": "o",
123
+ "ö": "o",
124
+ "ø": "o",
125
+ "ō": "o",
126
+ "ő": "o",
127
+ "œ": "oe",
128
+ "ŕ": "r",
129
+ "ř": "r",
130
+ "ŗ": "r",
131
+ "š": "s",
132
+ "ş": "s",
133
+ "ś": "s",
134
+ "ș": "s",
135
+ "ß": "ss",
136
+ "ť": "t",
137
+ "ţ": "t",
138
+ "ū": "u",
139
+ "ŭ": "u",
140
+ "ũ": "u",
141
+ "û": "u",
142
+ "ü": "u",
143
+ "ù": "u",
144
+ "ú": "u",
145
+ "ų": "u",
146
+ "ű": "u",
147
+ "ŵ": "w",
148
+ "ý": "y",
149
+ "ÿ": "y",
150
+ "ŷ": "y",
151
+ "ž": "z",
152
+ "ż": "z",
153
+ "ź": "z",
154
+ "æ": "ae",
155
+ "ǎ": "a",
156
+ "ǧ": "g",
157
+ "ə": "e",
158
+ "ƒ": "f",
159
+ "ǐ": "i",
160
+ "ǒ": "o",
161
+ "ǔ": "u",
162
+ "ǚ": "u",
163
+ "ǜ": "u",
164
+ "ǹ": "n",
165
+ "ǻ": "a",
166
+ "ǽ": "ae",
167
+ "ǿ": "o",
168
+ }
169
+
170
+ translation_table = str.maketrans(translation_table)
171
+ info_hash_pattern = re.compile(r"\b([a-fA-F0-9]{40})\b")
172
+
173
+
174
+ def translate(title: str):
175
+ return title.translate(translation_table)
176
+
177
+
178
+ def is_video(title: str):
179
+ return title.endswith(
180
+ tuple(
181
+ [
182
+ ".mkv",
183
+ ".mp4",
184
+ ".avi",
185
+ ".mov",
186
+ ".flv",
187
+ ".wmv",
188
+ ".webm",
189
+ ".mpg",
190
+ ".mpeg",
191
+ ".m4v",
192
+ ".3gp",
193
+ ".3g2",
194
+ ".ogv",
195
+ ".ogg",
196
+ ".drc",
197
+ ".gif",
198
+ ".gifv",
199
+ ".mng",
200
+ ".avi",
201
+ ".mov",
202
+ ".qt",
203
+ ".wmv",
204
+ ".yuv",
205
+ ".rm",
206
+ ".rmvb",
207
+ ".asf",
208
+ ".amv",
209
+ ".m4p",
210
+ ".m4v",
211
+ ".mpg",
212
+ ".mp2",
213
+ ".mpeg",
214
+ ".mpe",
215
+ ".mpv",
216
+ ".mpg",
217
+ ".mpeg",
218
+ ".m2v",
219
+ ".m4v",
220
+ ".svi",
221
+ ".3gp",
222
+ ".3g2",
223
+ ".mxf",
224
+ ".roq",
225
+ ".nsv",
226
+ ".flv",
227
+ ".f4v",
228
+ ".f4p",
229
+ ".f4a",
230
+ ".f4b",
231
+ ]
232
+ )
233
+ )
234
+
235
+
236
+ def bytes_to_size(bytes: int):
237
+ sizes = ["Bytes", "KB", "MB", "GB", "TB"]
238
+ if bytes == 0:
239
+ return "0 Byte"
240
+
241
+ i = 0
242
+ while bytes >= 1024 and i < len(sizes) - 1:
243
+ bytes /= 1024
244
+ i += 1
245
+
246
+ return f"{round(bytes, 2)} {sizes[i]}"
247
+
248
+
249
+ def config_check(b64config: str):
250
+ try:
251
+ config = json.loads(base64.b64decode(b64config).decode())
252
+ validated_config = ConfigModel(**config)
253
+ return validated_config.model_dump()
254
+ except:
255
+ return False
256
+
257
+
258
+ def get_debrid_extension(debridService: str):
259
+ debrid_extension = None
260
+ if debridService == "realdebrid":
261
+ debrid_extension = "RD"
262
+ elif debridService == "alldebrid":
263
+ debrid_extension = "AD"
264
+ elif debridService == "premiumize":
265
+ debrid_extension = "PM"
266
+ elif debridService == "torbox":
267
+ debrid_extension = "TB"
268
+ elif debridService == "debridlink":
269
+ debrid_extension = "DL"
270
+
271
+ return debrid_extension
272
+
273
+
274
+ async def get_indexer_manager(
275
+ session: aiohttp.ClientSession,
276
+ indexer_manager_type: str,
277
+ indexers: list,
278
+ query: str,
279
+ ):
280
+ results = []
281
+ try:
282
+ indexers = [indexer.replace("_", " ") for indexer in indexers]
283
+
284
+ if indexer_manager_type == "jackett":
285
+
286
+ async def fetch_jackett_results(
287
+ session: aiohttp.ClientSession, indexer: str, query: str
288
+ ):
289
+ try:
290
+ async with session.get(
291
+ f"{settings.INDEXER_MANAGER_URL}/api/v2.0/indexers/all/results?apikey={settings.INDEXER_MANAGER_API_KEY}&Query={query}&Tracker[]={indexer}",
292
+ timeout=aiohttp.ClientTimeout(
293
+ total=settings.INDEXER_MANAGER_TIMEOUT
294
+ ),
295
+ ) as response:
296
+ response_json = await response.json()
297
+ return response_json.get("Results", [])
298
+ except Exception as e:
299
+ logger.warning(
300
+ f"Exception while fetching Jackett results for indexer {indexer}: {e}"
301
+ )
302
+ return []
303
+
304
+ tasks = [
305
+ fetch_jackett_results(session, indexer, query) for indexer in indexers
306
+ ]
307
+ all_results = await asyncio.gather(*tasks)
308
+
309
+ for result_set in all_results:
310
+ results.extend(result_set)
311
+
312
+ elif indexer_manager_type == "prowlarr":
313
+ get_indexers = await session.get(
314
+ f"{settings.INDEXER_MANAGER_URL}/api/v1/indexer",
315
+ headers={"X-Api-Key": settings.INDEXER_MANAGER_API_KEY},
316
+ )
317
+ get_indexers = await get_indexers.json()
318
+
319
+ indexers_id = []
320
+ for indexer in get_indexers:
321
+ if (
322
+ indexer["name"].lower() in indexers
323
+ or indexer["definitionName"].lower() in indexers
324
+ ):
325
+ indexers_id.append(indexer["id"])
326
+
327
+ response = await session.get(
328
+ f"{settings.INDEXER_MANAGER_URL}/api/v1/search?query={query}&indexerIds={'&indexerIds='.join(str(indexer_id) for indexer_id in indexers_id)}&type=search",
329
+ headers={"X-Api-Key": settings.INDEXER_MANAGER_API_KEY},
330
+ )
331
+ response = await response.json()
332
+
333
+ for result in response:
334
+ result["InfoHash"] = (
335
+ result["infoHash"] if "infoHash" in result else None
336
+ )
337
+ result["Title"] = result["title"]
338
+ result["Size"] = result["size"]
339
+ result["Link"] = (
340
+ result["downloadUrl"] if "downloadUrl" in result else None
341
+ )
342
+ result["Tracker"] = result["indexer"]
343
+
344
+ results.append(result)
345
+ except Exception as e:
346
+ logger.warning(
347
+ f"Exception while getting {indexer_manager_type} results for {query} with {indexers}: {e}"
348
+ )
349
+ pass
350
+
351
+ return results
352
+
353
+
354
+ async def get_zilean(
355
+ session: aiohttp.ClientSession, name: str, log_name: str, season: int, episode: int
356
+ ):
357
+ results = []
358
+ try:
359
+ show = f"&season={season}&episode={episode}"
360
+ get_dmm = await session.get(
361
+ f"{settings.ZILEAN_URL}/dmm/filtered?query={name}{show if season else ''}"
362
+ )
363
+ get_dmm = await get_dmm.json()
364
+
365
+ if isinstance(get_dmm, list):
366
+ take_first = get_dmm[: settings.ZILEAN_TAKE_FIRST]
367
+ for result in take_first:
368
+ object = {
369
+ "Title": result["raw_title"],
370
+ "InfoHash": result["info_hash"],
371
+ "Size": result["size"],
372
+ "Tracker": "DMM",
373
+ }
374
+
375
+ results.append(object)
376
+
377
+ logger.info(f"{len(results)} torrents found for {log_name} with Zilean")
378
+ except Exception as e:
379
+ logger.warning(
380
+ f"Exception while getting torrents for {log_name} with Zilean: {e}"
381
+ )
382
+ pass
383
+
384
+ return results
385
+
386
+
387
+ async def get_torrentio(log_name: str, type: str, full_id: str):
388
+ results = []
389
+ try:
390
+ try:
391
+ get_torrentio = requests.get(
392
+ f"https://torrentio.strem.fun/stream/{type}/{full_id}.json"
393
+ ).json()
394
+ except:
395
+ get_torrentio = requests.get(
396
+ f"https://torrentio.strem.fun/stream/{type}/{full_id}.json",
397
+ proxies={
398
+ "http": settings.DEBRID_PROXY_URL,
399
+ "https": settings.DEBRID_PROXY_URL,
400
+ },
401
+ ).json()
402
+
403
+ for torrent in get_torrentio["streams"]:
404
+ title = torrent["title"]
405
+ title_full = title.split("\n👤")[0]
406
+ tracker = title.split("⚙️ ")[1].split("\n")[0]
407
+
408
+ results.append(
409
+ {
410
+ "Title": title_full,
411
+ "InfoHash": torrent["infoHash"],
412
+ "Size": None,
413
+ "Tracker": f"Torrentio|{tracker}",
414
+ }
415
+ )
416
+
417
+ logger.info(f"{len(results)} torrents found for {log_name} with Torrentio")
418
+ except Exception as e:
419
+ logger.warning(
420
+ f"Exception while getting torrents for {log_name} with Torrentio, your IP is most likely blacklisted (you should try proxying Comet): {e}"
421
+ )
422
+ pass
423
+
424
+ return results
425
+
426
+
427
+ async def filter(torrents: list, name: str, year: int):
428
+ results = []
429
+ for torrent in torrents:
430
+ index = torrent[0]
431
+ title = torrent[1]
432
+
433
+ if "\n" in title: # Torrentio title parsing
434
+ title = title.split("\n")[1]
435
+
436
+ parsed = parse(title)
437
+ if not title_match(name, parsed.parsed_title):
438
+ results.append((index, False))
439
+ continue
440
+
441
+ if year and parsed.year and year != parsed.year:
442
+ results.append((index, False))
443
+ continue
444
+
445
+ results.append((index, True))
446
+
447
+ return results
448
+
449
+
450
+ async def get_torrent_hash(session: aiohttp.ClientSession, torrent: tuple):
451
+ index = torrent[0]
452
+ torrent = torrent[1]
453
+ if "InfoHash" in torrent and torrent["InfoHash"] is not None:
454
+ return (index, torrent["InfoHash"].lower())
455
+
456
+ url = torrent["Link"]
457
+
458
+ try:
459
+ timeout = aiohttp.ClientTimeout(total=settings.GET_TORRENT_TIMEOUT)
460
+ response = await session.get(url, allow_redirects=False, timeout=timeout)
461
+ if response.status == 200:
462
+ torrent_data = await response.read()
463
+ torrent_dict = bencodepy.decode(torrent_data)
464
+ info = bencodepy.encode(torrent_dict[b"info"])
465
+ hash = hashlib.sha1(info).hexdigest()
466
+ else:
467
+ location = response.headers.get("Location", "")
468
+ if not location:
469
+ return (index, None)
470
+
471
+ match = info_hash_pattern.search(location)
472
+ if not match:
473
+ return (index, None)
474
+
475
+ hash = match.group(1).upper()
476
+
477
+ return (index, hash.lower())
478
+ except Exception as e:
479
+ logger.warning(
480
+ f"Exception while getting torrent info hash for {torrent['indexer'] if 'indexer' in torrent else (torrent['Tracker'] if 'Tracker' in torrent else '')}|{url}: {e}"
481
+ )
482
+
483
+ return (index, None)
484
+
485
+
486
+ def get_balanced_hashes(hashes: dict, config: dict):
487
+ max_results = config["maxResults"]
488
+
489
+ max_size = config["maxSize"]
490
+ config_resolutions = [resolution.lower() for resolution in config["resolutions"]]
491
+ include_all_resolutions = "all" in config_resolutions
492
+
493
+ languages = [language.lower() for language in config["languages"]]
494
+ include_all_languages = "all" in languages
495
+ if not include_all_languages:
496
+ config_languages = [
497
+ code
498
+ for code, name in PTT.parse.LANGUAGES_TRANSLATION_TABLE.items()
499
+ if name.lower() in languages
500
+ ]
501
+
502
+ hashes_by_resolution = {}
503
+ for hash, hash_data in hashes.items():
504
+ hash_info = hash_data["data"]
505
+
506
+ if max_size != 0 and hash_info["size"] > max_size:
507
+ continue
508
+
509
+ if (
510
+ not include_all_languages
511
+ and not any(lang in hash_info["languages"] for lang in config_languages)
512
+ and ("multi" not in languages if hash_info["dubbed"] else True)
513
+ ):
514
+ continue
515
+
516
+ resolution = hash_info["resolution"]
517
+ if not include_all_resolutions and resolution not in config_resolutions:
518
+ continue
519
+
520
+ if resolution not in hashes_by_resolution:
521
+ hashes_by_resolution[resolution] = []
522
+ hashes_by_resolution[resolution].append(hash)
523
+
524
+ total_resolutions = len(hashes_by_resolution)
525
+ if max_results == 0 or total_resolutions == 0:
526
+ return hashes_by_resolution
527
+
528
+ hashes_per_resolution = max_results // total_resolutions
529
+ extra_hashes = max_results % total_resolutions
530
+
531
+ balanced_hashes = {}
532
+ for resolution, hash_list in hashes_by_resolution.items():
533
+ selected_count = hashes_per_resolution + (1 if extra_hashes > 0 else 0)
534
+ balanced_hashes[resolution] = hash_list[:selected_count]
535
+ if extra_hashes > 0:
536
+ extra_hashes -= 1
537
+
538
+ selected_total = sum(len(hashes) for hashes in balanced_hashes.values())
539
+ if selected_total < max_results:
540
+ missing_hashes = max_results - selected_total
541
+ for resolution, hash_list in hashes_by_resolution.items():
542
+ if missing_hashes <= 0:
543
+ break
544
+ current_count = len(balanced_hashes[resolution])
545
+ available_hashes = hash_list[current_count : current_count + missing_hashes]
546
+ balanced_hashes[resolution].extend(available_hashes)
547
+ missing_hashes -= len(available_hashes)
548
+
549
+ return balanced_hashes
550
+
551
+
552
+ def format_metadata(data: dict):
553
+ extras = []
554
+ if data["quality"]:
555
+ extras.append(data["quality"])
556
+ if data["hdr"]:
557
+ extras.extend(data["hdr"])
558
+ if data["codec"]:
559
+ extras.append(data["codec"])
560
+ if data["audio"]:
561
+ extras.extend(data["audio"])
562
+ if data["channels"]:
563
+ extras.extend(data["channels"])
564
+ if data["bit_depth"]:
565
+ extras.append(data["bit_depth"])
566
+ if data["network"]:
567
+ extras.append(data["network"])
568
+ if data["group"]:
569
+ extras.append(data["group"])
570
+
571
+ return "|".join(extras)
572
+
573
+
574
+ def format_title(data: dict, config: dict):
575
+ title = ""
576
+ if "All" in config["resultFormat"] or "Title" in config["resultFormat"]:
577
+ title += f"{data['title']}\n"
578
+
579
+ if "All" in config["resultFormat"] or "Metadata" in config["resultFormat"]:
580
+ metadata = format_metadata(data)
581
+ if metadata != "":
582
+ title += f"💿 {metadata}\n"
583
+
584
+ if "All" in config["resultFormat"] or "Size" in config["resultFormat"]:
585
+ title += f"💾 {bytes_to_size(data['size'])} "
586
+
587
+ if "All" in config["resultFormat"] or "Tracker" in config["resultFormat"]:
588
+ title += f"🔎 {data['tracker'] if 'tracker' in data else '?'}"
589
+
590
+ if "All" in config["resultFormat"] or "Languages" in config["resultFormat"]:
591
+ languages = data["languages"]
592
+ if data["dubbed"]:
593
+ languages.insert(0, "multi")
594
+ formatted_languages = (
595
+ "/".join(get_language_emoji(language) for language in languages)
596
+ if languages
597
+ else None
598
+ )
599
+ languages_str = "\n" + formatted_languages if formatted_languages else ""
600
+ title += f"{languages_str}"
601
+
602
+ if title == "":
603
+ # Without this, Streamio shows SD as the result, which is confusing
604
+ title = "Empty result format configuration"
605
+
606
+ return title
607
+
608
+
609
+ def get_client_ip(request: Request):
610
+ return (
611
+ request.headers["cf-connecting-ip"]
612
+ if "cf-connecting-ip" in request.headers
613
+ else request.client.host
614
+ )
comet/utils/logger.py ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+
3
+ from loguru import logger
4
+
5
+
6
+ def setupLogger(level: str):
7
+ logger.level("COMET", no=50, icon="🌠", color="<fg #7871d6>")
8
+ logger.level("API", no=40, icon="👾", color="<fg #7871d6>")
9
+
10
+ logger.level("INFO", icon="📰", color="<fg #FC5F39>")
11
+ logger.level("DEBUG", icon="🕸️", color="<fg #DC5F00>")
12
+ logger.level("WARNING", icon="⚠️", color="<fg #DC5F00>")
13
+
14
+ log_format = (
15
+ "<white>{time:YYYY-MM-DD}</white> <magenta>{time:HH:mm:ss}</magenta> | "
16
+ "<level>{level.icon}</level> <level>{level}</level> | "
17
+ "<cyan>{module}</cyan>.<cyan>{function}</cyan> - <level>{message}</level>"
18
+ )
19
+
20
+ logger.configure(
21
+ handlers=[
22
+ {
23
+ "sink": sys.stderr,
24
+ "level": level,
25
+ "format": log_format,
26
+ "backtrace": False,
27
+ "diagnose": False,
28
+ "enqueue": True,
29
+ }
30
+ ]
31
+ )
32
+
33
+
34
+ setupLogger("DEBUG")
comet/utils/models.py ADDED
@@ -0,0 +1,123 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import random
3
+ import string
4
+
5
+ from typing import List, Optional
6
+ from databases import Database
7
+ from pydantic import BaseModel, field_validator
8
+ from pydantic_settings import BaseSettings, SettingsConfigDict
9
+ from RTN import RTN, BestRanking, SettingsModel
10
+
11
+
12
+ class AppSettings(BaseSettings):
13
+ model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8")
14
+
15
+ ADDON_ID: Optional[str] = "stremio.comet.fast"
16
+ ADDON_NAME: Optional[str] = "Comet"
17
+ FASTAPI_HOST: Optional[str] = "0.0.0.0"
18
+ FASTAPI_PORT: Optional[int] = 8000
19
+ FASTAPI_WORKERS: Optional[int] = 2 * (os.cpu_count() or 1)
20
+ DASHBOARD_ADMIN_PASSWORD: Optional[str] = None
21
+ DATABASE_TYPE: Optional[str] = "sqlite"
22
+ DATABASE_URL: Optional[str] = "username:password@hostname:port"
23
+ DATABASE_PATH: Optional[str] = "data/comet.db"
24
+ CACHE_TTL: Optional[int] = 86400
25
+ DEBRID_PROXY_URL: Optional[str] = None
26
+ INDEXER_MANAGER_TYPE: Optional[str] = "jackett"
27
+ INDEXER_MANAGER_URL: Optional[str] = "http://127.0.0.1:9117"
28
+ INDEXER_MANAGER_API_KEY: Optional[str] = None
29
+ INDEXER_MANAGER_TIMEOUT: Optional[int] = 30
30
+ INDEXER_MANAGER_INDEXERS: List[str] = ["EXAMPLE1_CHANGETHIS", "EXAMPLE2_CHANGETHIS"]
31
+ GET_TORRENT_TIMEOUT: Optional[int] = 5
32
+ ZILEAN_URL: Optional[str] = None
33
+ ZILEAN_TAKE_FIRST: Optional[int] = 500
34
+ SCRAPE_TORRENTIO: Optional[bool] = False
35
+ CUSTOM_HEADER_HTML: Optional[str] = None
36
+ PROXY_DEBRID_STREAM: Optional[bool] = False
37
+ PROXY_DEBRID_STREAM_PASSWORD: Optional[str] = None
38
+ PROXY_DEBRID_STREAM_MAX_CONNECTIONS: Optional[int] = 100
39
+ PROXY_DEBRID_STREAM_DEBRID_DEFAULT_SERVICE: Optional[str] = "realdebrid"
40
+ PROXY_DEBRID_STREAM_DEBRID_DEFAULT_APIKEY: Optional[str] = None
41
+ TITLE_MATCH_CHECK: Optional[bool] = True
42
+
43
+ @field_validator("DASHBOARD_ADMIN_PASSWORD")
44
+ def set_dashboard_admin_password(cls, v, values):
45
+ if v is None:
46
+ return "".join(random.choices(string.ascii_letters + string.digits, k=16))
47
+ return v
48
+
49
+ @field_validator("INDEXER_MANAGER_TYPE")
50
+ def set_indexer_manager_type(cls, v, values):
51
+ if v == "None":
52
+ return None
53
+ return v
54
+
55
+ @field_validator("PROXY_DEBRID_STREAM_PASSWORD")
56
+ def set_debrid_stream_proxy_password(cls, v, values):
57
+ if v is None:
58
+ return "".join(random.choices(string.ascii_letters + string.digits, k=16))
59
+ return v
60
+
61
+
62
+ settings = AppSettings()
63
+
64
+
65
+ class ConfigModel(BaseModel):
66
+ indexers: List[str]
67
+ languages: Optional[List[str]] = ["All"]
68
+ resolutions: Optional[List[str]] = ["All"]
69
+ resultFormat: Optional[List[str]] = ["All"]
70
+ maxResults: Optional[int] = 0
71
+ maxSize: Optional[float] = 0
72
+ debridService: str
73
+ debridApiKey: str
74
+ debridStreamProxyPassword: Optional[str] = ""
75
+
76
+ @field_validator("indexers")
77
+ def check_indexers(cls, v, values):
78
+ settings.INDEXER_MANAGER_INDEXERS = [
79
+ indexer.replace(" ", "_").lower()
80
+ for indexer in settings.INDEXER_MANAGER_INDEXERS
81
+ ] # to equal webui
82
+ valid_indexers = [
83
+ indexer for indexer in v if indexer in settings.INDEXER_MANAGER_INDEXERS
84
+ ]
85
+ # if not valid_indexers: # For only Zilean mode
86
+ # raise ValueError(
87
+ # f"At least one indexer must be from {settings.INDEXER_MANAGER_INDEXERS}"
88
+ # )
89
+ return valid_indexers
90
+
91
+ @field_validator("maxResults")
92
+ def check_max_results(cls, v):
93
+ if v < 0:
94
+ v = 0
95
+ return v
96
+
97
+ @field_validator("maxSize")
98
+ def check_max_size(cls, v):
99
+ if v < 0:
100
+ v = 0
101
+ return v
102
+
103
+ @field_validator("debridService")
104
+ def check_debrid_service(cls, v):
105
+ if v not in ["realdebrid", "alldebrid", "premiumize", "torbox", "debridlink"]:
106
+ raise ValueError("Invalid debridService")
107
+ return v
108
+
109
+
110
+ rtn_settings = SettingsModel()
111
+ rtn_ranking = BestRanking()
112
+
113
+ # For use anywhere
114
+ rtn = RTN(settings=rtn_settings, ranking_model=rtn_ranking)
115
+
116
+ database_url = (
117
+ settings.DATABASE_PATH
118
+ if settings.DATABASE_TYPE == "sqlite"
119
+ else settings.DATABASE_URL
120
+ )
121
+ database = Database(
122
+ f"{'sqlite' if settings.DATABASE_TYPE == 'sqlite' else 'postgresql+asyncpg'}://{'/' if settings.DATABASE_TYPE == 'sqlite' else ''}{database_url}"
123
+ )
compose.yaml ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ volumes:
2
+ comet_data:
3
+
4
+ services:
5
+ comet:
6
+ container_name: comet
7
+ image: g0ldyy/comet
8
+ restart: unless-stopped
9
+ ports:
10
+ - "8000:8000"
11
+ env_file:
12
+ - .env
13
+ volumes:
14
+ - comet_data:/data
pyproject.toml ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [tool.poetry]
2
+ name = "comet"
3
+ version = "0.1.0"
4
+ description = "Stremio's fastest torrent/debrid search add-on."
5
+ authors = ["Goldy"]
6
+ license = "MIT"
7
+ readme = "README.md"
8
+ repository = "https://github.com/g0ldyy/comet"
9
+
10
+ [tool.poetry.dependencies]
11
+ python = "^3.11"
12
+ uvicorn = "*"
13
+ fastapi = "*"
14
+ aiohttp = "*"
15
+ asyncio = "*"
16
+ loguru = "*"
17
+ databases = "*"
18
+ pydantic-settings = "*"
19
+ bencode-py = "*"
20
+ httpx = "*"
21
+ curl-cffi = "*"
22
+ orjson = "*"
23
+ asyncpg = "*"
24
+ aiosqlite = "*"
25
+ jinja2 = "*"
26
+ rank-torrent-name = "*"
27
+ parsett = "*"
28
+
29
+
30
+ [tool.poetry.group.dev.dependencies]
31
+ isort = "*"
32
+ pyright = "*"
33
+ pytest = "*"
34
+
35
+ [build-system]
36
+ requires = ["poetry-core"]
37
+ build-backend = "poetry.core.masonry.api"