Upload 26 files
Browse files- .env-sample +27 -0
- .gitignore +167 -0
- LICENSE +21 -0
- comet/__init__.py +0 -0
- comet/api/__init__.py +0 -0
- comet/api/core.py +79 -0
- comet/api/stream.py +601 -0
- comet/assets/invalidconfig.mp4 +0 -0
- comet/assets/proxylimit.mp4 +0 -0
- comet/assets/uncached.mp4 +0 -0
- comet/debrid/__init__.py +0 -0
- comet/debrid/alldebrid.py +172 -0
- comet/debrid/debridlink.py +126 -0
- comet/debrid/manager.py +22 -0
- comet/debrid/premiumize.py +177 -0
- comet/debrid/realdebrid.py +192 -0
- comet/debrid/torbox.py +161 -0
- comet/main.py +165 -0
- comet/templates/index.html +794 -0
- comet/utils/__init__.py +0 -0
- comet/utils/db.py +34 -0
- comet/utils/general.py +614 -0
- comet/utils/logger.py +34 -0
- comet/utils/models.py +123 -0
- compose.yaml +14 -0
- pyproject.toml +37 -0
.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"
|