mirror of
https://github.com/g0ldyy/comet.git
synced 2026-01-12 01:16:12 +01:00
Merge rewrite into main by overwriting files
This commit is contained in:
+35
-31
@@ -1,31 +1,35 @@
|
|||||||
ADDON_ID=stremio.comet.fast # for Stremio
|
ADDON_ID=stremio.comet.fast # for Stremio
|
||||||
ADDON_NAME=Comet # for Stremio
|
ADDON_NAME=Comet # for Stremio
|
||||||
FASTAPI_HOST=0.0.0.0
|
FASTAPI_HOST=0.0.0.0
|
||||||
FASTAPI_PORT=8000
|
FASTAPI_PORT=8000
|
||||||
FASTAPI_WORKERS=1 # remove to destroy CPU -> max performances :)
|
FASTAPI_WORKERS=1
|
||||||
DASHBOARD_ADMIN_PASSWORD=CHANGE_ME # The password to access the dashboard with active connections and soon more...
|
USE_GUNICORN=True # will use uvicorn if False or if on Windows
|
||||||
DATABASE_TYPE=sqlite # or postgresql if you know what you're doing
|
DASHBOARD_ADMIN_PASSWORD=CHANGE_ME # The password to access the dashboard with active connections and soon more...
|
||||||
DATABASE_URL=username:password@hostname:port # to connect to PostgreSQL
|
DATABASE_TYPE=sqlite # or postgresql if you're making a Comet cluster
|
||||||
DATABASE_PATH=data/comet.db # only change it if you know what it is - folders in path must exist - ignored if PostgreSQL used
|
DATABASE_URL=username:password@hostname:port # to connect to PostgreSQL
|
||||||
CACHE_TTL=86400 # cache duration in seconds
|
DATABASE_PATH=data/comet.db # only change it if you know what it is - folders in path must exist - ignored if PostgreSQL used
|
||||||
DEBRID_PROXY_URL=http://127.0.0.1:1080 # https://github.com/cmj2002/warp-docker to bypass Debrid Services and Torrentio server IP blacklist
|
METADATA_CACHE_TTL=2592000 # metadata cache duration in seconds (30 days by default)
|
||||||
INDEXER_MANAGER_TYPE=None # jackett or prowlarr or None if you want to disable it completely and use Zilean or Torrentio
|
TORRENT_CACHE_TTL=1296000 # torrent cache duration in seconds (15 days by default)
|
||||||
INDEXER_MANAGER_URL=http://127.0.0.1:9117
|
DEBRID_CACHE_TTL=86400 # debrid availability cache duration in seconds (1 day by default)
|
||||||
INDEXER_MANAGER_API_KEY=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
|
DEBRID_PROXY_URL=http://127.0.0.1:1080 # https://github.com/cmj2002/warp-docker to bypass Debrid Services and Torrentio server IP blacklist
|
||||||
INDEXER_MANAGER_TIMEOUT=60 # maximum time to obtain search results from indexer manager in seconds
|
INDEXER_MANAGER_TYPE=none # jackett or prowlarr or none if you want to disable it completely and use Zilean or Torrentio
|
||||||
INDEXER_MANAGER_INDEXERS='["EXAMPLE1_CHANGETHIS", "EXAMPLE2_CHANGETHIS"]' # for jackett, get the names from https://github.com/Jackett/Jackett/tree/master/src/Jackett.Common/Definitions - for prowlarr you can write them like on the web dashboard
|
INDEXER_MANAGER_URL=http://127.0.0.1:9117
|
||||||
GET_TORRENT_TIMEOUT=5 # maximum time to obtain the torrent info hash in seconds
|
INDEXER_MANAGER_API_KEY=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
|
||||||
ZILEAN_URL=None # for DMM search - https://github.com/iPromKnight/zilean - ex: http://127.0.0.1:8181
|
INDEXER_MANAGER_TIMEOUT=60 # maximum time to obtain search results from indexer manager in seconds
|
||||||
ZILEAN_TAKE_FIRST=500 # only change it if you know what it is
|
INDEXER_MANAGER_INDEXERS='["EXAMPLE1_CHANGETHIS", "EXAMPLE2_CHANGETHIS"]' # for jackett, get the names from https://github.com/Jackett/Jackett/tree/master/src/Jackett.Common/Definitions - for prowlarr you can write them like on the web dashboard
|
||||||
SCRAPE_TORRENTIO=False # scrape Torrentio
|
GET_TORRENT_TIMEOUT=5 # maximum time to obtain the torrent info hash in seconds
|
||||||
SCRAPE_MEDIAFUSION=False # scrape MediaFusion - has better results for Indian content
|
DOWNLOAD_TORRENT_FILES=False # set to True to enable torrent file retrieval instead of using only magnet link info (infohash and sources, ensuring file index is included in results for Jackett and Prowlarr torrents)
|
||||||
MEDIAFUSION_URL=https://mediafusion.elfhosted.com # Allows you to scrape custom instances of MediaFusion
|
SCRAPE_ZILEAN=False # scrape Zilean/DMM
|
||||||
PROXY_DEBRID_STREAM=False # Proxy Debrid Streams (very useful to use your debrid service on multiple IPs at same time)
|
ZILEAN_URL=https://zilean.elfhosted.com # for DMM search - https://github.com/iPromKnight/zilean - ex: http://127.0.0.1:8181
|
||||||
PROXY_DEBRID_STREAM_PASSWORD=CHANGE_ME # Secret password to enter on configuration page to prevent people from abusing your debrid stream proxy
|
SCRAPE_TORRENTIO=False # scrape Torrentio
|
||||||
PROXY_DEBRID_STREAM_MAX_CONNECTIONS=-1 # IP-Based connection limit for the Debrid Stream Proxy (-1 = disabled)
|
TORRENTIO_URL=https://torrentio.strem.fun # or https://knightcrawler.elfhosted.com if you prefer to scrape the ElfHosted KnightCrawler instance
|
||||||
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
|
SCRAPE_MEDIAFUSION=False # scrape MediaFusion - has better results for Indian content
|
||||||
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
|
MEDIAFUSION_URL=https://mediafusion.elfhosted.com # Allows you to scrape custom instances of MediaFusion
|
||||||
TITLE_MATCH_CHECK=True # disable if you only use Torrentio / MediaFusion and are sure you're only scraping good titles, for example (keep it True if Zilean is enabled)
|
PROXY_DEBRID_STREAM=False # Proxy Debrid Streams (very useful to use your debrid service on multiple IPs at same time)
|
||||||
REMOVE_ADULT_CONTENT=False # detect and remove adult content
|
PROXY_DEBRID_STREAM_PASSWORD=CHANGE_ME # Secret password to enter on configuration page to prevent people from abusing your debrid stream proxy
|
||||||
STREMTHRU_DEFAULT_URL=None # if you want your users to use StremThru without having to specify it
|
PROXY_DEBRID_STREAM_MAX_CONNECTIONS=-1 # IP-Based connection limit for the Debrid Stream Proxy (-1 = disabled)
|
||||||
CUSTOM_HEADER_HTML=None # only set it if you know what it is
|
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
|
||||||
|
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
|
||||||
|
REMOVE_ADULT_CONTENT=False # detect and remove adult content
|
||||||
|
CUSTOM_HEADER_HTML=None # only set it if you know what it is
|
||||||
|
STREMTHRU_URL=https://stremthru.13377001.xyz # StremThru acts as a proxy between Comet and debrid services to support them all, so you must have it
|
||||||
@@ -4,8 +4,7 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
# release:
|
- rewrite
|
||||||
# types: [created]
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -42,6 +41,19 @@ jobs:
|
|||||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
|
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
|
||||||
|
|
||||||
|
- name: Get branch name
|
||||||
|
id: branch-name
|
||||||
|
run: echo "branch=${GITHUB_REF#refs/heads/}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Set Docker tags
|
||||||
|
id: docker_tags
|
||||||
|
run: |
|
||||||
|
if [ "${{ steps.branch-name.outputs.branch }}" = "main" ]; then
|
||||||
|
echo "tags=ghcr.io/g0ldyy/comet:latest,docker.io/g0ldyy/comet:latest" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "tags=ghcr.io/g0ldyy/comet:${{ steps.branch-name.outputs.branch }},docker.io/g0ldyy/comet:${{ steps.branch-name.outputs.branch }}" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Build and push Docker image
|
- name: Build and push Docker image
|
||||||
uses: docker/build-push-action@v5.3.0
|
uses: docker/build-push-action@v5.3.0
|
||||||
with:
|
with:
|
||||||
@@ -51,6 +63,4 @@ jobs:
|
|||||||
push: true
|
push: true
|
||||||
cache-from: type=gha
|
cache-from: type=gha
|
||||||
cache-to: type=gha,mode=max
|
cache-to: type=gha,mode=max
|
||||||
tags: |
|
tags: ${{ steps.docker_tags.outputs.tags }}
|
||||||
ghcr.io/g0ldyy/comet:latest
|
|
||||||
docker.io/g0ldyy/comet:latest
|
|
||||||
+177
-167
@@ -1,167 +1,177 @@
|
|||||||
# Project Based
|
# Project Based
|
||||||
*.db
|
*.db
|
||||||
|
|
||||||
# Byte-compiled / optimized / DLL files
|
# Byte-compiled / optimized / DLL files
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
*$py.class
|
*$py.class
|
||||||
logs/
|
|
||||||
.vscode/
|
# C extensions
|
||||||
|
*.so
|
||||||
# C extensions
|
|
||||||
*.so
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
# Distribution / packaging
|
build/
|
||||||
.Python
|
develop-eggs/
|
||||||
build/
|
dist/
|
||||||
develop-eggs/
|
downloads/
|
||||||
dist/
|
eggs/
|
||||||
downloads/
|
.eggs/
|
||||||
eggs/
|
lib/
|
||||||
.eggs/
|
lib64/
|
||||||
lib/
|
parts/
|
||||||
lib64/
|
sdist/
|
||||||
parts/
|
var/
|
||||||
sdist/
|
wheels/
|
||||||
var/
|
share/python-wheels/
|
||||||
wheels/
|
*.egg-info/
|
||||||
share/python-wheels/
|
.installed.cfg
|
||||||
*.egg-info/
|
*.egg
|
||||||
.installed.cfg
|
MANIFEST
|
||||||
*.egg
|
|
||||||
MANIFEST
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
# PyInstaller
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
# Usually these files are written by a python script from a template
|
*.manifest
|
||||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
*.spec
|
||||||
*.manifest
|
|
||||||
*.spec
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
# Installer logs
|
pip-delete-this-directory.txt
|
||||||
pip-log.txt
|
|
||||||
pip-delete-this-directory.txt
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
# Unit test / coverage reports
|
.tox/
|
||||||
htmlcov/
|
.nox/
|
||||||
.tox/
|
.coverage
|
||||||
.nox/
|
.coverage.*
|
||||||
.coverage
|
.cache
|
||||||
.coverage.*
|
nosetests.xml
|
||||||
.cache
|
coverage.xml
|
||||||
nosetests.xml
|
*.cover
|
||||||
coverage.xml
|
*.py,cover
|
||||||
*.cover
|
.hypothesis/
|
||||||
*.py,cover
|
.pytest_cache/
|
||||||
.hypothesis/
|
cover/
|
||||||
.pytest_cache/
|
|
||||||
cover/
|
# Translations
|
||||||
|
*.mo
|
||||||
# Translations
|
*.pot
|
||||||
*.mo
|
|
||||||
*.pot
|
# Django stuff:
|
||||||
|
*.log
|
||||||
# Django stuff:
|
local_settings.py
|
||||||
*.log
|
db.sqlite3
|
||||||
local_settings.py
|
db.sqlite3-journal
|
||||||
db.sqlite3
|
|
||||||
db.sqlite3-journal
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
# Flask stuff:
|
.webassets-cache
|
||||||
instance/
|
|
||||||
.webassets-cache
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
# Scrapy stuff:
|
|
||||||
.scrapy
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
# Sphinx documentation
|
|
||||||
docs/_build/
|
# PyBuilder
|
||||||
|
.pybuilder/
|
||||||
# PyBuilder
|
target/
|
||||||
.pybuilder/
|
|
||||||
target/
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
# Jupyter Notebook
|
|
||||||
.ipynb_checkpoints
|
# IPython
|
||||||
|
profile_default/
|
||||||
# IPython
|
ipython_config.py
|
||||||
profile_default/
|
|
||||||
ipython_config.py
|
# pyenv
|
||||||
|
# For a library or package, you might want to ignore these files since the code is
|
||||||
# pyenv
|
# intended to run in multiple environments; otherwise, check them in:
|
||||||
# For a library or package, you might want to ignore these files since the code is
|
# .python-version
|
||||||
# intended to run in multiple environments; otherwise, check them in:
|
|
||||||
# .python-version
|
# pipenv
|
||||||
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||||
# pipenv
|
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
# install all needed dependencies.
|
||||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
#Pipfile.lock
|
||||||
# install all needed dependencies.
|
|
||||||
#Pipfile.lock
|
# UV
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
||||||
# poetry
|
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
# commonly ignored for libraries.
|
||||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
uv.lock
|
||||||
# commonly ignored for libraries.
|
|
||||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
# poetry
|
||||||
poetry.lock
|
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||||
|
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||||
# pdm
|
# commonly ignored for libraries.
|
||||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||||
#pdm.lock
|
#poetry.lock
|
||||||
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
|
||||||
# in version control.
|
# pdm
|
||||||
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
|
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||||
.pdm.toml
|
#pdm.lock
|
||||||
.pdm-python
|
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||||
.pdm-build/
|
# in version control.
|
||||||
|
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
|
||||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
.pdm.toml
|
||||||
__pypackages__/
|
.pdm-python
|
||||||
|
.pdm-build/
|
||||||
# Celery stuff
|
|
||||||
celerybeat-schedule
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||||
celerybeat.pid
|
__pypackages__/
|
||||||
|
|
||||||
# SageMath parsed files
|
# Celery stuff
|
||||||
*.sage.py
|
celerybeat-schedule
|
||||||
|
celerybeat.pid
|
||||||
# Environments
|
|
||||||
.env
|
# SageMath parsed files
|
||||||
.venv
|
*.sage.py
|
||||||
env/
|
|
||||||
venv/
|
# Environments
|
||||||
ENV/
|
.env
|
||||||
env.bak/
|
.venv
|
||||||
venv.bak/
|
env/
|
||||||
|
venv/
|
||||||
# Spyder project settings
|
ENV/
|
||||||
.spyderproject
|
env.bak/
|
||||||
.spyproject
|
venv.bak/
|
||||||
|
|
||||||
# Rope project settings
|
# Spyder project settings
|
||||||
.ropeproject
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
# mkdocs documentation
|
|
||||||
/site
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
# mypy
|
|
||||||
.mypy_cache/
|
# mkdocs documentation
|
||||||
.dmypy.json
|
/site
|
||||||
dmypy.json
|
|
||||||
|
# mypy
|
||||||
# Pyre type checker
|
.mypy_cache/
|
||||||
.pyre/
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
# pytype static type analyzer
|
|
||||||
.pytype/
|
# Pyre type checker
|
||||||
|
.pyre/
|
||||||
# Cython debug symbols
|
|
||||||
cython_debug/
|
# pytype static type analyzer
|
||||||
|
.pytype/
|
||||||
# PyCharm
|
|
||||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
# Cython debug symbols
|
||||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
cython_debug/
|
||||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
||||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
# PyCharm
|
||||||
#.idea/
|
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||||
|
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||||
|
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||||
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
|
#.idea/
|
||||||
|
|
||||||
|
# Ruff stuff:
|
||||||
|
.ruff_cache/
|
||||||
|
|
||||||
|
# PyPI configuration file
|
||||||
|
.pypirc
|
||||||
@@ -1,29 +1,5 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## [1.53.0](https://github.com/g0ldyy/comet/compare/v1.52.0...v1.53.0) (2025-02-07)
|
|
||||||
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* **debrid/stremthru:** forward client ip ([982c398](https://github.com/g0ldyy/comet/commit/982c398774480badbb60d7214a91c3a62260fee6))
|
|
||||||
* **debrid/stremthru:** match behavior with mediafusion ([24611a2](https://github.com/g0ldyy/comet/commit/24611a2940dec82cd3cd09aab757cef06a9beb8a))
|
|
||||||
* **debrid/stremthru:** pass stremio video id for magnet cache check ([786a298](https://github.com/g0ldyy/comet/commit/786a2985ca3ef2463b65c684bc8cef6a86ce772f))
|
|
||||||
* **debrid/stremthru:** use default url only for specific services ([f0b029d](https://github.com/g0ldyy/comet/commit/f0b029d7933bc85a7ca794c3edea1afccc8d4f28))
|
|
||||||
* **debrid:** add support for stremthru ([64ba2b3](https://github.com/g0ldyy/comet/commit/64ba2b39fb0faab69e16e8755f024eb82c14f888))
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* **debrid/stremthru:** handle missing file index ([32ae49e](https://github.com/g0ldyy/comet/commit/32ae49ed1dd7ef0ab52ec8f6247250b3a8e888c7))
|
|
||||||
* direct torrent results ([dea8a90](https://github.com/g0ldyy/comet/commit/dea8a9041cb54245e2fdadd9d914c0285efebc56))
|
|
||||||
|
|
||||||
## [1.52.0](https://github.com/g0ldyy/comet/compare/v1.51.0...v1.52.0) (2025-01-03)
|
|
||||||
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* revert random manifest id ([f792529](https://github.com/g0ldyy/comet/commit/f7925298993e904a635041248079cf578d602818))
|
|
||||||
|
|
||||||
## [1.51.0](https://github.com/g0ldyy/comet/compare/v1.50.1...v1.51.0) (2024-11-28)
|
## [1.51.0](https://github.com/g0ldyy/comet/compare/v1.50.1...v1.51.0) (2024-11-28)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+16
-29
@@ -1,29 +1,16 @@
|
|||||||
FROM python:3.11-alpine
|
FROM ghcr.io/astral-sh/uv:python3.11-alpine
|
||||||
LABEL name="Comet" \
|
LABEL name="Comet" \
|
||||||
description="Stremio's fastest torrent/debrid search add-on." \
|
description="Stremio's fastest torrent/debrid search add-on." \
|
||||||
url="https://github.com/g0ldyy/comet"
|
url="https://github.com/g0ldyy/comet"
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
ARG DATABASE_PATH
|
ARG DATABASE_PATH
|
||||||
|
|
||||||
ENV PYTHONUNBUFFERED=1 \
|
COPY pyproject.toml .
|
||||||
PYTHONDONTWRITEBYTECODE=1 \
|
|
||||||
POETRY_NO_INTERACTION=1 \
|
RUN uv sync
|
||||||
POETRY_HOME="/usr/local" \
|
|
||||||
FORCE_COLOR=1 \
|
COPY . .
|
||||||
TERM=xterm-256color
|
|
||||||
|
ENTRYPOINT ["uv", "run", "python", "-m", "comet.main"]
|
||||||
# Fix python-alpine gcc
|
|
||||||
RUN apk add --no-cache \
|
|
||||||
gcc \
|
|
||||||
musl-dev \
|
|
||||||
libffi-dev \
|
|
||||||
make
|
|
||||||
|
|
||||||
RUN pip install poetry
|
|
||||||
COPY pyproject.toml .
|
|
||||||
RUN poetry install --no-cache --no-root --without dev
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
ENTRYPOINT ["poetry", "run", "python", "-m", "comet.main"]
|
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2024 Goldy
|
Copyright (c) 2024 Goldy
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
in the Software without restriction, including without limitation the rights
|
in the Software without restriction, including without limitation the rights
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
furnished to do so, subject to the following conditions:
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
The above copyright notice and this permission notice shall be included in all
|
||||||
copies or substantial portions of the Software.
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
SOFTWARE.
|
SOFTWARE.
|
||||||
|
|||||||
@@ -1,112 +1,85 @@
|
|||||||
<h1 align="center" id="title">☄️ Comet - <a href="https://discord.gg/rivenmedia">Discord</a></h1>
|
<h1 align="center" id="title">☄️ Comet - <a href="https://discord.gg/rivenmedia">Discord</a></h1>
|
||||||
<p align="center"><img src="https://socialify.git.ci/g0ldyy/comet/image?description=1&font=Inter&forks=1&language=1&name=1&owner=1&pattern=Solid&stargazers=1&theme=Dark" /></p>
|
<p align="center"><img src="https://socialify.git.ci/g0ldyy/comet/image?description=1&font=Inter&forks=1&language=1&name=1&owner=1&pattern=Solid&stargazers=1&theme=Dark" /></p>
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://ko-fi.com/E1E7ZVMAD">
|
<a href="https://ko-fi.com/E1E7ZVMAD">
|
||||||
<img src="https://ko-fi.com/img/githubbutton_sm.svg">
|
<img src="https://ko-fi.com/img/githubbutton_sm.svg">
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
# Features
|
# Features
|
||||||
- The only Stremio addon that can Proxy Debrid Streams to allow use of the Debrid Service on multiple IPs at the same time on the same account!
|
- The only Stremio addon that can Proxy Debrid Streams to allow use of the Debrid Service on multiple IPs at the same time on the same account!
|
||||||
- IP-Based Max Connection Limit and Dashboard for Debrid Stream Proxier
|
- IP-Based Max Connection Limit and Dashboard for Debrid Stream Proxier
|
||||||
- Jackett and Prowlarr support (change the `INDEXER_MANAGER_TYPE` environment variable to `jackett` or `prowlarr`)
|
- Jackett and Prowlarr support (change the `INDEXER_MANAGER_TYPE` environment variable to `jackett` or `prowlarr`)
|
||||||
- [Zilean](https://github.com/iPromKnight/zilean) ([DMM](https://hashlists.debridmediamanager.com/) Scraper) support for even more results
|
- [Zilean](https://github.com/iPromKnight/zilean) ([DMM](https://hashlists.debridmediamanager.com/) Scraper) support for even more results
|
||||||
- [Torrentio](https://torrentio.strem.fun/) Scraper
|
- [Torrentio](https://torrentio.strem.fun/) Scraper
|
||||||
- Caching system ft. SQLite / PostgreSQL
|
- Caching system ft. SQLite / PostgreSQL
|
||||||
- Smart Torrent Ranking powered by [RTN](https://github.com/dreulavelle/rank-torrent-name)
|
- Smart Torrent Ranking powered by [RTN](https://github.com/dreulavelle/rank-torrent-name)
|
||||||
- Proxy support to bypass debrid restrictions
|
- Proxy support to bypass debrid restrictions
|
||||||
- Real-Debrid, All-Debrid, Premiumize, TorBox and Debrid-Link supported
|
- Real-Debrid, All-Debrid, Premiumize, TorBox and Debrid-Link supported
|
||||||
- Direct Torrent supported (do not specify a Debrid API Key on the configuration page (webui) to activate it - it will use the cached results of other users using debrid service)
|
- Direct Torrent supported
|
||||||
- [Kitsu](https://kitsu.io/) support (anime)
|
- [Kitsu](https://kitsu.io/) support (anime)
|
||||||
- Adult Content Filter
|
- Adult Content Filter
|
||||||
- [StremThru](https://github.com/MunifTanjim/stremthru) support
|
- [StremThru](https://github.com/MunifTanjim/stremthru) support
|
||||||
|
|
||||||
# Installation
|
# Installation
|
||||||
To customize your Comet experience to suit your needs, please first take a look at all the [environment variables](https://github.com/g0ldyy/comet/blob/main/.env-sample)!
|
To customize your Comet experience to suit your needs, please first take a look at all the [environment variables](https://github.com/g0ldyy/comet/blob/main/.env-sample)!
|
||||||
## ElfHosted
|
|
||||||
A free, public Comet instance is available at https://comet.elfhosted.com
|
## ElfHosted
|
||||||
|
A free, public Comet instance is available at https://comet.elfhosted.com
|
||||||
[ElfHosted](https://elfhosted.com) is a geeky [open-source](https://elfhosted.com/open/) PaaS which provides all the "plumbing" (*hosting, security, updates, etc*) for your self-hosted apps.
|
|
||||||
|
[ElfHosted](https://elfhosted.com) is a geeky [open-source](https://elfhosted.com/open/) PaaS which provides all the "plumbing" (*hosting, security, updates, etc*) for your self-hosted apps.
|
||||||
ElfHosted offer "one-click" [private Comet instances](https://elfhosted.com/app/comet/), allowing you to customize your indexers, and enabling "Proxy Stream" mode, to permit streaming from multiple source IPs with the same RD token!
|
|
||||||
|
ElfHosted offer "one-click" [private Comet instances](https://elfhosted.com/app/comet/), allowing you to customize your indexers, and enabling "Proxy Stream" mode, to permit streaming from multiple source IPs with the same RD token!
|
||||||
> [!IMPORTANT]
|
|
||||||
> Comet is a top-tier app in the [ElfHosted app catalogue](https://elfhosted.com/apps/). 30% of your subscription goes to the app developer :heart:
|
> [!IMPORTANT]
|
||||||
|
> Comet is a top-tier app in the [ElfHosted app catalogue](https://elfhosted.com/apps/). 30% of your subscription goes to the app developer :heart:
|
||||||
(*[ElfHosted Discord](https://discord.elfhosted.com)*)
|
|
||||||
|
(*[ElfHosted Discord](https://discord.elfhosted.com)*)
|
||||||
## Self Hosted
|
|
||||||
### From source
|
## Self Hosted
|
||||||
- Clone the repository and enter the folder
|
### From source
|
||||||
```sh
|
- Clone the repository and enter the folder
|
||||||
git clone https://github.com/g0ldyy/comet
|
```sh
|
||||||
cd comet
|
git clone https://github.com/g0ldyy/comet
|
||||||
```
|
cd comet
|
||||||
- Install dependencies
|
```
|
||||||
```sh
|
- Install dependencies
|
||||||
pip install poetry
|
```sh
|
||||||
poetry install
|
pip install uv
|
||||||
````
|
uv sync
|
||||||
- Start Comet
|
````
|
||||||
```sh
|
- Start Comet
|
||||||
poetry run python -m comet.main
|
```sh
|
||||||
````
|
uv run python -m comet.main
|
||||||
|
````
|
||||||
### With Docker
|
|
||||||
- Simply run the Docker image after modifying the environment variables
|
### With Docker Compose
|
||||||
```sh
|
- Copy *deployment/docker-compose.yml* in a directory
|
||||||
docker run --name comet -p 8000:8000 -d \
|
- Copy *.env-sample* to *.env* in the same directory and keep only the variables you wish to modify, also remove all comments
|
||||||
-e FASTAPI_HOST=0.0.0.0 \
|
- Pull the latest version from docker hub
|
||||||
-e FASTAPI_PORT=8000 \
|
```sh
|
||||||
-e FASTAPI_WORKERS=1 \
|
docker compose pull
|
||||||
-e CACHE_TTL=86400 \
|
```
|
||||||
-e DEBRID_PROXY_URL=http://127.0.0.1:1080 \
|
- Run
|
||||||
-e INDEXER_MANAGER_TYPE=jackett \
|
```sh
|
||||||
-e INDEXER_MANAGER_URL=http://127.0.0.1:9117 \
|
docker compose up -d
|
||||||
-e INDEXER_MANAGER_API_KEY=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX \
|
```
|
||||||
-e INDEXER_MANAGER_INDEXERS='["EXAMPLE1_CHANGETHIS", "EXAMPLE2_CHANGETHIS"]' \
|
|
||||||
-e INDEXER_MANAGER_TIMEOUT=30 \
|
### Nginx Reverse Proxy
|
||||||
-e GET_TORRENT_TIMEOUT=5 \
|
If you want to serve Comet via a Nginx Reverse Proxy, here's the configuration you should use.
|
||||||
g0ldyy/comet
|
```
|
||||||
```
|
server {
|
||||||
- To update your container
|
server_name example.com;
|
||||||
|
|
||||||
- Find your existing container name
|
location / {
|
||||||
```sh
|
proxy_pass http://localhost:8000;
|
||||||
docker ps
|
proxy_set_header Host $host;
|
||||||
```
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
- Stop your existing container
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
```sh
|
}
|
||||||
docker stop <CONTAINER_ID>
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- Remove your existing container
|
## Web UI Showcase
|
||||||
```sh
|
<img src="https://i.imgur.com/SaD365F.png" />
|
||||||
docker rm <CONTAINER_ID>
|
|
||||||
```
|
|
||||||
|
|
||||||
- Pull the latest version from docker hub
|
|
||||||
```sh
|
|
||||||
docker pull g0ldyy/comet
|
|
||||||
```
|
|
||||||
|
|
||||||
- Finally, re-run the docker run command
|
|
||||||
|
|
||||||
### With Docker Compose
|
|
||||||
- Copy *compose.yaml* in a directory
|
|
||||||
- Copy *env-sample* to *.env* in the same directory
|
|
||||||
- Pull the latest version from docker hub
|
|
||||||
```sh
|
|
||||||
docker compose pull
|
|
||||||
```
|
|
||||||
- Run
|
|
||||||
```sh
|
|
||||||
docker compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
## Debrid IP Blacklist
|
|
||||||
To bypass Real-Debrid's (or AllDebrid) IP blacklist, start a cloudflare-warp container: https://github.com/cmj2002/warp-docker
|
|
||||||
|
|
||||||
## Web UI Showcase
|
|
||||||
<img src="https://i.imgur.com/SaD365F.png" />
|
|
||||||
|
|||||||
+56
-32
@@ -1,39 +1,32 @@
|
|||||||
import PTT
|
import random
|
||||||
import RTN
|
import string
|
||||||
|
import secrets
|
||||||
|
import orjson
|
||||||
|
|
||||||
from fastapi import APIRouter, Request
|
from fastapi import APIRouter, Request, Depends, HTTPException
|
||||||
from fastapi.responses import RedirectResponse
|
from fastapi.responses import RedirectResponse, Response
|
||||||
from fastapi.templating import Jinja2Templates
|
from fastapi.templating import Jinja2Templates
|
||||||
|
from fastapi.security import HTTPBasic, HTTPBasicCredentials
|
||||||
|
|
||||||
from comet.utils.models import settings
|
from comet.utils.models import settings, web_config, database
|
||||||
from comet.utils.general import config_check, get_debrid_extension
|
from comet.utils.general import config_check
|
||||||
|
from comet.debrid.manager import get_debrid_extension
|
||||||
|
|
||||||
templates = Jinja2Templates("comet/templates")
|
templates = Jinja2Templates("comet/templates")
|
||||||
main = APIRouter()
|
main = APIRouter()
|
||||||
|
security = HTTPBasic()
|
||||||
|
|
||||||
|
|
||||||
@main.get("/", status_code=200)
|
@main.get("/")
|
||||||
async def root():
|
async def root():
|
||||||
return RedirectResponse("/configure")
|
return RedirectResponse("/configure")
|
||||||
|
|
||||||
|
|
||||||
@main.get("/health", status_code=200)
|
@main.get("/health")
|
||||||
async def health():
|
async def health():
|
||||||
return {"status": "ok"}
|
return {"status": "ok"}
|
||||||
|
|
||||||
|
|
||||||
indexers = settings.INDEXER_MANAGER_INDEXERS
|
|
||||||
languages = [language for language in PTT.parse.LANGUAGES_TRANSLATION_TABLE.values()]
|
|
||||||
languages.insert(0, "Unknown")
|
|
||||||
languages.insert(1, "Multi")
|
|
||||||
web_config = {
|
|
||||||
"indexers": [indexer.replace(" ", "_").lower() for indexer in indexers],
|
|
||||||
"languages": languages,
|
|
||||||
"resolutions": [resolution.value for resolution in RTN.models.Resolution],
|
|
||||||
"resultFormat": ["Title", "Metadata", "Size", "Tracker", "Languages"],
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@main.get("/configure")
|
@main.get("/configure")
|
||||||
@main.get("/{b64config}/configure")
|
@main.get("/{b64config}/configure")
|
||||||
async def configure(request: Request):
|
async def configure(request: Request):
|
||||||
@@ -45,25 +38,16 @@ async def configure(request: Request):
|
|||||||
if settings.CUSTOM_HEADER_HTML
|
if settings.CUSTOM_HEADER_HTML
|
||||||
else "",
|
else "",
|
||||||
"webConfig": web_config,
|
"webConfig": web_config,
|
||||||
"indexerManager": settings.INDEXER_MANAGER_TYPE,
|
|
||||||
"proxyDebridStream": settings.PROXY_DEBRID_STREAM,
|
"proxyDebridStream": settings.PROXY_DEBRID_STREAM,
|
||||||
"stremthruDefaultUrl": settings.STREMTHRU_DEFAULT_URL or "",
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@main.get("/manifest.json")
|
@main.get("/manifest.json")
|
||||||
@main.get("/{b64config}/manifest.json")
|
@main.get("/{b64config}/manifest.json")
|
||||||
async def manifest(b64config: str = None):
|
async def manifest(request: Request, b64config: str = None):
|
||||||
config = config_check(b64config)
|
base_manifest = {
|
||||||
if not config:
|
"id": f"{settings.ADDON_ID}.{''.join(random.choice(string.ascii_letters) for _ in range(4))}",
|
||||||
config = {"debridService": None}
|
|
||||||
|
|
||||||
debrid_extension = get_debrid_extension(config["debridService"], config["debridApiKey"])
|
|
||||||
|
|
||||||
return {
|
|
||||||
"id": settings.ADDON_ID,
|
|
||||||
"name": f"{settings.ADDON_NAME}{(' | ' + debrid_extension) if debrid_extension is not None else ''}",
|
|
||||||
"description": "Stremio's fastest torrent/debrid search add-on.",
|
"description": "Stremio's fastest torrent/debrid search add-on.",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"catalogs": [],
|
"catalogs": [],
|
||||||
@@ -79,3 +63,43 @@ async def manifest(b64config: str = None):
|
|||||||
"background": "https://i.imgur.com/WwnXB3k.jpeg",
|
"background": "https://i.imgur.com/WwnXB3k.jpeg",
|
||||||
"behaviorHints": {"configurable": True, "configurationRequired": False},
|
"behaviorHints": {"configurable": True, "configurationRequired": False},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
config = config_check(b64config)
|
||||||
|
if not config:
|
||||||
|
base_manifest["name"] = "❌ | Comet"
|
||||||
|
base_manifest["description"] = f"⚠️ OBSOLETE CONFIGURATION, PLEASE RE-CONFIGURE ON {request.url.scheme}://{request.url.netloc} ⚠️"
|
||||||
|
return base_manifest
|
||||||
|
|
||||||
|
debrid_extension = get_debrid_extension(config["debridService"])
|
||||||
|
base_manifest["name"] = f"{settings.ADDON_NAME}{(' | ' + debrid_extension) if debrid_extension is not None else ''}"
|
||||||
|
|
||||||
|
return base_manifest
|
||||||
|
|
||||||
|
|
||||||
|
class CustomORJSONResponse(Response):
|
||||||
|
media_type = "application/json"
|
||||||
|
|
||||||
|
def render(self, content) -> bytes:
|
||||||
|
assert orjson is not None, "orjson must be installed"
|
||||||
|
return orjson.dumps(content, option=orjson.OPT_INDENT_2)
|
||||||
|
|
||||||
|
|
||||||
|
def verify_dashboard_auth(credentials: HTTPBasicCredentials = Depends(security)):
|
||||||
|
is_correct = secrets.compare_digest(
|
||||||
|
credentials.password, settings.DASHBOARD_ADMIN_PASSWORD
|
||||||
|
)
|
||||||
|
|
||||||
|
if not is_correct:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=401,
|
||||||
|
detail="Incorrect password",
|
||||||
|
headers={"WWW-Authenticate": "Basic"},
|
||||||
|
)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@main.get("/dashboard", response_class=CustomORJSONResponse)
|
||||||
|
async def dashboard(authenticated: bool = Depends(verify_dashboard_auth)):
|
||||||
|
rows = await database.fetch_all("SELECT * FROM active_connections")
|
||||||
|
return rows
|
||||||
|
|||||||
+304
-608
@@ -1,480 +1,248 @@
|
|||||||
import asyncio
|
|
||||||
import time
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
import httpx
|
import time
|
||||||
import uuid
|
import mediaflow_proxy.utils.http_utils
|
||||||
import orjson
|
|
||||||
|
|
||||||
from fastapi import APIRouter, Request, BackgroundTasks
|
from fastapi import APIRouter, Request, BackgroundTasks
|
||||||
from fastapi.responses import (
|
from fastapi.responses import (
|
||||||
RedirectResponse,
|
|
||||||
StreamingResponse,
|
|
||||||
FileResponse,
|
FileResponse,
|
||||||
Response,
|
RedirectResponse,
|
||||||
)
|
)
|
||||||
from starlette.background import BackgroundTask
|
|
||||||
from RTN import Torrent, sort_torrents
|
|
||||||
|
|
||||||
from comet.debrid.manager import getDebrid
|
from comet.utils.models import settings, database, trackers
|
||||||
from comet.utils.general import (
|
from comet.utils.general import parse_media_id
|
||||||
config_check,
|
from comet.metadata.manager import MetadataScraper
|
||||||
get_debrid_extension,
|
from comet.scrapers.manager import TorrentManager
|
||||||
get_indexer_manager,
|
from comet.utils.general import config_check, format_title, get_client_ip
|
||||||
get_zilean,
|
from comet.debrid.manager import get_debrid_extension, get_debrid
|
||||||
get_torrentio,
|
from comet.utils.streaming import custom_handle_stream_request
|
||||||
get_mediafusion,
|
|
||||||
filter,
|
|
||||||
get_torrent_hash,
|
|
||||||
translate,
|
|
||||||
get_balanced_hashes,
|
|
||||||
format_title,
|
|
||||||
get_client_ip,
|
|
||||||
get_aliases,
|
|
||||||
add_torrent_to_cache,
|
|
||||||
)
|
|
||||||
from comet.utils.config import is_proxy_stream_authed, is_proxy_stream_enabled, prepare_debrid_config, should_skip_proxy_stream
|
|
||||||
from comet.utils.logger import logger
|
from comet.utils.logger import logger
|
||||||
from comet.utils.models import database, rtn, settings, trackers
|
|
||||||
|
|
||||||
streams = APIRouter()
|
streams = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
@streams.get("/stream/{type}/{id}.json")
|
async def remove_ongoing_search_from_database(media_id: str):
|
||||||
async def stream_noconfig(request: Request, type: str, id: str):
|
await database.execute(
|
||||||
return {
|
"DELETE FROM ongoing_searches WHERE media_id = :media_id",
|
||||||
"streams": [
|
{"media_id": media_id},
|
||||||
{
|
)
|
||||||
"name": "[⚠️] Comet",
|
|
||||||
"description": f"{request.url.scheme}://{request.url.netloc}/configure",
|
|
||||||
"url": "https://comet.fast",
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@streams.get("/{b64config}/stream/{type}/{id}.json")
|
async def is_first_search(media_id: str) -> bool:
|
||||||
|
try:
|
||||||
|
await database.execute(
|
||||||
|
"INSERT INTO first_searches VALUES (:media_id, :timestamp)",
|
||||||
|
{"media_id": media_id, "timestamp": time.time()},
|
||||||
|
)
|
||||||
|
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
async def background_scrape(
|
||||||
|
torrent_manager: TorrentManager, media_id: str, debrid_service: str
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
async with aiohttp.ClientSession() as new_session:
|
||||||
|
await torrent_manager.scrape_torrents(new_session)
|
||||||
|
|
||||||
|
if debrid_service != "torrent" and len(torrent_manager.torrents) > 0:
|
||||||
|
await torrent_manager.get_and_cache_debrid_availability(new_session)
|
||||||
|
|
||||||
|
logger.log(
|
||||||
|
"SCRAPER",
|
||||||
|
"📥 Background scrape + availability check complete!",
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.log("SCRAPER", f"❌ Background scrape + availability check failed: {e}")
|
||||||
|
finally:
|
||||||
|
await remove_ongoing_search_from_database(media_id)
|
||||||
|
|
||||||
|
|
||||||
|
@streams.get("/stream/{media_type}/{media_id}.json")
|
||||||
|
@streams.get("/{b64config}/stream/{media_type}/{media_id}.json")
|
||||||
async def stream(
|
async def stream(
|
||||||
request: Request,
|
request: Request,
|
||||||
b64config: str,
|
media_type: str,
|
||||||
type: str,
|
media_id: str,
|
||||||
id: str,
|
|
||||||
background_tasks: BackgroundTasks,
|
background_tasks: BackgroundTasks,
|
||||||
|
b64config: str = None,
|
||||||
):
|
):
|
||||||
config = config_check(b64config)
|
config = config_check(b64config)
|
||||||
if not config:
|
if not config:
|
||||||
return {
|
return {
|
||||||
"streams": [
|
"streams": [
|
||||||
{
|
{
|
||||||
"name": "[⚠️] Comet",
|
"name": "[❌] Comet",
|
||||||
"description": "Invalid Comet config.",
|
"description": f"⚠️ OBSOLETE CONFIGURATION, PLEASE RE-CONFIGURE ON {request.url.scheme}://{request.url.netloc} ⚠️",
|
||||||
|
"url": "https://comet.fast",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
ongoing_search = await database.fetch_one(
|
||||||
|
"SELECT timestamp FROM ongoing_searches WHERE media_id = :media_id",
|
||||||
|
{"media_id": media_id},
|
||||||
|
)
|
||||||
|
|
||||||
|
if ongoing_search:
|
||||||
|
return {
|
||||||
|
"streams": [
|
||||||
|
{
|
||||||
|
"name": "[🔄] Comet",
|
||||||
|
"description": "Search in progress, please try again in a few seconds...",
|
||||||
"url": "https://comet.fast",
|
"url": "https://comet.fast",
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
connector = aiohttp.TCPConnector(limit=0)
|
connector = aiohttp.TCPConnector(limit=0)
|
||||||
async with aiohttp.ClientSession(
|
async with aiohttp.ClientSession(connector=connector) as session:
|
||||||
connector=connector, raise_for_status=True
|
metadata, aliases = await MetadataScraper(session).fetch_metadata_and_aliases(
|
||||||
) as session:
|
media_type, media_id
|
||||||
full_id = id
|
|
||||||
season = None
|
|
||||||
episode = None
|
|
||||||
if type == "series":
|
|
||||||
info = id.split(":")
|
|
||||||
id = info[0]
|
|
||||||
season = int(info[1])
|
|
||||||
episode = int(info[2])
|
|
||||||
|
|
||||||
year = None
|
|
||||||
year_end = None
|
|
||||||
try:
|
|
||||||
kitsu = False
|
|
||||||
if id == "kitsu":
|
|
||||||
kitsu = True
|
|
||||||
get_metadata = await session.get(
|
|
||||||
f"https://kitsu.io/api/edge/anime/{season}"
|
|
||||||
)
|
|
||||||
metadata = await get_metadata.json()
|
|
||||||
name = metadata["data"]["attributes"]["canonicalTitle"]
|
|
||||||
season = 1
|
|
||||||
else:
|
|
||||||
get_metadata = await session.get(
|
|
||||||
f"https://v3.sg.media-imdb.com/suggestion/a/{id}.json"
|
|
||||||
)
|
|
||||||
metadata = await get_metadata.json()
|
|
||||||
element = metadata["d"][
|
|
||||||
0
|
|
||||||
if metadata["d"][0]["id"]
|
|
||||||
not in ["/imdbpicks/summer-watch-guide", "/emmys"]
|
|
||||||
else 1
|
|
||||||
]
|
|
||||||
|
|
||||||
for element in metadata["d"]:
|
|
||||||
if "/" not in element["id"]:
|
|
||||||
break
|
|
||||||
|
|
||||||
name = element["l"]
|
|
||||||
year = element.get("y")
|
|
||||||
|
|
||||||
if "yr" in element:
|
|
||||||
year_end = int(element["yr"].split("-")[1])
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"Exception while getting metadata for {id}: {e}")
|
|
||||||
|
|
||||||
return {
|
|
||||||
"streams": [
|
|
||||||
{
|
|
||||||
"name": "[⚠️] Comet",
|
|
||||||
"description": f"Can't get metadata for {id}",
|
|
||||||
"url": "https://comet.fast",
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
name = translate(name)
|
|
||||||
log_name = name
|
|
||||||
if type == "series":
|
|
||||||
log_name = f"{name} S{season:02d}E{episode:02d}"
|
|
||||||
|
|
||||||
prepare_debrid_config(config)
|
|
||||||
|
|
||||||
if config["debridApiKey"] == "":
|
|
||||||
services = ["realdebrid", "alldebrid", "premiumize", "torbox", "debridlink", "stremthru"]
|
|
||||||
debrid_emoji = "⬇️"
|
|
||||||
else:
|
|
||||||
services = [config["debridService"]]
|
|
||||||
debrid_emoji = "⚡"
|
|
||||||
|
|
||||||
results = []
|
|
||||||
if (
|
|
||||||
is_proxy_stream_enabled(config)
|
|
||||||
and not is_proxy_stream_authed(config)
|
|
||||||
):
|
|
||||||
results.append(
|
|
||||||
{
|
|
||||||
"name": "[⚠️] Comet",
|
|
||||||
"description": "Debrid Stream Proxy Password incorrect.\nStreams will not be proxied.",
|
|
||||||
"url": "https://comet.fast",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
indexers = config["indexers"].copy()
|
|
||||||
if settings.SCRAPE_TORRENTIO:
|
|
||||||
indexers.append("torrentio")
|
|
||||||
if settings.SCRAPE_MEDIAFUSION:
|
|
||||||
indexers.append("mediafusion")
|
|
||||||
if settings.ZILEAN_URL:
|
|
||||||
indexers.append("dmm")
|
|
||||||
indexers_json = orjson.dumps(indexers).decode("utf-8")
|
|
||||||
|
|
||||||
all_sorted_ranked_files = {}
|
|
||||||
trackers_found = (
|
|
||||||
set()
|
|
||||||
) # we want to check that we have a cache for each of the user's trackers
|
|
||||||
the_time = time.time()
|
|
||||||
cache_ttl = settings.CACHE_TTL
|
|
||||||
|
|
||||||
for debrid_service in services:
|
|
||||||
cached_results = await database.fetch_all(
|
|
||||||
f"""
|
|
||||||
SELECT info_hash, tracker, data
|
|
||||||
FROM cache
|
|
||||||
WHERE debridService = :debrid_service
|
|
||||||
AND name = :name
|
|
||||||
AND ((cast(:season as INTEGER) IS NULL AND season IS NULL) OR season = cast(:season as INTEGER))
|
|
||||||
AND ((cast(:episode as INTEGER) IS NULL AND episode IS NULL) OR episode = cast(:episode as INTEGER))
|
|
||||||
AND tracker IN (SELECT cast(value as TEXT) FROM {'json_array_elements_text' if settings.DATABASE_TYPE == 'postgresql' else 'json_each'}(:indexers))
|
|
||||||
AND timestamp + :cache_ttl >= :current_time
|
|
||||||
""",
|
|
||||||
{
|
|
||||||
"debrid_service": debrid_service,
|
|
||||||
"name": name,
|
|
||||||
"season": season,
|
|
||||||
"episode": episode,
|
|
||||||
"indexers": indexers_json,
|
|
||||||
"cache_ttl": cache_ttl,
|
|
||||||
"current_time": the_time,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
for result in cached_results:
|
|
||||||
trackers_found.add(result["tracker"].lower())
|
|
||||||
|
|
||||||
hash = result["info_hash"]
|
|
||||||
if "searched" in hash:
|
|
||||||
continue
|
|
||||||
|
|
||||||
all_sorted_ranked_files[hash] = orjson.loads(result["data"])
|
|
||||||
|
|
||||||
if len(all_sorted_ranked_files) != 0 and set(indexers).issubset(trackers_found):
|
|
||||||
debrid_extension = get_debrid_extension(
|
|
||||||
debrid_service, config["debridApiKey"]
|
|
||||||
)
|
|
||||||
balanced_hashes = get_balanced_hashes(all_sorted_ranked_files, config)
|
|
||||||
|
|
||||||
for resolution in balanced_hashes:
|
|
||||||
for hash in balanced_hashes[resolution]:
|
|
||||||
data = all_sorted_ranked_files[hash]["data"]
|
|
||||||
the_stream = {
|
|
||||||
"name": f"[{debrid_extension}{debrid_emoji}] Comet {data['resolution']}",
|
|
||||||
"description": format_title(data, config),
|
|
||||||
"torrentTitle": (
|
|
||||||
data["torrent_title"] if "torrent_title" in data else None
|
|
||||||
),
|
|
||||||
"torrentSize": (
|
|
||||||
data["torrent_size"] if "torrent_size" in data else None
|
|
||||||
),
|
|
||||||
"behaviorHints": {
|
|
||||||
"filename": data["raw_title"],
|
|
||||||
"bingeGroup": "comet|" + hash,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
if config["debridApiKey"] != "":
|
|
||||||
the_stream["url"] = (
|
|
||||||
f"{request.url.scheme}://{request.url.netloc}/{b64config}/playback/{hash}/{data['index']}"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
the_stream["infoHash"] = hash
|
|
||||||
index = str(data["index"])
|
|
||||||
the_stream["fileIdx"] = (
|
|
||||||
1 if "|" in index else int(index)
|
|
||||||
) # 1 because for Premiumize it's impossible to get the file index
|
|
||||||
the_stream["sources"] = trackers
|
|
||||||
|
|
||||||
results.append(the_stream)
|
|
||||||
|
|
||||||
logger.info(
|
|
||||||
f"{len(all_sorted_ranked_files)} cached results found for {log_name}"
|
|
||||||
)
|
|
||||||
|
|
||||||
return {"streams": results}
|
|
||||||
|
|
||||||
if config["debridApiKey"] == "":
|
|
||||||
return {
|
|
||||||
"streams": [
|
|
||||||
{
|
|
||||||
"name": "[⚠️] Comet",
|
|
||||||
"description": "No cache found for Direct Torrenting.",
|
|
||||||
"url": "https://comet.fast",
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
logger.info(f"No cache found for {log_name} with user configuration")
|
|
||||||
|
|
||||||
debrid = getDebrid(session, config, get_client_ip(request))
|
|
||||||
|
|
||||||
check_premium = await debrid.check_premium()
|
|
||||||
if not check_premium:
|
|
||||||
additional_info = ""
|
|
||||||
if config["debridService"] == "alldebrid":
|
|
||||||
additional_info = "\nCheck your email!"
|
|
||||||
|
|
||||||
return {
|
|
||||||
"streams": [
|
|
||||||
{
|
|
||||||
"name": "[⚠️] Comet",
|
|
||||||
"description": f"Invalid {config['debridService']} account.{additional_info}",
|
|
||||||
"url": "https://comet.fast",
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
indexer_manager_type = settings.INDEXER_MANAGER_TYPE
|
|
||||||
|
|
||||||
search_indexer = len(config["indexers"]) != 0
|
|
||||||
torrents = []
|
|
||||||
tasks = []
|
|
||||||
if indexer_manager_type and search_indexer:
|
|
||||||
logger.info(
|
|
||||||
f"Start of {indexer_manager_type} search for {log_name} with indexers {config['indexers']}"
|
|
||||||
)
|
|
||||||
|
|
||||||
search_terms = [name]
|
|
||||||
if type == "series":
|
|
||||||
if not kitsu:
|
|
||||||
search_terms.append(f"{name} S{season:02d}E{episode:02d}")
|
|
||||||
else:
|
|
||||||
search_terms.append(f"{name} {episode}")
|
|
||||||
tasks.extend(
|
|
||||||
get_indexer_manager(
|
|
||||||
session, indexer_manager_type, config["indexers"], term
|
|
||||||
)
|
|
||||||
for term in search_terms
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
logger.info(
|
|
||||||
f"No indexer {'manager ' if not indexer_manager_type else ''}{'selected by user' if indexer_manager_type else 'defined'} for {log_name}"
|
|
||||||
)
|
|
||||||
|
|
||||||
if settings.ZILEAN_URL:
|
|
||||||
tasks.append(get_zilean(session, name, log_name, season, episode))
|
|
||||||
|
|
||||||
if settings.SCRAPE_TORRENTIO:
|
|
||||||
tasks.append(get_torrentio(log_name, type, full_id))
|
|
||||||
|
|
||||||
if settings.SCRAPE_MEDIAFUSION:
|
|
||||||
tasks.append(get_mediafusion(log_name, type, full_id))
|
|
||||||
|
|
||||||
search_response = await asyncio.gather(*tasks)
|
|
||||||
for results in search_response:
|
|
||||||
for result in results:
|
|
||||||
torrents.append(result)
|
|
||||||
|
|
||||||
logger.info(
|
|
||||||
f"{len(torrents)} unique torrents found for {log_name}"
|
|
||||||
+ (
|
|
||||||
" with "
|
|
||||||
+ ", ".join(
|
|
||||||
part
|
|
||||||
for part in [
|
|
||||||
indexer_manager_type,
|
|
||||||
"Zilean" if settings.ZILEAN_URL else None,
|
|
||||||
"Torrentio" if settings.SCRAPE_TORRENTIO else None,
|
|
||||||
"MediaFusion" if settings.SCRAPE_MEDIAFUSION else None,
|
|
||||||
]
|
|
||||||
if part
|
|
||||||
)
|
|
||||||
if any(
|
|
||||||
[
|
|
||||||
indexer_manager_type,
|
|
||||||
settings.ZILEAN_URL,
|
|
||||||
settings.SCRAPE_TORRENTIO,
|
|
||||||
settings.SCRAPE_MEDIAFUSION,
|
|
||||||
]
|
|
||||||
)
|
|
||||||
else ""
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
if metadata is None:
|
||||||
|
logger.log("SCRAPER", f"❌ Failed to fetch metadata for {media_id}")
|
||||||
|
return {
|
||||||
|
"streams": [
|
||||||
|
{
|
||||||
|
"name": "[⚠️] Comet",
|
||||||
|
"description": "Unable to get metadata.",
|
||||||
|
"url": "https://comet.fast",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
if len(torrents) == 0:
|
title = metadata["title"]
|
||||||
return {"streams": []}
|
year = metadata["year"]
|
||||||
|
year_end = metadata["year_end"]
|
||||||
|
season = metadata["season"]
|
||||||
|
episode = metadata["episode"]
|
||||||
|
|
||||||
if settings.TITLE_MATCH_CHECK:
|
log_title = f"({media_id}) {title}"
|
||||||
aliases = await get_aliases(
|
if media_type == "series":
|
||||||
session, "movies" if type == "movie" else "shows", id
|
log_title += f" S{season:02d}E{episode:02d}"
|
||||||
)
|
|
||||||
|
|
||||||
indexed_torrents = [(i, torrents[i]["Title"]) for i in range(len(torrents))]
|
logger.log("SCRAPER", f"🔍 Starting search for {log_title}")
|
||||||
chunk_size = 50
|
|
||||||
chunks = [
|
|
||||||
indexed_torrents[i : i + chunk_size]
|
|
||||||
for i in range(0, len(indexed_torrents), chunk_size)
|
|
||||||
]
|
|
||||||
|
|
||||||
remove_adult_content = (
|
id, season, episode = parse_media_id(media_type, media_id)
|
||||||
settings.REMOVE_ADULT_CONTENT and config["removeTrash"]
|
media_only_id = id
|
||||||
)
|
|
||||||
tasks = []
|
|
||||||
for chunk in chunks:
|
|
||||||
tasks.append(
|
|
||||||
filter(chunk, name, year, year_end, aliases, remove_adult_content)
|
|
||||||
)
|
|
||||||
|
|
||||||
filtered_torrents = await asyncio.gather(*tasks)
|
debrid_service = config["debridService"]
|
||||||
index_less = 0
|
torrent_manager = TorrentManager(
|
||||||
for result in filtered_torrents:
|
debrid_service,
|
||||||
for filtered in result:
|
config["debridApiKey"],
|
||||||
if not filtered[1]:
|
get_client_ip(request),
|
||||||
del torrents[filtered[0] - index_less]
|
media_type,
|
||||||
index_less += 1
|
media_id,
|
||||||
continue
|
media_only_id,
|
||||||
|
title,
|
||||||
logger.info(
|
year,
|
||||||
f"{len(torrents)} torrents passed title match check for {log_name}"
|
year_end,
|
||||||
)
|
|
||||||
|
|
||||||
if len(torrents) == 0:
|
|
||||||
return {"streams": []}
|
|
||||||
|
|
||||||
tasks = []
|
|
||||||
for i in range(len(torrents)):
|
|
||||||
tasks.append(get_torrent_hash(session, (i, torrents[i])))
|
|
||||||
|
|
||||||
torrent_hashes = await asyncio.gather(*tasks)
|
|
||||||
index_less = 0
|
|
||||||
for hash in torrent_hashes:
|
|
||||||
if not hash[1]:
|
|
||||||
del torrents[hash[0] - index_less]
|
|
||||||
index_less += 1
|
|
||||||
continue
|
|
||||||
|
|
||||||
torrents[hash[0] - index_less]["InfoHash"] = hash[1]
|
|
||||||
|
|
||||||
logger.info(f"{len(torrents)} info hashes found for {log_name}")
|
|
||||||
|
|
||||||
if len(torrents) == 0:
|
|
||||||
return {"streams": []}
|
|
||||||
|
|
||||||
files = await debrid.get_files(
|
|
||||||
list({hash[1] for hash in torrent_hashes if hash[1] is not None}),
|
|
||||||
type,
|
|
||||||
season,
|
season,
|
||||||
episode,
|
episode,
|
||||||
kitsu,
|
aliases,
|
||||||
video_id=full_id,
|
settings.REMOVE_ADULT_CONTENT and config["removeTrash"],
|
||||||
)
|
)
|
||||||
|
|
||||||
ranked_files = set()
|
await torrent_manager.get_cached_torrents()
|
||||||
torrents_by_hash = {torrent["InfoHash"]: torrent for torrent in torrents}
|
logger.log(
|
||||||
for hash in files:
|
"SCRAPER", f"📦 Found cached torrents: {len(torrent_manager.torrents)}"
|
||||||
try:
|
|
||||||
ranked_file = rtn.rank(
|
|
||||||
torrents_by_hash[hash]["Title"],
|
|
||||||
hash,
|
|
||||||
remove_trash=False, # user can choose if he wants to remove it
|
|
||||||
)
|
|
||||||
|
|
||||||
ranked_files.add(ranked_file)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
sorted_ranked_files = sort_torrents(ranked_files)
|
|
||||||
|
|
||||||
len_sorted_ranked_files = len(sorted_ranked_files)
|
|
||||||
logger.info(
|
|
||||||
f"{len_sorted_ranked_files} cached files found on {config['debridService']} for {log_name}"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if len_sorted_ranked_files == 0:
|
is_first = await is_first_search(media_id)
|
||||||
return {"streams": []}
|
has_cached_results = len(torrent_manager.torrents) > 0
|
||||||
|
|
||||||
sorted_ranked_files = {
|
cached_results = []
|
||||||
key: (value.model_dump() if isinstance(value, Torrent) else value)
|
non_cached_results = []
|
||||||
for key, value in sorted_ranked_files.items()
|
|
||||||
}
|
if not has_cached_results:
|
||||||
for hash in sorted_ranked_files: # needed for caching
|
logger.log("SCRAPER", f"🔎 Starting new search for {log_title}")
|
||||||
sorted_ranked_files[hash]["data"]["title"] = files[hash]["title"]
|
await database.execute(
|
||||||
sorted_ranked_files[hash]["data"]["torrent_title"] = torrents_by_hash[hash][
|
f"INSERT {'OR IGNORE ' if settings.DATABASE_TYPE == 'sqlite' else ''}INTO ongoing_searches VALUES (:media_id, :timestamp){' ON CONFLICT DO NOTHING' if settings.DATABASE_TYPE == 'postgresql' else ''}",
|
||||||
"Title"
|
{"media_id": media_id, "timestamp": time.time()},
|
||||||
]
|
|
||||||
sorted_ranked_files[hash]["data"]["tracker"] = torrents_by_hash[hash][
|
|
||||||
"Tracker"
|
|
||||||
]
|
|
||||||
torrent_size = torrents_by_hash[hash]["Size"]
|
|
||||||
sorted_ranked_files[hash]["data"]["size"] = (
|
|
||||||
files[hash]["size"]
|
|
||||||
)
|
)
|
||||||
sorted_ranked_files[hash]["data"]["torrent_size"] = (
|
background_tasks.add_task(remove_ongoing_search_from_database, media_id)
|
||||||
torrent_size if torrent_size else files[hash]["size"]
|
|
||||||
|
await torrent_manager.scrape_torrents(session)
|
||||||
|
logger.log(
|
||||||
|
"SCRAPER",
|
||||||
|
f"📥 Scraped torrents: {len(torrent_manager.torrents)}",
|
||||||
|
)
|
||||||
|
elif is_first:
|
||||||
|
logger.log(
|
||||||
|
"SCRAPER",
|
||||||
|
f"🔄 Starting background scrape + availability check for {log_title}",
|
||||||
|
)
|
||||||
|
await database.execute(
|
||||||
|
f"INSERT {'OR IGNORE ' if settings.DATABASE_TYPE == 'sqlite' else ''}INTO ongoing_searches VALUES (:media_id, :timestamp){' ON CONFLICT DO NOTHING' if settings.DATABASE_TYPE == 'postgresql' else ''}",
|
||||||
|
{"media_id": media_id, "timestamp": time.time()},
|
||||||
)
|
)
|
||||||
sorted_ranked_files[hash]["data"]["index"] = files[hash]["index"]
|
|
||||||
|
|
||||||
background_tasks.add_task(
|
background_tasks.add_task(
|
||||||
add_torrent_to_cache, config, name, season, episode, sorted_ranked_files
|
background_scrape, torrent_manager, media_id, debrid_service
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info(f"Results have been cached for {log_name}")
|
cached_results.append(
|
||||||
|
{
|
||||||
|
"name": "[🔄] Comet",
|
||||||
|
"description": "First search for this media - More results will be available in a few seconds...",
|
||||||
|
"url": "https://comet.fast",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
debrid_extension = get_debrid_extension(config["debridService"], config["debridApiKey"])
|
await torrent_manager.get_cached_availability()
|
||||||
|
|
||||||
balanced_hashes = get_balanced_hashes(sorted_ranked_files, config)
|
|
||||||
|
|
||||||
results = []
|
|
||||||
if (
|
if (
|
||||||
is_proxy_stream_enabled(config)
|
(
|
||||||
and not is_proxy_stream_authed(config)
|
not has_cached_results
|
||||||
|
or sum(
|
||||||
|
1
|
||||||
|
for torrent in torrent_manager.torrents.values()
|
||||||
|
if torrent["cached"]
|
||||||
|
)
|
||||||
|
== 0
|
||||||
|
)
|
||||||
|
and len(torrent_manager.torrents) > 0
|
||||||
|
and debrid_service != "torrent"
|
||||||
):
|
):
|
||||||
results.append(
|
logger.log("SCRAPER", "🔄 Checking availability on debrid service...")
|
||||||
|
await torrent_manager.get_and_cache_debrid_availability(session)
|
||||||
|
|
||||||
|
if debrid_service != "torrent":
|
||||||
|
cached_count = sum(
|
||||||
|
1 for torrent in torrent_manager.torrents.values() if torrent["cached"]
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.log(
|
||||||
|
"SCRAPER",
|
||||||
|
f"💾 Available cached torrents on {debrid_service}: {cached_count}/{len(torrent_manager.torrents)}",
|
||||||
|
)
|
||||||
|
|
||||||
|
initial_torrent_count = len(torrent_manager.torrents)
|
||||||
|
|
||||||
|
torrent_manager.rank_torrents(
|
||||||
|
config["rtnSettings"],
|
||||||
|
config["rtnRanking"],
|
||||||
|
config["maxResultsPerResolution"],
|
||||||
|
config["maxSize"],
|
||||||
|
config["cachedOnly"],
|
||||||
|
config["removeTrash"],
|
||||||
|
)
|
||||||
|
logger.log(
|
||||||
|
"SCRAPER",
|
||||||
|
f"⚖️ Torrents after RTN filtering: {len(torrent_manager.ranked_torrents)}/{initial_torrent_count}",
|
||||||
|
)
|
||||||
|
|
||||||
|
debrid_extension = get_debrid_extension(debrid_service)
|
||||||
|
|
||||||
|
if (
|
||||||
|
config["debridStreamProxyPassword"] != ""
|
||||||
|
and settings.PROXY_DEBRID_STREAM
|
||||||
|
and settings.PROXY_DEBRID_STREAM_PASSWORD
|
||||||
|
!= config["debridStreamProxyPassword"]
|
||||||
|
):
|
||||||
|
cached_results.append(
|
||||||
{
|
{
|
||||||
"name": "[⚠️] Comet",
|
"name": "[⚠️] Comet",
|
||||||
"description": "Debrid Stream Proxy Password incorrect.\nStreams will not be proxied.",
|
"description": "Debrid Stream Proxy Password incorrect.\nStreams will not be proxied.",
|
||||||
@@ -482,209 +250,137 @@ async def stream(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
for resolution in balanced_hashes:
|
result_season = season if season is not None else "n"
|
||||||
for hash in balanced_hashes[resolution]:
|
result_episode = episode if episode is not None else "n"
|
||||||
data = sorted_ranked_files[hash]["data"]
|
|
||||||
index = data['index']
|
torrents = torrent_manager.torrents
|
||||||
if index == -1:
|
for info_hash in torrent_manager.ranked_torrents:
|
||||||
index = data['title']
|
torrent = torrents[info_hash]
|
||||||
url = f"{request.url.scheme}://{request.url.netloc}/{b64config}/playback/{hash}/{index}"
|
rtn_data = torrent["parsed"]
|
||||||
results.append(
|
|
||||||
{
|
debrid_emoji = (
|
||||||
"name": f"[{debrid_extension}⚡] Comet {data['resolution']}",
|
"🧲"
|
||||||
"description": format_title(data, config),
|
if debrid_service == "torrent"
|
||||||
"torrentTitle": data["torrent_title"],
|
else ("⚡" if torrent["cached"] else "⬇️")
|
||||||
"torrentSize": data["torrent_size"],
|
)
|
||||||
"url": url,
|
|
||||||
"behaviorHints": {
|
the_stream = {
|
||||||
"filename": data["raw_title"],
|
"name": f"[{debrid_extension}{debrid_emoji}] Comet {rtn_data.resolution}",
|
||||||
"bingeGroup": "comet|" + hash,
|
"description": format_title(
|
||||||
},
|
rtn_data,
|
||||||
}
|
torrent["title"],
|
||||||
|
torrent["seeders"],
|
||||||
|
torrent["size"],
|
||||||
|
torrent["tracker"],
|
||||||
|
config["resultFormat"],
|
||||||
|
),
|
||||||
|
"behaviorHints": {
|
||||||
|
"bingeGroup": "comet|" + info_hash,
|
||||||
|
"videoSize": torrent["size"],
|
||||||
|
"filename": rtn_data.raw_title,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if debrid_service == "torrent":
|
||||||
|
the_stream["infoHash"] = info_hash
|
||||||
|
|
||||||
|
if torrent["fileIndex"] is not None:
|
||||||
|
the_stream["fileIdx"] = torrent["fileIndex"]
|
||||||
|
|
||||||
|
if len(torrent["sources"]) == 0:
|
||||||
|
the_stream["sources"] = trackers
|
||||||
|
else:
|
||||||
|
the_stream["sources"] = torrent["sources"]
|
||||||
|
else:
|
||||||
|
the_stream["url"] = (
|
||||||
|
f"{request.url.scheme}://{request.url.netloc}/{b64config}/playback/{info_hash}/{torrent['fileIndex'] if torrent['cached'] and torrent['fileIndex'] is not None else 'n'}/{title}/{result_season}/{result_episode}"
|
||||||
)
|
)
|
||||||
|
|
||||||
return {"streams": results}
|
if torrent["cached"]:
|
||||||
|
cached_results.append(the_stream)
|
||||||
|
else:
|
||||||
|
non_cached_results.append(the_stream)
|
||||||
|
|
||||||
|
return {"streams": cached_results + non_cached_results}
|
||||||
|
|
||||||
|
|
||||||
@streams.head("/{b64config}/playback/{hash}/{index}")
|
@streams.get("/{b64config}/playback/{hash}/{index}/{name}/{season}/{episode}")
|
||||||
async def playback(b64config: str, hash: str, index: str):
|
async def playback(
|
||||||
return RedirectResponse("https://stremio.fast", status_code=302)
|
request: Request,
|
||||||
|
b64config: str,
|
||||||
|
hash: str,
|
||||||
class CustomORJSONResponse(Response):
|
index: str,
|
||||||
media_type = "application/json"
|
name: str,
|
||||||
|
season: str,
|
||||||
def render(self, content) -> bytes:
|
episode: str,
|
||||||
assert orjson is not None, "orjson must be installed"
|
):
|
||||||
return orjson.dumps(content, option=orjson.OPT_INDENT_2)
|
|
||||||
|
|
||||||
|
|
||||||
@streams.get("/active-connections", response_class=CustomORJSONResponse)
|
|
||||||
async def active_connections(request: Request, password: str):
|
|
||||||
if password != settings.DASHBOARD_ADMIN_PASSWORD:
|
|
||||||
return "Invalid Password"
|
|
||||||
|
|
||||||
active_connections = await database.fetch_all("SELECT * FROM active_connections")
|
|
||||||
|
|
||||||
return {
|
|
||||||
"total_connections": len(active_connections),
|
|
||||||
"active_connections": active_connections,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@streams.get("/{b64config}/playback/{hash}/{index}")
|
|
||||||
async def playback(request: Request, b64config: str, hash: str, index: str):
|
|
||||||
config = config_check(b64config)
|
config = config_check(b64config)
|
||||||
if not config:
|
# if not config:
|
||||||
return FileResponse("comet/assets/invalidconfig.mp4")
|
# return FileResponse("comet/assets/invalidconfig.mp4")
|
||||||
|
|
||||||
prepare_debrid_config(config)
|
season = int(season) if season != "n" else None
|
||||||
|
episode = int(episode) if episode != "n" else None
|
||||||
|
|
||||||
async with aiohttp.ClientSession(raise_for_status=True) as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
# Check for cached download link
|
|
||||||
cached_link = await database.fetch_one(
|
cached_link = await database.fetch_one(
|
||||||
f"SELECT link, timestamp FROM download_links WHERE debrid_key = '{config['debridApiKey']}' AND hash = '{hash}' AND file_index = '{index}'"
|
f"SELECT download_url FROM download_links_cache WHERE debrid_key = '{config['debridApiKey']}' AND info_hash = '{hash}' AND ((cast(:season as INTEGER) IS NULL AND season IS NULL) OR season = cast(:season as INTEGER)) AND ((cast(:episode as INTEGER) IS NULL AND episode IS NULL) OR episode = cast(:episode as INTEGER)) AND timestamp + 3600 >= :current_time",
|
||||||
|
{
|
||||||
|
"current_time": time.time(),
|
||||||
|
"season": season,
|
||||||
|
"episode": season,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
current_time = time.time()
|
download_url = None
|
||||||
download_link = None
|
|
||||||
if cached_link:
|
if cached_link:
|
||||||
link = cached_link["link"]
|
download_url = cached_link["download_url"]
|
||||||
timestamp = cached_link["timestamp"]
|
|
||||||
|
|
||||||
if current_time - timestamp < 3600:
|
|
||||||
download_link = link
|
|
||||||
else:
|
|
||||||
# Cache expired, remove old entry
|
|
||||||
await database.execute(
|
|
||||||
f"DELETE FROM download_links WHERE debrid_key = '{config['debridApiKey']}' AND hash = '{hash}' AND file_index = '{index}'"
|
|
||||||
)
|
|
||||||
|
|
||||||
ip = get_client_ip(request)
|
ip = get_client_ip(request)
|
||||||
|
if download_url is None:
|
||||||
if not download_link:
|
debrid = get_debrid(
|
||||||
debrid = getDebrid(
|
|
||||||
session,
|
session,
|
||||||
config,
|
None,
|
||||||
ip
|
None,
|
||||||
if (
|
config["debridService"],
|
||||||
not is_proxy_stream_enabled(config)
|
config["debridApiKey"],
|
||||||
or not is_proxy_stream_authed(config)
|
ip,
|
||||||
)
|
|
||||||
else "",
|
|
||||||
)
|
)
|
||||||
download_link = await debrid.generate_download_link(hash, index)
|
download_url = await debrid.generate_download_link(
|
||||||
if not download_link:
|
hash, index, name, season, episode
|
||||||
|
)
|
||||||
|
if not download_url:
|
||||||
return FileResponse("comet/assets/uncached.mp4")
|
return FileResponse("comet/assets/uncached.mp4")
|
||||||
|
|
||||||
# Cache the new download link
|
query = f"""
|
||||||
|
INSERT {"OR IGNORE " if settings.DATABASE_TYPE == "sqlite" else ""}
|
||||||
|
INTO download_links_cache
|
||||||
|
VALUES (:debrid_key, :info_hash, :season, :episode, :download_url, :timestamp)
|
||||||
|
{" ON CONFLICT DO NOTHING" if settings.DATABASE_TYPE == "postgresql" else ""}
|
||||||
|
"""
|
||||||
|
|
||||||
await database.execute(
|
await database.execute(
|
||||||
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 ''}",
|
query,
|
||||||
{
|
{
|
||||||
"debrid_key": config["debridApiKey"],
|
"debrid_key": config["debridApiKey"],
|
||||||
"hash": hash,
|
"info_hash": hash,
|
||||||
"file_index": index,
|
"season": season,
|
||||||
"link": download_link,
|
"episode": episode,
|
||||||
"timestamp": current_time,
|
"download_url": download_url,
|
||||||
|
"timestamp": time.time(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
if should_skip_proxy_stream(config):
|
|
||||||
return RedirectResponse(download_link, status_code=302)
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
is_proxy_stream_enabled(config)
|
settings.PROXY_DEBRID_STREAM
|
||||||
and is_proxy_stream_authed(config)
|
and settings.PROXY_DEBRID_STREAM_PASSWORD
|
||||||
|
== config["debridStreamProxyPassword"]
|
||||||
):
|
):
|
||||||
if settings.PROXY_DEBRID_STREAM_MAX_CONNECTIONS != -1:
|
return await custom_handle_stream_request(
|
||||||
active_ip_connections = await database.fetch_all(
|
request.method,
|
||||||
"SELECT ip, COUNT(*) as connections FROM active_connections GROUP BY ip"
|
download_url,
|
||||||
)
|
mediaflow_proxy.utils.http_utils.get_proxy_headers(request),
|
||||||
if any(
|
media_id=hash,
|
||||||
connection["ip"] == ip
|
ip=ip,
|
||||||
and connection["connections"]
|
)
|
||||||
>= settings.PROXY_DEBRID_STREAM_MAX_CONNECTIONS
|
|
||||||
for connection in active_ip_connections
|
|
||||||
):
|
|
||||||
return FileResponse("comet/assets/proxylimit.mp4")
|
|
||||||
|
|
||||||
proxy = None
|
return RedirectResponse(download_url, status_code=302)
|
||||||
|
|
||||||
class Streamer:
|
|
||||||
def __init__(self, id: str):
|
|
||||||
self.id = id
|
|
||||||
|
|
||||||
self.client = httpx.AsyncClient(proxy=proxy, timeout=None)
|
|
||||||
self.response = None
|
|
||||||
|
|
||||||
async def stream_content(self, headers: dict):
|
|
||||||
async with self.client.stream(
|
|
||||||
"GET", download_link, headers=headers
|
|
||||||
) as self.response:
|
|
||||||
async for chunk in self.response.aiter_raw():
|
|
||||||
yield chunk
|
|
||||||
|
|
||||||
async def close(self):
|
|
||||||
await database.execute(
|
|
||||||
f"DELETE FROM active_connections WHERE id = '{self.id}'"
|
|
||||||
)
|
|
||||||
|
|
||||||
if self.response is not None:
|
|
||||||
await self.response.aclose()
|
|
||||||
if self.client is not None:
|
|
||||||
await self.client.aclose()
|
|
||||||
|
|
||||||
range_header = request.headers.get("range", "bytes=0-")
|
|
||||||
|
|
||||||
try:
|
|
||||||
if config["debridService"] != "torbox":
|
|
||||||
response = await session.head(
|
|
||||||
download_link, headers={"Range": range_header}
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
response = await session.get(
|
|
||||||
download_link, headers={"Range": range_header}
|
|
||||||
)
|
|
||||||
except aiohttp.ClientResponseError as e:
|
|
||||||
if e.status == 503 and config["debridService"] == "alldebrid":
|
|
||||||
proxy = (
|
|
||||||
settings.DEBRID_PROXY_URL
|
|
||||||
) # proxy is needed only to proxy alldebrid streams
|
|
||||||
|
|
||||||
response = await session.head(
|
|
||||||
download_link, headers={"Range": range_header}, proxy=proxy
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
logger.warning(f"Exception while proxying {download_link}: {e}")
|
|
||||||
return
|
|
||||||
|
|
||||||
if response.status == 206 or (
|
|
||||||
response.status == 200 and config["debridService"] == "torbox"
|
|
||||||
):
|
|
||||||
id = str(uuid.uuid4())
|
|
||||||
await database.execute(
|
|
||||||
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 ''}",
|
|
||||||
{
|
|
||||||
"id": id,
|
|
||||||
"ip": ip,
|
|
||||||
"content": str(response.url),
|
|
||||||
"timestamp": current_time,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
streamer = Streamer(id)
|
|
||||||
|
|
||||||
return StreamingResponse(
|
|
||||||
streamer.stream_content({"Range": range_header}),
|
|
||||||
status_code=206,
|
|
||||||
headers={
|
|
||||||
"Content-Range": response.headers["Content-Range"],
|
|
||||||
"Content-Length": response.headers["Content-Length"],
|
|
||||||
"Accept-Ranges": "bytes",
|
|
||||||
},
|
|
||||||
background=BackgroundTask(streamer.close),
|
|
||||||
)
|
|
||||||
|
|
||||||
return FileResponse("comet/assets/uncached.mp4")
|
|
||||||
|
|
||||||
return RedirectResponse(download_link, status_code=302)
|
|
||||||
|
|||||||
+3
-167
@@ -1,172 +1,8 @@
|
|||||||
import aiohttp
|
import aiohttp
|
||||||
import asyncio
|
|
||||||
|
|
||||||
from RTN import parse
|
|
||||||
|
|
||||||
from comet.utils.general import is_video
|
|
||||||
from comet.utils.logger import logger
|
|
||||||
from comet.utils.models import settings
|
|
||||||
|
|
||||||
|
|
||||||
class AllDebrid:
|
class AllDebrid:
|
||||||
def __init__(self, session: aiohttp.ClientSession, debrid_api_key: str):
|
def __init__(
|
||||||
session.headers["Authorization"] = f"Bearer {debrid_api_key}"
|
self, session: aiohttp.ClientSession, video_id, debrid_api_key: str, ip: str
|
||||||
self.session = session
|
|
||||||
self.proxy = None
|
|
||||||
|
|
||||||
self.api_url = "http://api.alldebrid.com/v4"
|
|
||||||
self.agent = "comet"
|
|
||||||
|
|
||||||
async def check_premium(self):
|
|
||||||
try:
|
|
||||||
check_premium = await self.session.get(
|
|
||||||
f"{self.api_url}/user?agent={self.agent}"
|
|
||||||
)
|
|
||||||
check_premium = await check_premium.text()
|
|
||||||
if '"isPremium":true' in check_premium:
|
|
||||||
return True
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(
|
|
||||||
f"Exception while checking premium status on All-Debrid: {e}"
|
|
||||||
)
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
async def get_instant(self, chunk: list):
|
|
||||||
try:
|
|
||||||
get_instant = await self.session.get(
|
|
||||||
f"{self.api_url}/magnet/instant?agent={self.agent}&magnets[]={'&magnets[]='.join(chunk)}"
|
|
||||||
)
|
|
||||||
return await get_instant.json()
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(
|
|
||||||
f"Exception while checking hashes instant availability on All-Debrid: {e}"
|
|
||||||
)
|
|
||||||
|
|
||||||
async def get_files(
|
|
||||||
self, torrent_hashes: list, type: str, season: str, episode: str, kitsu: bool, **kwargs
|
|
||||||
):
|
):
|
||||||
chunk_size = 500
|
pass
|
||||||
chunks = [
|
|
||||||
torrent_hashes[i : i + chunk_size]
|
|
||||||
for i in range(0, len(torrent_hashes), chunk_size)
|
|
||||||
]
|
|
||||||
|
|
||||||
tasks = []
|
|
||||||
for chunk in chunks:
|
|
||||||
tasks.append(self.get_instant(chunk))
|
|
||||||
|
|
||||||
responses = await asyncio.gather(*tasks)
|
|
||||||
|
|
||||||
availability = [response for response in responses if response]
|
|
||||||
|
|
||||||
files = {}
|
|
||||||
|
|
||||||
if type == "series":
|
|
||||||
for result in availability:
|
|
||||||
if "status" not in result or result["status"] != "success":
|
|
||||||
continue
|
|
||||||
|
|
||||||
for magnet in result["data"]["magnets"]:
|
|
||||||
if not magnet["instant"]:
|
|
||||||
continue
|
|
||||||
|
|
||||||
for file in magnet["files"]:
|
|
||||||
filename = file["n"]
|
|
||||||
pack = False
|
|
||||||
if "e" in file: # PACK
|
|
||||||
filename = file["e"][0]["n"]
|
|
||||||
pack = True
|
|
||||||
|
|
||||||
if not is_video(filename):
|
|
||||||
continue
|
|
||||||
|
|
||||||
if "sample" in filename.lower():
|
|
||||||
continue
|
|
||||||
|
|
||||||
filename_parsed = parse(filename)
|
|
||||||
if episode not in filename_parsed.episodes:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if kitsu:
|
|
||||||
if filename_parsed.seasons:
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
if season not in filename_parsed.seasons:
|
|
||||||
continue
|
|
||||||
|
|
||||||
files[magnet["hash"]] = {
|
|
||||||
"index": magnet["files"].index(file),
|
|
||||||
"title": filename,
|
|
||||||
"size": file["e"][0]["s"] if pack else file["s"],
|
|
||||||
}
|
|
||||||
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
for result in availability:
|
|
||||||
if "status" not in result or result["status"] != "success":
|
|
||||||
continue
|
|
||||||
|
|
||||||
for magnet in result["data"]["magnets"]:
|
|
||||||
if not magnet["instant"]:
|
|
||||||
continue
|
|
||||||
|
|
||||||
for file in magnet["files"]:
|
|
||||||
filename = file["n"]
|
|
||||||
|
|
||||||
if not is_video(filename):
|
|
||||||
continue
|
|
||||||
|
|
||||||
if "sample" in filename.lower():
|
|
||||||
continue
|
|
||||||
|
|
||||||
files[magnet["hash"]] = {
|
|
||||||
"index": magnet["files"].index(file),
|
|
||||||
"title": filename,
|
|
||||||
"size": file["s"],
|
|
||||||
}
|
|
||||||
|
|
||||||
break
|
|
||||||
|
|
||||||
return files
|
|
||||||
|
|
||||||
async def generate_download_link(self, hash: str, index: str):
|
|
||||||
try:
|
|
||||||
check_blacklisted = await self.session.get(
|
|
||||||
f"{self.api_url}/magnet/upload?agent=comet&magnets[]={hash}"
|
|
||||||
)
|
|
||||||
check_blacklisted = await check_blacklisted.text()
|
|
||||||
if "NO_SERVER" in check_blacklisted:
|
|
||||||
self.proxy = settings.DEBRID_PROXY_URL
|
|
||||||
if not self.proxy:
|
|
||||||
logger.warning(
|
|
||||||
"All-Debrid blacklisted server's IP. No proxy found."
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
logger.warning(
|
|
||||||
f"All-Debrid blacklisted server's IP. Switching to proxy {self.proxy} for {hash}|{index}"
|
|
||||||
)
|
|
||||||
|
|
||||||
upload_magnet = await self.session.get(
|
|
||||||
f"{self.api_url}/magnet/upload?agent=comet&magnets[]={hash}",
|
|
||||||
proxy=self.proxy,
|
|
||||||
)
|
|
||||||
upload_magnet = await upload_magnet.json()
|
|
||||||
|
|
||||||
get_magnet_status = await self.session.get(
|
|
||||||
f"{self.api_url}/magnet/status?agent=comet&id={upload_magnet['data']['magnets'][0]['id']}",
|
|
||||||
proxy=self.proxy,
|
|
||||||
)
|
|
||||||
get_magnet_status = await get_magnet_status.json()
|
|
||||||
|
|
||||||
unlock_link = await self.session.get(
|
|
||||||
f"{self.api_url}/link/unlock?agent=comet&link={get_magnet_status['data']['magnets']['links'][int(index)]['link']}",
|
|
||||||
proxy=self.proxy,
|
|
||||||
)
|
|
||||||
unlock_link = await unlock_link.json()
|
|
||||||
|
|
||||||
return unlock_link["data"]["link"]
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(
|
|
||||||
f"Exception while getting download link from All-Debrid for {hash}|{index}: {e}"
|
|
||||||
)
|
|
||||||
|
|||||||
+3
-135
@@ -1,140 +1,8 @@
|
|||||||
import aiohttp
|
import aiohttp
|
||||||
import asyncio
|
|
||||||
|
|
||||||
from RTN import parse
|
|
||||||
|
|
||||||
from comet.utils.general import is_video
|
|
||||||
from comet.utils.logger import logger
|
|
||||||
|
|
||||||
|
|
||||||
class DebridLink:
|
class DebridLink:
|
||||||
def __init__(self, session: aiohttp.ClientSession, debrid_api_key: str):
|
def __init__(
|
||||||
session.headers["Authorization"] = f"Bearer {debrid_api_key}"
|
self, session: aiohttp.ClientSession, video_id, debrid_api_key: str, ip: str
|
||||||
self.session = session
|
|
||||||
self.proxy = None
|
|
||||||
|
|
||||||
self.api_url = "https://debrid-link.com/api/v2"
|
|
||||||
|
|
||||||
async def check_premium(self):
|
|
||||||
try:
|
|
||||||
check_premium = await self.session.get(f"{self.api_url}/account/infos")
|
|
||||||
check_premium = await check_premium.text()
|
|
||||||
if '"accountType":1' in check_premium:
|
|
||||||
return True
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(
|
|
||||||
f"Exception while checking premium status on Debrid-Link: {e}"
|
|
||||||
)
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
async def get_instant(self, chunk: list):
|
|
||||||
responses = []
|
|
||||||
for hash in chunk:
|
|
||||||
try:
|
|
||||||
add_torrent = await self.session.post(
|
|
||||||
f"{self.api_url}/seedbox/add",
|
|
||||||
data={"url": hash, "wait": True, "async": True},
|
|
||||||
)
|
|
||||||
add_torrent = await add_torrent.json()
|
|
||||||
|
|
||||||
torrent_id = add_torrent["value"]["id"]
|
|
||||||
await self.session.delete(f"{self.api_url}/seedbox/{torrent_id}/remove")
|
|
||||||
|
|
||||||
responses.append(add_torrent)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
return responses
|
|
||||||
|
|
||||||
async def get_files(
|
|
||||||
self, torrent_hashes: list, type: str, season: str, episode: str, kitsu: bool, **kwargs
|
|
||||||
):
|
):
|
||||||
chunk_size = 10
|
pass
|
||||||
chunks = [
|
|
||||||
torrent_hashes[i : i + chunk_size]
|
|
||||||
for i in range(0, len(torrent_hashes), chunk_size)
|
|
||||||
]
|
|
||||||
|
|
||||||
tasks = []
|
|
||||||
for chunk in chunks:
|
|
||||||
tasks.append(self.get_instant(chunk))
|
|
||||||
|
|
||||||
responses = await asyncio.gather(*tasks)
|
|
||||||
|
|
||||||
availability = []
|
|
||||||
for response_list in responses:
|
|
||||||
for response in response_list:
|
|
||||||
availability.append(response)
|
|
||||||
|
|
||||||
files = {}
|
|
||||||
|
|
||||||
if type == "series":
|
|
||||||
for result in availability:
|
|
||||||
torrent_files = result["value"]["files"]
|
|
||||||
for file in torrent_files:
|
|
||||||
if file["downloadPercent"] != 100:
|
|
||||||
continue
|
|
||||||
|
|
||||||
filename = file["name"]
|
|
||||||
|
|
||||||
if not is_video(filename):
|
|
||||||
continue
|
|
||||||
|
|
||||||
if "sample" in filename.lower():
|
|
||||||
continue
|
|
||||||
|
|
||||||
filename_parsed = parse(filename)
|
|
||||||
if episode not in filename_parsed.episodes:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if kitsu:
|
|
||||||
if filename_parsed.seasons:
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
if season not in filename_parsed.seasons:
|
|
||||||
continue
|
|
||||||
|
|
||||||
files[result["value"]["hashString"]] = {
|
|
||||||
"index": torrent_files.index(file),
|
|
||||||
"title": filename,
|
|
||||||
"size": file["size"],
|
|
||||||
}
|
|
||||||
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
for result in availability:
|
|
||||||
value = result["value"]
|
|
||||||
torrent_files = value["files"]
|
|
||||||
for file in torrent_files:
|
|
||||||
if file["downloadPercent"] != 100:
|
|
||||||
continue
|
|
||||||
|
|
||||||
filename = file["name"]
|
|
||||||
|
|
||||||
if not is_video(filename):
|
|
||||||
continue
|
|
||||||
|
|
||||||
if "sample" in filename.lower():
|
|
||||||
continue
|
|
||||||
|
|
||||||
files[value["hashString"]] = {
|
|
||||||
"index": torrent_files.index(file),
|
|
||||||
"title": filename,
|
|
||||||
"size": file["size"],
|
|
||||||
}
|
|
||||||
|
|
||||||
return files
|
|
||||||
|
|
||||||
async def generate_download_link(self, hash: str, index: str):
|
|
||||||
try:
|
|
||||||
add_torrent = await self.session.post(
|
|
||||||
f"{self.api_url}/seedbox/add", data={"url": hash, "async": True}
|
|
||||||
)
|
|
||||||
add_torrent = await add_torrent.json()
|
|
||||||
|
|
||||||
return add_torrent["value"]["files"][int(index)]["downloadUrl"]
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(
|
|
||||||
f"Exception while getting download link from Debrid-Link for {hash}|{index}: {e}"
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import aiohttp
|
||||||
|
|
||||||
|
|
||||||
|
class EasyDebrid:
|
||||||
|
def __init__(
|
||||||
|
self, session: aiohttp.ClientSession, video_id, debrid_api_key: str, ip: str
|
||||||
|
):
|
||||||
|
pass
|
||||||
+96
-22
@@ -1,35 +1,109 @@
|
|||||||
import aiohttp
|
import aiohttp
|
||||||
|
|
||||||
from comet.utils.config import should_use_stremthru
|
|
||||||
|
|
||||||
from .realdebrid import RealDebrid
|
from .realdebrid import RealDebrid
|
||||||
from .alldebrid import AllDebrid
|
from .alldebrid import AllDebrid
|
||||||
from .premiumize import Premiumize
|
from .premiumize import Premiumize
|
||||||
from .torbox import TorBox
|
from .torbox import TorBox
|
||||||
from .debridlink import DebridLink
|
from .debridlink import DebridLink
|
||||||
|
from .torrent import Torrent
|
||||||
from .stremthru import StremThru
|
from .stremthru import StremThru
|
||||||
|
from .easydebrid import EasyDebrid
|
||||||
|
from .offcloud import Offcloud
|
||||||
|
from .pikpak import PikPak
|
||||||
|
|
||||||
|
debrid_services = {
|
||||||
|
"realdebrid": {
|
||||||
|
"extension": "RD",
|
||||||
|
"cache_availability_endpoint": False,
|
||||||
|
"class": RealDebrid,
|
||||||
|
},
|
||||||
|
"alldebrid": {
|
||||||
|
"extension": "AD",
|
||||||
|
"cache_availability_endpoint": False,
|
||||||
|
"class": AllDebrid,
|
||||||
|
},
|
||||||
|
"premiumize": {
|
||||||
|
"extension": "PM",
|
||||||
|
"cache_availability_endpoint": True,
|
||||||
|
"class": Premiumize,
|
||||||
|
},
|
||||||
|
"torbox": {"extension": "TB", "cache_availability_endpoint": True, "class": TorBox},
|
||||||
|
"debridlink": {
|
||||||
|
"extension": "DL",
|
||||||
|
"cache_availability_endpoint": False,
|
||||||
|
"class": DebridLink,
|
||||||
|
},
|
||||||
|
"stremthru": {
|
||||||
|
"extension": "ST",
|
||||||
|
"cache_availability_endpoint": True,
|
||||||
|
"class": StremThru,
|
||||||
|
},
|
||||||
|
"easydebrid": {
|
||||||
|
"extension": "ED",
|
||||||
|
"cache_availability_endpoint": True,
|
||||||
|
"class": EasyDebrid,
|
||||||
|
},
|
||||||
|
"offcloud": {
|
||||||
|
"extension": "OC",
|
||||||
|
"cache_availability_endpoint": False,
|
||||||
|
"class": Offcloud,
|
||||||
|
},
|
||||||
|
"pikpak": {
|
||||||
|
"extension": "PP",
|
||||||
|
"cache_availability_endpoint": False,
|
||||||
|
"class": PikPak,
|
||||||
|
},
|
||||||
|
"torrent": {
|
||||||
|
"extension": "TORRENT",
|
||||||
|
"cache_availability_endpoint": False,
|
||||||
|
"class": Torrent,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def getDebrid(session: aiohttp.ClientSession, config: dict, ip: str):
|
def get_debrid_extension(debrid_service: str):
|
||||||
debrid_service = config["debridService"]
|
original_extension = debrid_services[debrid_service]["extension"]
|
||||||
debrid_api_key = config["debridApiKey"]
|
|
||||||
|
|
||||||
if should_use_stremthru(config):
|
return original_extension
|
||||||
return StremThru(
|
|
||||||
session=session,
|
|
||||||
url=config["stremthruUrl"],
|
def build_stremthru_token(debrid_service: str, debrid_api_key: str):
|
||||||
debrid_service=debrid_service,
|
return f"{debrid_service}:{debrid_api_key}"
|
||||||
token=debrid_api_key,
|
|
||||||
ip=ip,
|
|
||||||
|
def get_debrid(
|
||||||
|
session: aiohttp.ClientSession,
|
||||||
|
video_id: str,
|
||||||
|
media_only_id: str,
|
||||||
|
debrid_service: str,
|
||||||
|
debrid_api_key: str,
|
||||||
|
ip: str,
|
||||||
|
):
|
||||||
|
if debrid_service != "torrent":
|
||||||
|
return debrid_services["stremthru"]["class"](
|
||||||
|
session,
|
||||||
|
video_id,
|
||||||
|
media_only_id,
|
||||||
|
build_stremthru_token(debrid_service, debrid_api_key),
|
||||||
|
ip,
|
||||||
)
|
)
|
||||||
|
|
||||||
if debrid_service == "realdebrid":
|
|
||||||
return RealDebrid(session, debrid_api_key, ip)
|
async def retrieve_debrid_availability(
|
||||||
elif debrid_service == "alldebrid":
|
session: aiohttp.ClientSession,
|
||||||
return AllDebrid(session, debrid_api_key)
|
video_id: str,
|
||||||
elif debrid_service == "premiumize":
|
media_only_id: str,
|
||||||
return Premiumize(session, debrid_api_key)
|
debrid_service: str,
|
||||||
elif debrid_service == "torbox":
|
debrid_api_key: str,
|
||||||
return TorBox(session, debrid_api_key)
|
ip: str,
|
||||||
elif debrid_service == "debridlink":
|
info_hashes: list,
|
||||||
return DebridLink(session, debrid_api_key)
|
seeders_map: dict,
|
||||||
|
tracker_map: dict,
|
||||||
|
sources_map: dict,
|
||||||
|
):
|
||||||
|
if debrid_service == "torrent":
|
||||||
|
return []
|
||||||
|
|
||||||
|
return await get_debrid(
|
||||||
|
session, video_id, media_only_id, debrid_service, debrid_api_key, ip
|
||||||
|
).get_availability(info_hashes, seeders_map, tracker_map, sources_map)
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import aiohttp
|
||||||
|
|
||||||
|
|
||||||
|
class Offcloud:
|
||||||
|
def __init__(
|
||||||
|
self, session: aiohttp.ClientSession, video_id, debrid_api_key: str, ip: str
|
||||||
|
):
|
||||||
|
pass
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import aiohttp
|
||||||
|
|
||||||
|
|
||||||
|
class PikPak:
|
||||||
|
def __init__(
|
||||||
|
self, session: aiohttp.ClientSession, video_id, debrid_api_key: str, ip: str
|
||||||
|
):
|
||||||
|
pass
|
||||||
+3
-172
@@ -1,177 +1,8 @@
|
|||||||
import aiohttp
|
import aiohttp
|
||||||
import asyncio
|
|
||||||
|
|
||||||
from RTN import parse
|
|
||||||
|
|
||||||
from comet.utils.general import is_video
|
|
||||||
from comet.utils.logger import logger
|
|
||||||
|
|
||||||
|
|
||||||
class Premiumize:
|
class Premiumize:
|
||||||
def __init__(self, session: aiohttp.ClientSession, debrid_api_key: str):
|
def __init__(
|
||||||
self.session = session
|
self, session: aiohttp.ClientSession, video_id, debrid_api_key: str, ip: str
|
||||||
self.proxy = None
|
|
||||||
|
|
||||||
self.api_url = "https://premiumize.me/api"
|
|
||||||
self.debrid_api_key = debrid_api_key
|
|
||||||
|
|
||||||
async def check_premium(self):
|
|
||||||
try:
|
|
||||||
check_premium = await self.session.get(
|
|
||||||
f"{self.api_url}/account/info?apikey={self.debrid_api_key}"
|
|
||||||
)
|
|
||||||
check_premium = await check_premium.text()
|
|
||||||
if (
|
|
||||||
'"status":"success"' in check_premium
|
|
||||||
and '"premium_until":null' not in check_premium
|
|
||||||
):
|
|
||||||
return True
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(
|
|
||||||
f"Exception while checking premium status on Premiumize: {e}"
|
|
||||||
)
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
async def get_instant(self, chunk: list):
|
|
||||||
try:
|
|
||||||
response = await self.session.get(
|
|
||||||
f"{self.api_url}/cache/check?apikey={self.debrid_api_key}&items[]={'&items[]='.join(chunk)}"
|
|
||||||
)
|
|
||||||
|
|
||||||
response = await response.json()
|
|
||||||
response["hashes"] = chunk
|
|
||||||
|
|
||||||
return response
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(
|
|
||||||
f"Exception while checking hash instant availability on Premiumize: {e}"
|
|
||||||
)
|
|
||||||
|
|
||||||
async def get_files(
|
|
||||||
self, torrent_hashes: list, type: str, season: str, episode: str, kitsu: bool, **kwargs
|
|
||||||
):
|
):
|
||||||
chunk_size = 100
|
pass
|
||||||
chunks = [
|
|
||||||
torrent_hashes[i : i + chunk_size]
|
|
||||||
for i in range(0, len(torrent_hashes), chunk_size)
|
|
||||||
]
|
|
||||||
|
|
||||||
tasks = []
|
|
||||||
for chunk in chunks:
|
|
||||||
tasks.append(self.get_instant(chunk))
|
|
||||||
|
|
||||||
responses = await asyncio.gather(*tasks)
|
|
||||||
|
|
||||||
availability = []
|
|
||||||
for response in responses:
|
|
||||||
if not response:
|
|
||||||
continue
|
|
||||||
|
|
||||||
availability.append(response)
|
|
||||||
|
|
||||||
files = {}
|
|
||||||
|
|
||||||
if type == "series":
|
|
||||||
for result in availability:
|
|
||||||
if result["status"] != "success":
|
|
||||||
continue
|
|
||||||
|
|
||||||
responses = result["response"]
|
|
||||||
filenames = result["filename"]
|
|
||||||
filesizes = result["filesize"]
|
|
||||||
hashes = result["hashes"]
|
|
||||||
for index, response in enumerate(responses):
|
|
||||||
if not response:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if not filesizes[index]:
|
|
||||||
continue
|
|
||||||
|
|
||||||
filename = filenames[index]
|
|
||||||
|
|
||||||
if "sample" in filename.lower():
|
|
||||||
continue
|
|
||||||
|
|
||||||
filename_parsed = parse(filename)
|
|
||||||
if episode not in filename_parsed.episodes:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if kitsu:
|
|
||||||
if filename_parsed.seasons:
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
if season not in filename_parsed.seasons:
|
|
||||||
continue
|
|
||||||
|
|
||||||
files[hashes[index]] = {
|
|
||||||
"index": f"{season}|{episode}",
|
|
||||||
"title": filename,
|
|
||||||
"size": int(filesizes[index]),
|
|
||||||
}
|
|
||||||
else:
|
|
||||||
for result in availability:
|
|
||||||
if result["status"] != "success":
|
|
||||||
continue
|
|
||||||
|
|
||||||
responses = result["response"]
|
|
||||||
filenames = result["filename"]
|
|
||||||
filesizes = result["filesize"]
|
|
||||||
hashes = result["hashes"]
|
|
||||||
for index, response in enumerate(responses):
|
|
||||||
if response is False:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if not filesizes[index]:
|
|
||||||
continue
|
|
||||||
|
|
||||||
filename = filenames[index]
|
|
||||||
|
|
||||||
if "sample" in filename.lower():
|
|
||||||
continue
|
|
||||||
|
|
||||||
files[hashes[index]] = {
|
|
||||||
"index": 0,
|
|
||||||
"title": filename,
|
|
||||||
"size": int(filesizes[index]),
|
|
||||||
}
|
|
||||||
|
|
||||||
return files
|
|
||||||
|
|
||||||
async def generate_download_link(self, hash: str, index: str):
|
|
||||||
try:
|
|
||||||
add_magnet = await self.session.post(
|
|
||||||
f"{self.api_url}/transfer/directdl?apikey={self.debrid_api_key}&src=magnet:?xt=urn:btih:{hash}",
|
|
||||||
)
|
|
||||||
add_magnet = await add_magnet.json()
|
|
||||||
|
|
||||||
season = None
|
|
||||||
if "|" in index:
|
|
||||||
index = index.split("|")
|
|
||||||
season = int(index[0])
|
|
||||||
episode = int(index[1])
|
|
||||||
|
|
||||||
content = add_magnet["content"]
|
|
||||||
for file in content:
|
|
||||||
filename = file["path"]
|
|
||||||
if "/" in filename:
|
|
||||||
filename = filename.split("/")[1]
|
|
||||||
|
|
||||||
if not is_video(filename):
|
|
||||||
content.remove(file)
|
|
||||||
continue
|
|
||||||
|
|
||||||
if season is not None:
|
|
||||||
filename_parsed = parse(filename)
|
|
||||||
if (
|
|
||||||
season in filename_parsed.seasons
|
|
||||||
and episode in filename_parsed.episodes
|
|
||||||
):
|
|
||||||
return file["link"]
|
|
||||||
|
|
||||||
max_size_item = max(content, key=lambda x: x["size"])
|
|
||||||
return max_size_item["link"]
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(
|
|
||||||
f"Exception while getting download link from Premiumize for {hash}|{index}: {e}"
|
|
||||||
)
|
|
||||||
|
|||||||
+3
-187
@@ -1,192 +1,8 @@
|
|||||||
import aiohttp
|
import aiohttp
|
||||||
import asyncio
|
|
||||||
|
|
||||||
from RTN import parse
|
|
||||||
|
|
||||||
from comet.utils.general import is_video
|
|
||||||
from comet.utils.logger import logger
|
|
||||||
from comet.utils.models import settings
|
|
||||||
|
|
||||||
|
|
||||||
class RealDebrid:
|
class RealDebrid:
|
||||||
def __init__(self, session: aiohttp.ClientSession, debrid_api_key: str, ip: str):
|
def __init__(
|
||||||
session.headers["Authorization"] = f"Bearer {debrid_api_key}"
|
self, session: aiohttp.ClientSession, video_id, debrid_api_key: str, ip: str
|
||||||
self.session = session
|
|
||||||
self.ip = ip
|
|
||||||
self.proxy = None
|
|
||||||
|
|
||||||
self.api_url = "https://api.real-debrid.com/rest/1.0"
|
|
||||||
|
|
||||||
async def check_premium(self):
|
|
||||||
try:
|
|
||||||
check_premium = await self.session.get(f"{self.api_url}/user")
|
|
||||||
check_premium = await check_premium.text()
|
|
||||||
if '"type": "premium"' in check_premium:
|
|
||||||
return True
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(
|
|
||||||
f"Exception while checking premium status on Real-Debrid: {e}"
|
|
||||||
)
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
async def get_instant(self, chunk: list):
|
|
||||||
try:
|
|
||||||
response = await self.session.get(
|
|
||||||
f"{self.api_url}/torrents/instantAvailability/{'/'.join(chunk)}"
|
|
||||||
)
|
|
||||||
return await response.json()
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(
|
|
||||||
f"Exception while checking hash instant availability on Real-Debrid: {e}"
|
|
||||||
)
|
|
||||||
|
|
||||||
async def get_files(
|
|
||||||
self, torrent_hashes: list, type: str, season: str, episode: str, kitsu: bool, **kwargs
|
|
||||||
):
|
):
|
||||||
chunk_size = 100
|
pass
|
||||||
chunks = [
|
|
||||||
torrent_hashes[i : i + chunk_size]
|
|
||||||
for i in range(0, len(torrent_hashes), chunk_size)
|
|
||||||
]
|
|
||||||
|
|
||||||
tasks = []
|
|
||||||
for chunk in chunks:
|
|
||||||
tasks.append(self.get_instant(chunk))
|
|
||||||
|
|
||||||
responses = await asyncio.gather(*tasks)
|
|
||||||
|
|
||||||
availability = {}
|
|
||||||
for response in responses:
|
|
||||||
if response is not None:
|
|
||||||
availability.update(response)
|
|
||||||
|
|
||||||
files = {}
|
|
||||||
|
|
||||||
if type == "series":
|
|
||||||
for hash, details in availability.items():
|
|
||||||
if "rd" not in details:
|
|
||||||
continue
|
|
||||||
|
|
||||||
for variants in details["rd"]:
|
|
||||||
for index, file in variants.items():
|
|
||||||
filename = file["filename"]
|
|
||||||
|
|
||||||
if not is_video(filename):
|
|
||||||
continue
|
|
||||||
|
|
||||||
if "sample" in filename.lower():
|
|
||||||
continue
|
|
||||||
|
|
||||||
filename_parsed = parse(filename)
|
|
||||||
if episode not in filename_parsed.episodes:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if kitsu:
|
|
||||||
if filename_parsed.seasons:
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
if season not in filename_parsed.seasons:
|
|
||||||
continue
|
|
||||||
|
|
||||||
files[hash] = {
|
|
||||||
"index": index,
|
|
||||||
"title": filename,
|
|
||||||
"size": file["filesize"],
|
|
||||||
}
|
|
||||||
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
for hash, details in availability.items():
|
|
||||||
if "rd" not in details:
|
|
||||||
continue
|
|
||||||
|
|
||||||
for variants in details["rd"]:
|
|
||||||
for index, file in variants.items():
|
|
||||||
filename = file["filename"]
|
|
||||||
|
|
||||||
if not is_video(filename):
|
|
||||||
continue
|
|
||||||
|
|
||||||
if "sample" in filename.lower():
|
|
||||||
continue
|
|
||||||
|
|
||||||
files[hash] = {
|
|
||||||
"index": index,
|
|
||||||
"title": filename,
|
|
||||||
"size": file["filesize"],
|
|
||||||
}
|
|
||||||
|
|
||||||
break
|
|
||||||
|
|
||||||
return files
|
|
||||||
|
|
||||||
async def generate_download_link(self, hash: str, index: str):
|
|
||||||
try:
|
|
||||||
check_blacklisted = await self.session.get("https://real-debrid.com/vpn")
|
|
||||||
check_blacklisted = await check_blacklisted.text()
|
|
||||||
if (
|
|
||||||
"Your ISP or VPN provider IP address is currently blocked on our website"
|
|
||||||
in check_blacklisted
|
|
||||||
):
|
|
||||||
self.proxy = settings.DEBRID_PROXY_URL
|
|
||||||
if not self.proxy:
|
|
||||||
logger.warning(
|
|
||||||
"Real-Debrid blacklisted server's IP. No proxy found."
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
logger.warning(
|
|
||||||
f"Real-Debrid blacklisted server's IP. Switching to proxy {self.proxy} for {hash}|{index}"
|
|
||||||
)
|
|
||||||
|
|
||||||
add_magnet = await self.session.post(
|
|
||||||
f"{self.api_url}/torrents/addMagnet",
|
|
||||||
data={"magnet": f"magnet:?xt=urn:btih:{hash}", "ip": self.ip},
|
|
||||||
proxy=self.proxy,
|
|
||||||
)
|
|
||||||
add_magnet = await add_magnet.json()
|
|
||||||
|
|
||||||
get_magnet_info = await self.session.get(
|
|
||||||
add_magnet["uri"], proxy=self.proxy
|
|
||||||
)
|
|
||||||
get_magnet_info = await get_magnet_info.json()
|
|
||||||
|
|
||||||
await self.session.post(
|
|
||||||
f"{self.api_url}/torrents/selectFiles/{add_magnet['id']}",
|
|
||||||
data={
|
|
||||||
"files": ",".join(
|
|
||||||
str(file["id"])
|
|
||||||
for file in get_magnet_info["files"]
|
|
||||||
if is_video(file["path"])
|
|
||||||
),
|
|
||||||
"ip": self.ip,
|
|
||||||
},
|
|
||||||
proxy=self.proxy,
|
|
||||||
)
|
|
||||||
|
|
||||||
get_magnet_info = await self.session.get(
|
|
||||||
add_magnet["uri"], proxy=self.proxy
|
|
||||||
)
|
|
||||||
get_magnet_info = await get_magnet_info.json()
|
|
||||||
|
|
||||||
index = int(index)
|
|
||||||
realIndex = index
|
|
||||||
for file in get_magnet_info["files"]:
|
|
||||||
if file["id"] == realIndex:
|
|
||||||
break
|
|
||||||
|
|
||||||
if file["selected"] != 1:
|
|
||||||
index -= 1
|
|
||||||
|
|
||||||
unrestrict_link = await self.session.post(
|
|
||||||
f"{self.api_url}/unrestrict/link",
|
|
||||||
data={"link": get_magnet_info["links"][index - 1], "ip": self.ip},
|
|
||||||
proxy=self.proxy,
|
|
||||||
)
|
|
||||||
unrestrict_link = await unrestrict_link.json()
|
|
||||||
|
|
||||||
return unrestrict_link["download"]
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(
|
|
||||||
f"Exception while getting download link from Real-Debrid for {hash}|{index}: {e}"
|
|
||||||
)
|
|
||||||
|
|||||||
+135
-100
@@ -1,59 +1,44 @@
|
|||||||
import asyncio
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
from RTN import parse
|
import asyncio
|
||||||
|
|
||||||
|
from RTN import parse, title_match
|
||||||
|
|
||||||
|
from comet.utils.models import settings
|
||||||
from comet.utils.general import is_video
|
from comet.utils.general import is_video
|
||||||
|
from comet.utils.debrid import cache_availability
|
||||||
from comet.utils.logger import logger
|
from comet.utils.logger import logger
|
||||||
|
from comet.utils.torrent import torrent_update_queue
|
||||||
|
|
||||||
|
|
||||||
class StremThru:
|
class StremThru:
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
session: aiohttp.ClientSession,
|
session: aiohttp.ClientSession,
|
||||||
url: str,
|
video_id: str,
|
||||||
|
media_only_id: str,
|
||||||
token: str,
|
token: str,
|
||||||
debrid_service: str,
|
|
||||||
ip: str,
|
ip: str,
|
||||||
):
|
):
|
||||||
if not self.is_supported_store(debrid_service):
|
store, token = self.parse_store_creds(token)
|
||||||
raise ValueError(f"unsupported store: {debrid_service}")
|
session.headers["X-StremThru-Store-Name"] = store
|
||||||
|
session.headers["X-StremThru-Store-Authorization"] = f"Bearer {token}"
|
||||||
store, token = self.parse_store_creds(debrid_service, token)
|
|
||||||
if store == "stremthru":
|
|
||||||
session.headers["Proxy-Authorization"] = f"Basic {token}"
|
|
||||||
else:
|
|
||||||
session.headers["X-StremThru-Store-Name"] = store
|
|
||||||
session.headers["X-StremThru-Store-Authorization"] = f"Bearer {token}"
|
|
||||||
|
|
||||||
session.headers["User-Agent"] = "comet"
|
session.headers["User-Agent"] = "comet"
|
||||||
|
|
||||||
self.session = session
|
self.session = session
|
||||||
self.base_url = f"{url}/v0/store"
|
self.base_url = f"{settings.STREMTHRU_URL}/v0/store"
|
||||||
self.name = f"StremThru[{debrid_service}]" if debrid_service else "StremThru"
|
self.name = f"StremThru-{store}"
|
||||||
|
self.real_debrid_name = store
|
||||||
self.client_ip = ip
|
self.client_ip = ip
|
||||||
|
self.sid = video_id
|
||||||
|
self.media_only_id = media_only_id
|
||||||
|
|
||||||
@staticmethod
|
def parse_store_creds(self, token: str):
|
||||||
def parse_store_creds(debrid_service, token: str = ""):
|
|
||||||
if debrid_service != "stremthru":
|
|
||||||
return debrid_service, token
|
|
||||||
if ":" in token:
|
if ":" in token:
|
||||||
parts = token.split(":")
|
parts = token.split(":", 1)
|
||||||
return parts[0], parts[1]
|
return parts[0], parts[1]
|
||||||
return debrid_service, token
|
|
||||||
|
|
||||||
@staticmethod
|
return token, ""
|
||||||
def is_supported_store(name: Optional[str]):
|
|
||||||
return (
|
|
||||||
name == "stremthru"
|
|
||||||
or name == "alldebrid"
|
|
||||||
or name == "debridlink"
|
|
||||||
or name == "easydebrid"
|
|
||||||
or name == "premiumize"
|
|
||||||
or name == "realdebrid"
|
|
||||||
or name == "torbox"
|
|
||||||
)
|
|
||||||
|
|
||||||
async def check_premium(self):
|
async def check_premium(self):
|
||||||
try:
|
try:
|
||||||
@@ -69,11 +54,9 @@ class StremThru:
|
|||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async def get_instant(self, magnets: list, sid: Optional[str] = None):
|
async def get_instant(self, magnets: list):
|
||||||
try:
|
try:
|
||||||
url = f"{self.base_url}/magnets/check?magnet={','.join(magnets)}&client_ip={self.client_ip}"
|
url = f"{self.base_url}/magnets/check?magnet={','.join(magnets)}&client_ip={self.client_ip}&sid={self.sid}"
|
||||||
if sid:
|
|
||||||
url = f"{url}&sid={sid}"
|
|
||||||
magnet = await self.session.get(url)
|
magnet = await self.session.get(url)
|
||||||
return await magnet.json()
|
return await magnet.json()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -81,17 +64,17 @@ class StremThru:
|
|||||||
f"Exception while checking hash instant availability on {self.name}: {e}"
|
f"Exception while checking hash instant availability on {self.name}: {e}"
|
||||||
)
|
)
|
||||||
|
|
||||||
async def get_files(
|
async def get_availability(
|
||||||
self,
|
self,
|
||||||
torrent_hashes: list,
|
torrent_hashes: list,
|
||||||
type: str,
|
seeders_map: dict,
|
||||||
season: str,
|
tracker_map: dict,
|
||||||
episode: str,
|
sources_map: dict,
|
||||||
kitsu: bool,
|
|
||||||
video_id: Optional[str] = None,
|
|
||||||
**kwargs,
|
|
||||||
):
|
):
|
||||||
chunk_size = 25
|
if not await self.check_premium():
|
||||||
|
return []
|
||||||
|
|
||||||
|
chunk_size = 50
|
||||||
chunks = [
|
chunks = [
|
||||||
torrent_hashes[i : i + chunk_size]
|
torrent_hashes[i : i + chunk_size]
|
||||||
for i in range(0, len(torrent_hashes), chunk_size)
|
for i in range(0, len(torrent_hashes), chunk_size)
|
||||||
@@ -99,7 +82,7 @@ class StremThru:
|
|||||||
|
|
||||||
tasks = []
|
tasks = []
|
||||||
for chunk in chunks:
|
for chunk in chunks:
|
||||||
tasks.append(self.get_instant(chunk, sid=video_id))
|
tasks.append(self.get_instant(chunk))
|
||||||
|
|
||||||
responses = await asyncio.gather(*tasks)
|
responses = await asyncio.gather(*tasks)
|
||||||
|
|
||||||
@@ -109,62 +92,83 @@ class StremThru:
|
|||||||
if response and "data" in response
|
if response and "data" in response
|
||||||
]
|
]
|
||||||
|
|
||||||
files = {}
|
is_offcloud = self.real_debrid_name == "offcloud"
|
||||||
|
|
||||||
if type == "series":
|
files = []
|
||||||
for magnets in availability:
|
cached_count = 0
|
||||||
for magnet in magnets:
|
for result in availability:
|
||||||
if magnet["status"] != "cached":
|
for torrent in result:
|
||||||
continue
|
if torrent["status"] != "cached":
|
||||||
|
continue
|
||||||
|
|
||||||
for file in magnet["files"]:
|
cached_count += 1
|
||||||
filename = file["name"]
|
hash = torrent["hash"]
|
||||||
|
seeders = seeders_map[hash]
|
||||||
|
tracker = tracker_map[hash]
|
||||||
|
sources = sources_map[hash]
|
||||||
|
|
||||||
if not is_video(filename) or "sample" in filename:
|
if is_offcloud:
|
||||||
|
file_info = {
|
||||||
|
"info_hash": hash,
|
||||||
|
"index": None,
|
||||||
|
"title": None,
|
||||||
|
"size": None,
|
||||||
|
"season": None,
|
||||||
|
"episode": None,
|
||||||
|
"parsed": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
files.append(file_info)
|
||||||
|
else:
|
||||||
|
for file in torrent["files"]:
|
||||||
|
filename = file["name"].split("/")[-1]
|
||||||
|
|
||||||
|
if not is_video(filename) or "sample" in filename.lower():
|
||||||
continue
|
continue
|
||||||
|
|
||||||
filename_parsed = parse(filename)
|
filename_parsed = parse(filename)
|
||||||
|
|
||||||
if episode not in filename_parsed.episodes:
|
season = (
|
||||||
|
filename_parsed.seasons[0]
|
||||||
|
if filename_parsed.seasons
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
episode = (
|
||||||
|
filename_parsed.episodes[0]
|
||||||
|
if filename_parsed.episodes
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
if ":" in self.sid and (season is None or episode is None):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if kitsu:
|
index = file["index"] if file["index"] != -1 else None
|
||||||
if filename_parsed.seasons:
|
size = file["size"] if file["size"] != -1 else None
|
||||||
continue
|
|
||||||
else:
|
|
||||||
if season not in filename_parsed.seasons:
|
|
||||||
continue
|
|
||||||
|
|
||||||
files[magnet["hash"]] = {
|
file_info = {
|
||||||
"index": file["index"],
|
"info_hash": hash,
|
||||||
|
"index": index,
|
||||||
"title": filename,
|
"title": filename,
|
||||||
"size": file["size"],
|
"size": size,
|
||||||
|
"season": season,
|
||||||
|
"episode": episode,
|
||||||
|
"parsed": filename_parsed,
|
||||||
|
"seeders": seeders,
|
||||||
|
"tracker": tracker,
|
||||||
|
"sources": sources,
|
||||||
}
|
}
|
||||||
|
|
||||||
break
|
files.append(file_info)
|
||||||
else:
|
await torrent_update_queue.add_torrent_info(file_info, self.media_only_id)
|
||||||
for magnets in availability:
|
|
||||||
for magnet in magnets:
|
|
||||||
if magnet["status"] != "cached":
|
|
||||||
continue
|
|
||||||
|
|
||||||
for file in magnet["files"]:
|
|
||||||
filename = file["name"]
|
|
||||||
|
|
||||||
if not is_video(filename) or "sample" in filename:
|
|
||||||
continue
|
|
||||||
|
|
||||||
files[magnet["hash"]] = {
|
|
||||||
"index": file["index"],
|
|
||||||
"title": filename,
|
|
||||||
"size": file["size"],
|
|
||||||
}
|
|
||||||
|
|
||||||
break
|
|
||||||
|
|
||||||
|
logger.log(
|
||||||
|
"SCRAPER",
|
||||||
|
f"{self.name}: Found {cached_count} cached torrents with {len(files)} valid files",
|
||||||
|
)
|
||||||
return files
|
return files
|
||||||
|
|
||||||
async def generate_download_link(self, hash: str, index: str):
|
async def generate_download_link(
|
||||||
|
self, hash: str, index: str, name: str, season: int, episode: int
|
||||||
|
):
|
||||||
try:
|
try:
|
||||||
magnet = await self.session.post(
|
magnet = await self.session.post(
|
||||||
f"{self.base_url}/magnets?client_ip={self.client_ip}",
|
f"{self.base_url}/magnets?client_ip={self.client_ip}",
|
||||||
@@ -175,26 +179,57 @@ class StremThru:
|
|||||||
if magnet["data"]["status"] != "downloaded":
|
if magnet["data"]["status"] != "downloaded":
|
||||||
return
|
return
|
||||||
|
|
||||||
file = next(
|
name_parsed = parse(name)
|
||||||
(
|
target_file = None
|
||||||
file
|
|
||||||
for file in magnet["data"]["files"]
|
|
||||||
if str(file["index"]) == index or file["name"] == index
|
|
||||||
),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
|
|
||||||
if not file:
|
files = []
|
||||||
|
for file in magnet["data"]["files"]:
|
||||||
|
filename = file["name"]
|
||||||
|
filename_parsed = parse(filename)
|
||||||
|
|
||||||
|
if not is_video(filename) or not title_match(
|
||||||
|
name_parsed.parsed_title, filename_parsed.parsed_title
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
|
||||||
|
file_season = (
|
||||||
|
filename_parsed.seasons[0] if filename_parsed.seasons else None
|
||||||
|
)
|
||||||
|
file_episode = (
|
||||||
|
filename_parsed.episodes[0] if filename_parsed.episodes else None
|
||||||
|
)
|
||||||
|
file_index = file["index"] if file["index"] != -1 else None
|
||||||
|
file_size = file["size"] if file["size"] != -1 else None
|
||||||
|
|
||||||
|
file_info = {
|
||||||
|
"info_hash": hash,
|
||||||
|
"index": file_index,
|
||||||
|
"title": filename,
|
||||||
|
"size": file_size,
|
||||||
|
"season": file_season,
|
||||||
|
"episode": file_episode,
|
||||||
|
"parsed": filename_parsed,
|
||||||
|
}
|
||||||
|
files.append(file_info)
|
||||||
|
|
||||||
|
if str(file["index"]) == index:
|
||||||
|
target_file = file
|
||||||
|
|
||||||
|
if season == file_season and episode == file_episode:
|
||||||
|
target_file = file
|
||||||
|
|
||||||
|
if len(files) > 0:
|
||||||
|
asyncio.create_task(cache_availability(self.real_debrid_name, files))
|
||||||
|
|
||||||
|
if not target_file:
|
||||||
return
|
return
|
||||||
|
|
||||||
link = await self.session.post(
|
link = await self.session.post(
|
||||||
f"{self.base_url}/link/generate?client_ip={self.client_ip}",
|
f"{self.base_url}/link/generate?client_ip={self.client_ip}",
|
||||||
json={"link": file["link"]},
|
json={"link": target_file["link"]},
|
||||||
)
|
)
|
||||||
link = await link.json()
|
link = await link.json()
|
||||||
|
|
||||||
return link["data"]["link"]
|
return link["data"]["link"]
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(
|
logger.warning(f"Exception while getting download link for {hash}: {e}")
|
||||||
f"Exception while getting download link from {self.name} for {hash}|{index}: {e}"
|
|
||||||
)
|
|
||||||
|
|||||||
+3
-156
@@ -1,161 +1,8 @@
|
|||||||
import aiohttp
|
import aiohttp
|
||||||
import asyncio
|
|
||||||
|
|
||||||
from RTN import parse
|
|
||||||
|
|
||||||
from comet.utils.general import is_video
|
|
||||||
from comet.utils.logger import logger
|
|
||||||
|
|
||||||
|
|
||||||
class TorBox:
|
class TorBox:
|
||||||
def __init__(self, session: aiohttp.ClientSession, debrid_api_key: str):
|
def __init__(
|
||||||
session.headers["Authorization"] = f"Bearer {debrid_api_key}"
|
self, session: aiohttp.ClientSession, video_id, debrid_api_key: str, ip: str
|
||||||
self.session = session
|
|
||||||
self.proxy = None
|
|
||||||
|
|
||||||
self.api_url = "https://api.torbox.app/v1/api"
|
|
||||||
self.debrid_api_key = debrid_api_key
|
|
||||||
|
|
||||||
async def check_premium(self):
|
|
||||||
try:
|
|
||||||
check_premium = await self.session.get(
|
|
||||||
f"{self.api_url}/user/me?settings=false"
|
|
||||||
)
|
|
||||||
check_premium = await check_premium.text()
|
|
||||||
if '"success":true' in check_premium:
|
|
||||||
return True
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"Exception while checking premium status on TorBox: {e}")
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
async def get_instant(self, chunk: list):
|
|
||||||
try:
|
|
||||||
response = await self.session.get(
|
|
||||||
f"{self.api_url}/torrents/checkcached?hash={','.join(chunk)}&format=list&list_files=true"
|
|
||||||
)
|
|
||||||
return await response.json()
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(
|
|
||||||
f"Exception while checking hash instant availability on TorBox: {e}"
|
|
||||||
)
|
|
||||||
|
|
||||||
async def get_files(
|
|
||||||
self, torrent_hashes: list, type: str, season: str, episode: str, kitsu: bool, **kwargs
|
|
||||||
):
|
):
|
||||||
chunk_size = 50
|
pass
|
||||||
chunks = [
|
|
||||||
torrent_hashes[i : i + chunk_size]
|
|
||||||
for i in range(0, len(torrent_hashes), chunk_size)
|
|
||||||
]
|
|
||||||
|
|
||||||
tasks = []
|
|
||||||
for chunk in chunks:
|
|
||||||
tasks.append(self.get_instant(chunk))
|
|
||||||
|
|
||||||
responses = await asyncio.gather(*tasks)
|
|
||||||
|
|
||||||
availability = [response for response in responses if response is not None]
|
|
||||||
|
|
||||||
files = {}
|
|
||||||
|
|
||||||
if type == "series":
|
|
||||||
for result in availability:
|
|
||||||
if not result["success"] or not result["data"]:
|
|
||||||
continue
|
|
||||||
|
|
||||||
for torrent in result["data"]:
|
|
||||||
torrent_files = torrent["files"]
|
|
||||||
for file in torrent_files:
|
|
||||||
filename = file["name"].split("/")[1]
|
|
||||||
|
|
||||||
if not is_video(filename):
|
|
||||||
continue
|
|
||||||
|
|
||||||
if "sample" in filename.lower():
|
|
||||||
continue
|
|
||||||
|
|
||||||
filename_parsed = parse(filename)
|
|
||||||
if episode not in filename_parsed.episodes:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if kitsu:
|
|
||||||
if filename_parsed.seasons:
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
if season not in filename_parsed.seasons:
|
|
||||||
continue
|
|
||||||
|
|
||||||
files[torrent["hash"]] = {
|
|
||||||
"index": torrent_files.index(file),
|
|
||||||
"title": filename,
|
|
||||||
"size": file["size"],
|
|
||||||
}
|
|
||||||
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
for result in availability:
|
|
||||||
if not result["success"] or not result["data"]:
|
|
||||||
continue
|
|
||||||
|
|
||||||
for torrent in result["data"]:
|
|
||||||
torrent_files = torrent["files"]
|
|
||||||
for file in torrent_files:
|
|
||||||
filename = file["name"].split("/")[1]
|
|
||||||
|
|
||||||
if not is_video(filename):
|
|
||||||
continue
|
|
||||||
|
|
||||||
if "sample" in filename.lower():
|
|
||||||
continue
|
|
||||||
|
|
||||||
files[torrent["hash"]] = {
|
|
||||||
"index": torrent_files.index(file),
|
|
||||||
"title": filename,
|
|
||||||
"size": file["size"],
|
|
||||||
}
|
|
||||||
|
|
||||||
break
|
|
||||||
|
|
||||||
return files
|
|
||||||
|
|
||||||
async def generate_download_link(self, hash: str, index: str):
|
|
||||||
try:
|
|
||||||
get_torrents = await self.session.get(
|
|
||||||
f"{self.api_url}/torrents/mylist?bypass_cache=true"
|
|
||||||
)
|
|
||||||
get_torrents = await get_torrents.json()
|
|
||||||
exists = False
|
|
||||||
for torrent in get_torrents["data"]:
|
|
||||||
if torrent["hash"] == hash:
|
|
||||||
torrent_id = torrent["id"]
|
|
||||||
exists = True
|
|
||||||
break
|
|
||||||
if not exists:
|
|
||||||
create_torrent = await self.session.post(
|
|
||||||
f"{self.api_url}/torrents/createtorrent",
|
|
||||||
data={"magnet": f"magnet:?xt=urn:btih:{hash}"},
|
|
||||||
)
|
|
||||||
create_torrent = await create_torrent.json()
|
|
||||||
torrent_id = create_torrent["data"]["torrent_id"]
|
|
||||||
|
|
||||||
# get_torrents = await self.session.get(
|
|
||||||
# f"{self.api_url}/torrents/mylist?bypass_cache=true"
|
|
||||||
# )
|
|
||||||
# get_torrents = await get_torrents.json()
|
|
||||||
|
|
||||||
# for torrent in get_torrents["data"]:
|
|
||||||
# if torrent["id"] == torrent_id:
|
|
||||||
# file_id = torrent["files"][int(index)]["id"]
|
|
||||||
# Useless, we already have file index
|
|
||||||
|
|
||||||
get_download_link = await self.session.get(
|
|
||||||
f"{self.api_url}/torrents/requestdl?token={self.debrid_api_key}&torrent_id={torrent_id}&file_id={index}&zip=false",
|
|
||||||
)
|
|
||||||
get_download_link = await get_download_link.json()
|
|
||||||
|
|
||||||
return get_download_link["data"]
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(
|
|
||||||
f"Exception while getting download link from TorBox for {hash}|{index}: {e}"
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
class Torrent:
|
||||||
|
def __init__(self):
|
||||||
|
pass
|
||||||
+236
-177
@@ -1,177 +1,236 @@
|
|||||||
import contextlib
|
import contextlib
|
||||||
import signal
|
import signal
|
||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
from contextlib import asynccontextmanager
|
import uvicorn
|
||||||
|
import os
|
||||||
import uvicorn
|
|
||||||
from fastapi import FastAPI
|
from contextlib import asynccontextmanager
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi import FastAPI
|
||||||
from starlette.middleware.base import BaseHTTPMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from starlette.requests import Request
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
from starlette.middleware.base import BaseHTTPMiddleware
|
||||||
from comet.api.core import main
|
from starlette.requests import Request
|
||||||
from comet.api.stream import streams
|
|
||||||
from comet.utils.db import setup_database, teardown_database
|
from comet.api.core import main
|
||||||
from comet.utils.logger import logger
|
from comet.api.stream import streams
|
||||||
from comet.utils.models import settings
|
from comet.utils.database import setup_database, teardown_database
|
||||||
|
from comet.utils.trackers import download_best_trackers
|
||||||
|
from comet.utils.logger import logger
|
||||||
class LoguruMiddleware(BaseHTTPMiddleware):
|
from comet.utils.models import settings
|
||||||
async def dispatch(self, request: Request, call_next):
|
|
||||||
start_time = time.time()
|
|
||||||
try:
|
class LoguruMiddleware(BaseHTTPMiddleware):
|
||||||
response = await call_next(request)
|
async def dispatch(self, request: Request, call_next):
|
||||||
except Exception as e:
|
start_time = time.time()
|
||||||
logger.exception(f"Exception during request processing: {e}")
|
try:
|
||||||
raise
|
response = await call_next(request)
|
||||||
finally:
|
except Exception as e:
|
||||||
process_time = time.time() - start_time
|
logger.exception(f"Exception during request processing: {e}")
|
||||||
logger.log(
|
raise
|
||||||
"API",
|
finally:
|
||||||
f"{request.method} {request.url.path} - {response.status_code if 'response' in locals() else '500'} - {process_time:.2f}s",
|
process_time = time.time() - start_time
|
||||||
)
|
logger.log(
|
||||||
return response
|
"API",
|
||||||
|
f"{request.method} {request.url.path} - {response.status_code if 'response' in locals() else '500'} - {process_time:.2f}s",
|
||||||
|
)
|
||||||
@asynccontextmanager
|
return response
|
||||||
async def lifespan(app: FastAPI):
|
|
||||||
await setup_database()
|
|
||||||
yield
|
@asynccontextmanager
|
||||||
await teardown_database()
|
async def lifespan(app: FastAPI):
|
||||||
|
await setup_database()
|
||||||
|
await download_best_trackers()
|
||||||
app = FastAPI(
|
yield
|
||||||
title="Comet",
|
await teardown_database()
|
||||||
summary="Stremio's fastest torrent/debrid search add-on.",
|
|
||||||
version="1.0.0",
|
|
||||||
lifespan=lifespan,
|
app = FastAPI(
|
||||||
redoc_url=None,
|
title="Comet",
|
||||||
)
|
summary="Stremio's fastest torrent/debrid search add-on.",
|
||||||
|
lifespan=lifespan,
|
||||||
|
redoc_url=None,
|
||||||
app.add_middleware(LoguruMiddleware)
|
)
|
||||||
app.add_middleware(
|
|
||||||
CORSMiddleware,
|
|
||||||
allow_origins=["*"],
|
app.add_middleware(LoguruMiddleware)
|
||||||
allow_credentials=True,
|
app.add_middleware(
|
||||||
allow_methods=["*"],
|
CORSMiddleware,
|
||||||
allow_headers=["*"],
|
allow_origins=["*"],
|
||||||
)
|
allow_credentials=True,
|
||||||
|
allow_methods=["*"],
|
||||||
app.mount("/static", StaticFiles(directory="comet/templates"), name="static")
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
app.include_router(main)
|
|
||||||
app.include_router(streams)
|
app.mount("/static", StaticFiles(directory="comet/templates"), name="static")
|
||||||
|
|
||||||
|
app.include_router(main)
|
||||||
class Server(uvicorn.Server):
|
app.include_router(streams)
|
||||||
def install_signal_handlers(self):
|
|
||||||
pass
|
|
||||||
|
class Server(uvicorn.Server):
|
||||||
@contextlib.contextmanager
|
def install_signal_handlers(self):
|
||||||
def run_in_thread(self):
|
pass
|
||||||
thread = threading.Thread(target=self.run, name="Comet")
|
|
||||||
thread.start()
|
@contextlib.contextmanager
|
||||||
try:
|
def run_in_thread(self):
|
||||||
while not self.started:
|
thread = threading.Thread(target=self.run, name="Comet")
|
||||||
time.sleep(1e-3)
|
thread.start()
|
||||||
yield
|
try:
|
||||||
except Exception as e:
|
while not self.started:
|
||||||
logger.error(f"Error in server thread: {e}")
|
time.sleep(1e-3)
|
||||||
logger.exception(traceback.format_exc())
|
yield
|
||||||
raise e
|
except Exception as e:
|
||||||
finally:
|
logger.error(f"Error in server thread: {e}")
|
||||||
self.should_exit = True
|
logger.exception(traceback.format_exc())
|
||||||
sys.exit(0)
|
raise e
|
||||||
|
finally:
|
||||||
|
self.should_exit = True
|
||||||
def signal_handler(sig, frame):
|
sys.exit(0)
|
||||||
# This will handle kubernetes/docker shutdowns better
|
|
||||||
# Toss anything that needs to be gracefully shutdown here
|
|
||||||
logger.log("COMET", "Exiting Gracefully.")
|
def signal_handler(sig, frame):
|
||||||
sys.exit(0)
|
# This will handle kubernetes/docker shutdowns better
|
||||||
|
# Toss anything that needs to be gracefully shutdown here
|
||||||
|
logger.log("COMET", "Exiting Gracefully.")
|
||||||
signal.signal(signal.SIGINT, signal_handler)
|
sys.exit(0)
|
||||||
signal.signal(signal.SIGTERM, signal_handler)
|
|
||||||
|
|
||||||
config = uvicorn.Config(
|
signal.signal(signal.SIGINT, signal_handler)
|
||||||
app,
|
signal.signal(signal.SIGTERM, signal_handler)
|
||||||
host=settings.FASTAPI_HOST,
|
|
||||||
port=settings.FASTAPI_PORT,
|
|
||||||
proxy_headers=True,
|
def start_log():
|
||||||
forwarded_allow_ips="*",
|
logger.log(
|
||||||
workers=settings.FASTAPI_WORKERS,
|
"COMET",
|
||||||
log_config=None,
|
f"Server started on http://{settings.FASTAPI_HOST}:{settings.FASTAPI_PORT} - {settings.FASTAPI_WORKERS} workers",
|
||||||
)
|
)
|
||||||
server = Server(config=config)
|
logger.log(
|
||||||
|
"COMET",
|
||||||
|
f"Dashboard Admin Password: {settings.DASHBOARD_ADMIN_PASSWORD} - http://{settings.FASTAPI_HOST}:{settings.FASTAPI_PORT}/dashboard",
|
||||||
def start_log():
|
)
|
||||||
logger.log(
|
logger.log(
|
||||||
"COMET",
|
"COMET",
|
||||||
f"Server started on http://{settings.FASTAPI_HOST}:{settings.FASTAPI_PORT} - {settings.FASTAPI_WORKERS} workers",
|
f"Database ({settings.DATABASE_TYPE}): {settings.DATABASE_PATH if settings.DATABASE_TYPE == 'sqlite' else settings.DATABASE_URL} - TTL: metadata={settings.METADATA_CACHE_TTL}s, torrents={settings.TORRENT_CACHE_TTL}s, debrid={settings.DEBRID_CACHE_TTL}s",
|
||||||
)
|
)
|
||||||
logger.log(
|
logger.log("COMET", f"Debrid Proxy: {settings.DEBRID_PROXY_URL}")
|
||||||
"COMET",
|
|
||||||
f"Dashboard Admin Password: {settings.DASHBOARD_ADMIN_PASSWORD} - http://{settings.FASTAPI_HOST}:{settings.FASTAPI_PORT}/active-connections?password={settings.DASHBOARD_ADMIN_PASSWORD}",
|
if settings.INDEXER_MANAGER_TYPE:
|
||||||
)
|
logger.log(
|
||||||
logger.log(
|
"COMET",
|
||||||
"COMET",
|
f"Indexer Manager: {settings.INDEXER_MANAGER_TYPE}|{settings.INDEXER_MANAGER_URL} - Timeout: {settings.INDEXER_MANAGER_TIMEOUT}s",
|
||||||
f"Database ({settings.DATABASE_TYPE}): {settings.DATABASE_PATH if settings.DATABASE_TYPE == 'sqlite' else settings.DATABASE_URL} - TTL: {settings.CACHE_TTL}s",
|
)
|
||||||
)
|
logger.log("COMET", f"Indexers: {', '.join(settings.INDEXER_MANAGER_INDEXERS)}")
|
||||||
logger.log("COMET", f"Debrid Proxy: {settings.DEBRID_PROXY_URL}")
|
logger.log("COMET", f"Get Torrent Timeout: {settings.GET_TORRENT_TIMEOUT}s")
|
||||||
|
logger.log(
|
||||||
if settings.INDEXER_MANAGER_TYPE:
|
"COMET", f"Download Torrent Files: {bool(settings.DOWNLOAD_TORRENT_FILES)}"
|
||||||
logger.log(
|
)
|
||||||
"COMET",
|
else:
|
||||||
f"Indexer Manager: {settings.INDEXER_MANAGER_TYPE}|{settings.INDEXER_MANAGER_URL} - Timeout: {settings.INDEXER_MANAGER_TIMEOUT}s",
|
logger.log("COMET", "Indexer Manager: False")
|
||||||
)
|
|
||||||
logger.log("COMET", f"Indexers: {', '.join(settings.INDEXER_MANAGER_INDEXERS)}")
|
zilean_url = f" - {settings.ZILEAN_URL}"
|
||||||
logger.log("COMET", f"Get Torrent Timeout: {settings.GET_TORRENT_TIMEOUT}s")
|
logger.log(
|
||||||
else:
|
"COMET",
|
||||||
logger.log("COMET", "Indexer Manager: False")
|
f"Zilean Scraper: {bool(settings.SCRAPE_ZILEAN)}{zilean_url if settings.SCRAPE_ZILEAN else ''}",
|
||||||
|
)
|
||||||
if settings.ZILEAN_URL:
|
|
||||||
logger.log(
|
torrentio_url = f" - {settings.TORRENTIO_URL}"
|
||||||
"COMET",
|
logger.log(
|
||||||
f"Zilean: {settings.ZILEAN_URL} - Take first: {settings.ZILEAN_TAKE_FIRST}",
|
"COMET",
|
||||||
)
|
f"Torrentio Scraper: {bool(settings.SCRAPE_TORRENTIO)}{torrentio_url if settings.SCRAPE_TORRENTIO else ''}",
|
||||||
else:
|
)
|
||||||
logger.log("COMET", "Zilean: False")
|
|
||||||
|
mediafusion_url = f" - {settings.MEDIAFUSION_URL}"
|
||||||
logger.log("COMET", f"Torrentio Scraper: {bool(settings.SCRAPE_TORRENTIO)}")
|
logger.log(
|
||||||
|
"COMET",
|
||||||
mediafusion_url = f" - {settings.MEDIAFUSION_URL}"
|
f"MediaFusion Scraper: {bool(settings.SCRAPE_MEDIAFUSION)}{mediafusion_url if settings.SCRAPE_MEDIAFUSION else ''}",
|
||||||
logger.log(
|
)
|
||||||
"COMET",
|
|
||||||
f"MediaFusion Scraper: {bool(settings.SCRAPE_MEDIAFUSION)}{mediafusion_url if settings.SCRAPE_MEDIAFUSION else ''}",
|
logger.log(
|
||||||
)
|
"COMET",
|
||||||
|
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}",
|
||||||
logger.log(
|
)
|
||||||
"COMET",
|
|
||||||
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}",
|
logger.log("COMET", f"StremThru URL: {settings.STREMTHRU_URL}")
|
||||||
)
|
|
||||||
if settings.STREMTHRU_DEFAULT_URL:
|
logger.log("COMET", f"Remove Adult Content: {bool(settings.REMOVE_ADULT_CONTENT)}")
|
||||||
logger.log("COMET", f"Default StremThru URL: {settings.STREMTHRU_DEFAULT_URL}")
|
logger.log("COMET", f"Custom Header HTML: {bool(settings.CUSTOM_HEADER_HTML)}")
|
||||||
logger.log("COMET", f"Title Match Check: {bool(settings.TITLE_MATCH_CHECK)}")
|
|
||||||
logger.log("COMET", f"Remove Adult Content: {bool(settings.REMOVE_ADULT_CONTENT)}")
|
|
||||||
logger.log("COMET", f"Custom Header HTML: {bool(settings.CUSTOM_HEADER_HTML)}")
|
def run_with_uvicorn():
|
||||||
|
"""Run the server with uvicorn only"""
|
||||||
|
config = uvicorn.Config(
|
||||||
with server.run_in_thread():
|
app,
|
||||||
start_log()
|
host=settings.FASTAPI_HOST,
|
||||||
try:
|
port=settings.FASTAPI_PORT,
|
||||||
while True:
|
proxy_headers=True,
|
||||||
time.sleep(1) # Keep the main thread alive
|
forwarded_allow_ips="*",
|
||||||
except KeyboardInterrupt:
|
workers=settings.FASTAPI_WORKERS,
|
||||||
logger.log("COMET", "Server stopped by user")
|
log_config=None,
|
||||||
except Exception as e:
|
)
|
||||||
logger.error(f"Unexpected error: {e}")
|
server = Server(config=config)
|
||||||
logger.exception(traceback.format_exc())
|
|
||||||
finally:
|
with server.run_in_thread():
|
||||||
logger.log("COMET", "Server Shutdown")
|
start_log()
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
time.sleep(1) # Keep the main thread alive
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
logger.log("COMET", "Server stopped by user")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Unexpected error: {e}")
|
||||||
|
logger.exception(traceback.format_exc())
|
||||||
|
finally:
|
||||||
|
logger.log("COMET", "Server Shutdown")
|
||||||
|
|
||||||
|
|
||||||
|
def run_with_gunicorn():
|
||||||
|
"""Run the server with gunicorn and uvicorn workers"""
|
||||||
|
import gunicorn.app.base
|
||||||
|
|
||||||
|
class StandaloneApplication(gunicorn.app.base.BaseApplication):
|
||||||
|
def __init__(self, app, options=None):
|
||||||
|
self.options = options or {}
|
||||||
|
self.application = app
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
def load_config(self):
|
||||||
|
config = {
|
||||||
|
key: value for key, value in self.options.items()
|
||||||
|
if key in self.cfg.settings and value is not None
|
||||||
|
}
|
||||||
|
for key, value in config.items():
|
||||||
|
self.cfg.set(key.lower(), value)
|
||||||
|
|
||||||
|
def load(self):
|
||||||
|
return self.application
|
||||||
|
|
||||||
|
workers = settings.FASTAPI_WORKERS
|
||||||
|
if workers <= 1:
|
||||||
|
workers = (os.cpu_count() or 1) * 2 + 1
|
||||||
|
|
||||||
|
options = {
|
||||||
|
"bind": f"{settings.FASTAPI_HOST}:{settings.FASTAPI_PORT}",
|
||||||
|
"workers": workers,
|
||||||
|
"worker_class": "uvicorn.workers.UvicornWorker",
|
||||||
|
"timeout": 120,
|
||||||
|
"keepalive": 5,
|
||||||
|
"preload_app": True,
|
||||||
|
"proxy_protocol": True,
|
||||||
|
"forwarded_allow_ips": "*",
|
||||||
|
}
|
||||||
|
|
||||||
|
start_log()
|
||||||
|
logger.log("COMET", f"Starting with gunicorn using {workers} workers")
|
||||||
|
|
||||||
|
StandaloneApplication(app, options).run()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
if os.name == "nt" or not settings.USE_GUNICORN:
|
||||||
|
run_with_uvicorn()
|
||||||
|
else:
|
||||||
|
run_with_gunicorn()
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import aiohttp
|
||||||
|
|
||||||
|
from comet.utils.logger import logger
|
||||||
|
|
||||||
|
|
||||||
|
async def get_imdb_metadata(session: aiohttp.ClientSession, id: str):
|
||||||
|
try:
|
||||||
|
response = await session.get(
|
||||||
|
f"https://v3.sg.media-imdb.com/suggestion/a/{id}.json"
|
||||||
|
)
|
||||||
|
metadata = await response.json()
|
||||||
|
for element in metadata["d"]:
|
||||||
|
if "/" not in element["id"]:
|
||||||
|
title = element["l"]
|
||||||
|
year = element.get("y")
|
||||||
|
year_end = int(element["yr"].split("-")[1]) if "yr" in element else None
|
||||||
|
return title, year, year_end
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Exception while getting IMDB metadata for {id}: {e}")
|
||||||
|
return None, None, None, None, None
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import aiohttp
|
||||||
|
|
||||||
|
from comet.utils.logger import logger
|
||||||
|
|
||||||
|
|
||||||
|
async def get_kitsu_metadata(session: aiohttp.ClientSession, id: str):
|
||||||
|
try:
|
||||||
|
response = await session.get(f"https://kitsu.io/api/edge/anime/{id}")
|
||||||
|
metadata = await response.json()
|
||||||
|
|
||||||
|
attributes = metadata["data"]["attributes"]
|
||||||
|
year = int(attributes["createdAt"].split("-")[0])
|
||||||
|
year_end = int(attributes["updatedAt"].split("-")[0])
|
||||||
|
|
||||||
|
return attributes["canonicalTitle"], year, year_end
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Exception while getting Kitsu metadata for {id}: {e}")
|
||||||
|
return None, None, None
|
||||||
|
|
||||||
|
|
||||||
|
async def get_kitsu_aliases(session: aiohttp.ClientSession, id: str):
|
||||||
|
aliases = {}
|
||||||
|
try:
|
||||||
|
response = await session.get(f"https://find-my-anime.dtimur.de/api?id={id}&provider=Kitsu")
|
||||||
|
data = await response.json()
|
||||||
|
|
||||||
|
aliases["ez"] = []
|
||||||
|
aliases["ez"].append(data[0]["title"])
|
||||||
|
for synonym in data[0]["synonyms"]:
|
||||||
|
aliases["ez"].append(synonym)
|
||||||
|
|
||||||
|
total_aliases = len(aliases["ez"])
|
||||||
|
if total_aliases > 0:
|
||||||
|
logger.log(
|
||||||
|
"SCRAPER",
|
||||||
|
f"📜 Found {total_aliases} Kitsu aliases for {id}",
|
||||||
|
)
|
||||||
|
return aliases
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
logger.log("SCRAPER", f"📜 No Kitsu aliases found for {id}")
|
||||||
|
|
||||||
|
return {}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
import aiohttp
|
||||||
|
import asyncio
|
||||||
|
import time
|
||||||
|
import orjson
|
||||||
|
|
||||||
|
from RTN.patterns import normalize_title
|
||||||
|
|
||||||
|
from comet.utils.models import database, settings
|
||||||
|
from comet.utils.general import parse_media_id
|
||||||
|
|
||||||
|
from .kitsu import get_kitsu_metadata, get_kitsu_aliases
|
||||||
|
from .imdb import get_imdb_metadata
|
||||||
|
from .trakt import get_trakt_aliases
|
||||||
|
|
||||||
|
|
||||||
|
class MetadataScraper:
|
||||||
|
def __init__(self, session: aiohttp.ClientSession):
|
||||||
|
self.session = session
|
||||||
|
|
||||||
|
async def fetch_metadata_and_aliases(self, media_type: str, media_id: str):
|
||||||
|
id, season, episode = parse_media_id(media_type, media_id)
|
||||||
|
|
||||||
|
get_cached = await self.get_cached(
|
||||||
|
id, season if not "kitsu" in media_id else 1, episode
|
||||||
|
)
|
||||||
|
if get_cached is not None:
|
||||||
|
return get_cached[0], get_cached[1]
|
||||||
|
|
||||||
|
is_kitsu = "kitsu" in media_id
|
||||||
|
metadata_task = asyncio.create_task(self.get_metadata(id, season, episode, is_kitsu))
|
||||||
|
aliases_task = asyncio.create_task(self.get_aliases(media_type, id, is_kitsu))
|
||||||
|
metadata, aliases = await asyncio.gather(metadata_task, aliases_task)
|
||||||
|
await self.cache_metadata(id, metadata, aliases)
|
||||||
|
|
||||||
|
return metadata, aliases
|
||||||
|
|
||||||
|
async def get_cached(self, media_id: str, season: int, episode: int):
|
||||||
|
row = await database.fetch_one(
|
||||||
|
"""
|
||||||
|
SELECT title, year, year_end, aliases
|
||||||
|
FROM metadata_cache
|
||||||
|
WHERE media_id = :media_id
|
||||||
|
AND timestamp + :cache_ttl >= :current_time
|
||||||
|
""",
|
||||||
|
{
|
||||||
|
"media_id": media_id,
|
||||||
|
"cache_ttl": settings.METADATA_CACHE_TTL,
|
||||||
|
"current_time": time.time(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if row is not None:
|
||||||
|
metadata = {
|
||||||
|
"title": row["title"],
|
||||||
|
"year": row["year"],
|
||||||
|
"year_end": row["year_end"],
|
||||||
|
"season": season,
|
||||||
|
"episode": episode,
|
||||||
|
}
|
||||||
|
return metadata, orjson.loads(row["aliases"])
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def cache_metadata(self, media_id: str, metadata: dict, aliases: dict):
|
||||||
|
await database.execute(
|
||||||
|
f"""
|
||||||
|
INSERT {"OR IGNORE " if settings.DATABASE_TYPE == "sqlite" else ""}
|
||||||
|
INTO metadata_cache
|
||||||
|
VALUES (:media_id, :title, :year, :year_end, :aliases, :timestamp)
|
||||||
|
{" ON CONFLICT DO NOTHING" if settings.DATABASE_TYPE == "postgresql" else ""}
|
||||||
|
""",
|
||||||
|
{
|
||||||
|
"media_id": media_id,
|
||||||
|
"title": metadata["title"],
|
||||||
|
"year": metadata["year"],
|
||||||
|
"year_end": metadata["year_end"],
|
||||||
|
"aliases": orjson.dumps(aliases).decode("utf-8"),
|
||||||
|
"timestamp": time.time(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
def normalize_metadata(self, metadata: dict, season: int, episode: int):
|
||||||
|
title, year, year_end = metadata
|
||||||
|
|
||||||
|
if title is None: # metadata retrieving failed
|
||||||
|
return None
|
||||||
|
|
||||||
|
return {
|
||||||
|
"title": normalize_title(title),
|
||||||
|
"year": year,
|
||||||
|
"year_end": year_end,
|
||||||
|
"season": season,
|
||||||
|
"episode": episode,
|
||||||
|
}
|
||||||
|
|
||||||
|
async def get_metadata(self, id: str, season: int, episode: int, is_kitsu: bool):
|
||||||
|
if is_kitsu:
|
||||||
|
raw_metadata = await get_kitsu_metadata(self.session, id)
|
||||||
|
return self.normalize_metadata(raw_metadata, 1, episode)
|
||||||
|
else:
|
||||||
|
raw_metadata = await get_imdb_metadata(self.session, id)
|
||||||
|
return self.normalize_metadata(raw_metadata, season, episode)
|
||||||
|
|
||||||
|
async def get_aliases(self, media_type: str, media_id: str, is_kitsu: bool):
|
||||||
|
if is_kitsu:
|
||||||
|
return await get_kitsu_aliases(self.session, media_id)
|
||||||
|
|
||||||
|
return await get_trakt_aliases(self.session, media_type, media_id)
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import aiohttp
|
||||||
|
|
||||||
|
from comet.utils.logger import logger
|
||||||
|
|
||||||
|
|
||||||
|
async def get_trakt_aliases(
|
||||||
|
session: aiohttp.ClientSession, media_type: str, media_id: str
|
||||||
|
):
|
||||||
|
aliases = set()
|
||||||
|
try:
|
||||||
|
response = await session.get(
|
||||||
|
f"https://api.trakt.tv/{'movies' if media_type == 'movie' else 'shows'}/{media_id}/aliases"
|
||||||
|
)
|
||||||
|
data = await response.json()
|
||||||
|
|
||||||
|
for aliase in data:
|
||||||
|
aliases.add(aliase["title"])
|
||||||
|
|
||||||
|
total_aliases = len(aliases)
|
||||||
|
if total_aliases > 0:
|
||||||
|
logger.log(
|
||||||
|
"SCRAPER",
|
||||||
|
f"📜 Found {total_aliases} Trakt aliases for {media_id}",
|
||||||
|
)
|
||||||
|
return {"ez": list(aliases)}
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
logger.log("SCRAPER", f"📜 No Trakt aliases found for {media_id}")
|
||||||
|
|
||||||
|
return {}
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
import aiohttp
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
from comet.utils.models import settings
|
||||||
|
from comet.utils.logger import logger
|
||||||
|
from comet.utils.torrent import (
|
||||||
|
download_torrent,
|
||||||
|
extract_torrent_metadata,
|
||||||
|
extract_trackers_from_magnet,
|
||||||
|
add_torrent_queue,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def process_torrent(
|
||||||
|
session: aiohttp.ClientSession, result: dict, media_id: str, season: int
|
||||||
|
):
|
||||||
|
base_torrent = {
|
||||||
|
"title": result["Title"],
|
||||||
|
"infoHash": None,
|
||||||
|
"fileIndex": None,
|
||||||
|
"seeders": result["Seeders"],
|
||||||
|
"size": result["Size"],
|
||||||
|
"tracker": result["Tracker"],
|
||||||
|
"sources": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
torrents = []
|
||||||
|
|
||||||
|
if result["Link"] is not None:
|
||||||
|
content, magnet_hash, magnet_url = await download_torrent(
|
||||||
|
session, result["Link"]
|
||||||
|
)
|
||||||
|
|
||||||
|
if content:
|
||||||
|
metadata = extract_torrent_metadata(content)
|
||||||
|
if metadata:
|
||||||
|
for file in metadata["files"]:
|
||||||
|
torrent = base_torrent.copy()
|
||||||
|
torrent["title"] = file["name"]
|
||||||
|
torrent["infoHash"] = metadata["info_hash"].lower()
|
||||||
|
torrent["fileIndex"] = file["index"]
|
||||||
|
torrent["size"] = file["size"]
|
||||||
|
torrent["sources"] = metadata["announce_list"]
|
||||||
|
torrents.append(torrent)
|
||||||
|
return torrents
|
||||||
|
|
||||||
|
if magnet_hash:
|
||||||
|
base_torrent["infoHash"] = magnet_hash.lower()
|
||||||
|
base_torrent["sources"] = extract_trackers_from_magnet(magnet_url)
|
||||||
|
|
||||||
|
await add_torrent_queue.add_torrent(
|
||||||
|
magnet_url,
|
||||||
|
base_torrent["seeders"],
|
||||||
|
base_torrent["tracker"],
|
||||||
|
media_id,
|
||||||
|
season,
|
||||||
|
)
|
||||||
|
|
||||||
|
torrents.append(base_torrent)
|
||||||
|
return torrents
|
||||||
|
|
||||||
|
if "InfoHash" in result and result["InfoHash"]:
|
||||||
|
base_torrent["infoHash"] = result["InfoHash"].lower()
|
||||||
|
if result["MagnetUri"] is not None:
|
||||||
|
base_torrent["sources"] = extract_trackers_from_magnet(result["MagnetUri"])
|
||||||
|
|
||||||
|
await add_torrent_queue.add_torrent(
|
||||||
|
result["MagnetUri"],
|
||||||
|
base_torrent["seeders"],
|
||||||
|
base_torrent["tracker"],
|
||||||
|
media_id,
|
||||||
|
season,
|
||||||
|
)
|
||||||
|
|
||||||
|
torrents.append(base_torrent)
|
||||||
|
|
||||||
|
return torrents
|
||||||
|
|
||||||
|
|
||||||
|
async def fetch_jackett_results(
|
||||||
|
session: aiohttp.ClientSession, indexer: str, query: str
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
response = await session.get(
|
||||||
|
f"{settings.INDEXER_MANAGER_URL}/api/v2.0/indexers/all/results?apikey={settings.INDEXER_MANAGER_API_KEY}&Query={query}&Tracker[]={indexer}",
|
||||||
|
timeout=aiohttp.ClientTimeout(total=settings.INDEXER_MANAGER_TIMEOUT),
|
||||||
|
)
|
||||||
|
response = await response.json()
|
||||||
|
return response.get("Results", [])
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(
|
||||||
|
f"Exception while fetching Jackett results for indexer {indexer}: {e}"
|
||||||
|
)
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
async def get_jackett(manager, session: aiohttp.ClientSession, title: str, seen: set):
|
||||||
|
torrents = []
|
||||||
|
try:
|
||||||
|
tasks = [
|
||||||
|
fetch_jackett_results(session, indexer, title)
|
||||||
|
for indexer in settings.INDEXER_MANAGER_INDEXERS
|
||||||
|
]
|
||||||
|
all_results = await asyncio.gather(*tasks)
|
||||||
|
|
||||||
|
torrent_tasks = []
|
||||||
|
for result_set in all_results:
|
||||||
|
for result in result_set:
|
||||||
|
if result["Details"] in seen:
|
||||||
|
continue
|
||||||
|
|
||||||
|
seen.add(result["Details"])
|
||||||
|
torrent_tasks.append(
|
||||||
|
process_torrent(
|
||||||
|
session, result, manager.media_only_id, manager.season
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
processed_torrents = await asyncio.gather(*torrent_tasks)
|
||||||
|
torrents = [
|
||||||
|
t for sublist in processed_torrents for t in sublist if t["infoHash"]
|
||||||
|
]
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(
|
||||||
|
f"Exception while getting torrents for {title} with Jackett: {e}"
|
||||||
|
)
|
||||||
|
|
||||||
|
await manager.filter_manager(torrents)
|
||||||
@@ -0,0 +1,349 @@
|
|||||||
|
import aiohttp
|
||||||
|
import asyncio
|
||||||
|
import orjson
|
||||||
|
import time
|
||||||
|
|
||||||
|
from RTN import (
|
||||||
|
parse,
|
||||||
|
title_match,
|
||||||
|
get_rank,
|
||||||
|
check_fetch,
|
||||||
|
sort_torrents,
|
||||||
|
ParsedData,
|
||||||
|
BestRanking,
|
||||||
|
Torrent,
|
||||||
|
)
|
||||||
|
|
||||||
|
from comet.utils.models import settings, database, CometSettingsModel
|
||||||
|
from comet.utils.general import default_dump
|
||||||
|
from comet.utils.debrid import cache_availability, get_cached_availability
|
||||||
|
from comet.debrid.manager import retrieve_debrid_availability
|
||||||
|
from .zilean import get_zilean
|
||||||
|
from .torrentio import get_torrentio
|
||||||
|
from .mediafusion import get_mediafusion
|
||||||
|
from .jackett import get_jackett
|
||||||
|
from .prowlarr import get_prowlarr
|
||||||
|
|
||||||
|
|
||||||
|
class TorrentManager:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
debrid_service: str,
|
||||||
|
debrid_api_key: str,
|
||||||
|
ip: str,
|
||||||
|
media_type: str,
|
||||||
|
media_full_id: str,
|
||||||
|
media_only_id: str,
|
||||||
|
title: str,
|
||||||
|
year: int,
|
||||||
|
year_end: int,
|
||||||
|
season: int,
|
||||||
|
episode: int,
|
||||||
|
aliases: dict,
|
||||||
|
remove_adult_content: bool,
|
||||||
|
):
|
||||||
|
self.debrid_service = debrid_service
|
||||||
|
self.debrid_api_key = debrid_api_key
|
||||||
|
self.ip = ip
|
||||||
|
self.media_type = media_type
|
||||||
|
self.media_id = media_full_id
|
||||||
|
self.media_only_id = media_only_id
|
||||||
|
self.title = title
|
||||||
|
self.year = year
|
||||||
|
self.year_end = year_end
|
||||||
|
self.season = season
|
||||||
|
self.episode = episode
|
||||||
|
self.aliases = aliases
|
||||||
|
self.remove_adult_content = remove_adult_content
|
||||||
|
|
||||||
|
self.seen_hashes = set()
|
||||||
|
self.torrents = {}
|
||||||
|
self.ready_to_cache = []
|
||||||
|
self.ranked_torrents = {}
|
||||||
|
|
||||||
|
async def scrape_torrents(
|
||||||
|
self,
|
||||||
|
session: aiohttp.ClientSession,
|
||||||
|
):
|
||||||
|
tasks = []
|
||||||
|
if settings.SCRAPE_TORRENTIO:
|
||||||
|
tasks.append(get_torrentio(self, self.media_type, self.media_id))
|
||||||
|
if settings.SCRAPE_MEDIAFUSION:
|
||||||
|
tasks.append(get_mediafusion(self, self.media_type, self.media_id))
|
||||||
|
if settings.SCRAPE_ZILEAN:
|
||||||
|
tasks.append(
|
||||||
|
get_zilean(self, session, self.title, self.season, self.episode)
|
||||||
|
)
|
||||||
|
if settings.INDEXER_MANAGER_API_KEY:
|
||||||
|
queries = [self.title]
|
||||||
|
|
||||||
|
if self.media_type == "series":
|
||||||
|
queries.append(f"{self.title} S{self.season:02d}")
|
||||||
|
queries.append(f"{self.title} S{self.season:02d}E{self.episode:02d}")
|
||||||
|
|
||||||
|
seen_already = set()
|
||||||
|
for query in queries:
|
||||||
|
if settings.INDEXER_MANAGER_TYPE == "jackett":
|
||||||
|
tasks.append(get_jackett(self, session, query, seen_already))
|
||||||
|
elif settings.INDEXER_MANAGER_TYPE == "prowlarr":
|
||||||
|
tasks.append(get_prowlarr(self, session, query, seen_already))
|
||||||
|
|
||||||
|
await asyncio.gather(*tasks)
|
||||||
|
asyncio.create_task(self.cache_torrents())
|
||||||
|
|
||||||
|
for torrent in self.ready_to_cache:
|
||||||
|
season = torrent["parsed"].seasons[0] if torrent["parsed"].seasons else None
|
||||||
|
episode = (
|
||||||
|
torrent["parsed"].episodes[0] if torrent["parsed"].episodes else None
|
||||||
|
)
|
||||||
|
|
||||||
|
if (season is not None and season != self.season) or (
|
||||||
|
episode is not None and episode != self.episode
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
|
||||||
|
info_hash = torrent["infoHash"]
|
||||||
|
self.torrents[info_hash] = {
|
||||||
|
"fileIndex": torrent["fileIndex"],
|
||||||
|
"title": torrent["title"],
|
||||||
|
"seeders": torrent["seeders"],
|
||||||
|
"size": torrent["size"],
|
||||||
|
"tracker": torrent["tracker"],
|
||||||
|
"sources": torrent["sources"],
|
||||||
|
"parsed": torrent["parsed"],
|
||||||
|
}
|
||||||
|
|
||||||
|
async def get_cached_torrents(self):
|
||||||
|
rows = await database.fetch_all(
|
||||||
|
"""
|
||||||
|
SELECT info_hash, file_index, title, seeders, size, tracker, sources, parsed
|
||||||
|
FROM torrents
|
||||||
|
WHERE media_id = :media_id
|
||||||
|
AND ((season IS NOT NULL AND season = cast(:season as INTEGER)) OR (season IS NULL AND cast(:season as INTEGER) IS NULL))
|
||||||
|
AND (episode IS NULL OR episode = cast(:episode as INTEGER))
|
||||||
|
AND timestamp + :cache_ttl >= :current_time
|
||||||
|
""",
|
||||||
|
{
|
||||||
|
"media_id": self.media_only_id,
|
||||||
|
"season": self.season,
|
||||||
|
"episode": self.episode,
|
||||||
|
"cache_ttl": settings.TORRENT_CACHE_TTL,
|
||||||
|
"current_time": time.time(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
for row in rows:
|
||||||
|
info_hash = row["info_hash"]
|
||||||
|
self.torrents[info_hash] = {
|
||||||
|
"fileIndex": row["file_index"],
|
||||||
|
"title": row["title"],
|
||||||
|
"seeders": row["seeders"],
|
||||||
|
"size": row["size"],
|
||||||
|
"tracker": row["tracker"],
|
||||||
|
"sources": orjson.loads(row["sources"]),
|
||||||
|
"parsed": ParsedData(**orjson.loads(row["parsed"])),
|
||||||
|
}
|
||||||
|
|
||||||
|
async def cache_torrents(self):
|
||||||
|
current_time = time.time()
|
||||||
|
values = [
|
||||||
|
{
|
||||||
|
"media_id": self.media_only_id,
|
||||||
|
"info_hash": torrent["infoHash"],
|
||||||
|
"file_index": torrent["fileIndex"],
|
||||||
|
"season": torrent["parsed"].seasons[0]
|
||||||
|
if torrent["parsed"].seasons
|
||||||
|
else self.season,
|
||||||
|
"episode": torrent["parsed"].episodes[0]
|
||||||
|
if torrent["parsed"].episodes
|
||||||
|
else None,
|
||||||
|
"title": torrent["title"],
|
||||||
|
"seeders": torrent["seeders"],
|
||||||
|
"size": torrent["size"],
|
||||||
|
"tracker": torrent["tracker"],
|
||||||
|
"sources": orjson.dumps(torrent["sources"]).decode("utf-8"),
|
||||||
|
"parsed": orjson.dumps(torrent["parsed"], default_dump).decode("utf-8"),
|
||||||
|
"timestamp": current_time,
|
||||||
|
}
|
||||||
|
for torrent in self.ready_to_cache
|
||||||
|
]
|
||||||
|
|
||||||
|
query = f"""
|
||||||
|
INSERT {"OR IGNORE " if settings.DATABASE_TYPE == "sqlite" else ""}
|
||||||
|
INTO torrents
|
||||||
|
VALUES (:media_id, :info_hash, :file_index, :season, :episode, :title, :seeders, :size, :tracker, :sources, :parsed, :timestamp)
|
||||||
|
{" ON CONFLICT DO NOTHING" if settings.DATABASE_TYPE == "postgresql" else ""}
|
||||||
|
"""
|
||||||
|
|
||||||
|
await database.execute_many(query, values)
|
||||||
|
|
||||||
|
async def filter(self, torrents: list):
|
||||||
|
title = self.title
|
||||||
|
year = self.year
|
||||||
|
year_end = self.year_end
|
||||||
|
aliases = self.aliases
|
||||||
|
remove_adult_content = self.remove_adult_content
|
||||||
|
|
||||||
|
for torrent in torrents:
|
||||||
|
parsed = parse(torrent["title"])
|
||||||
|
|
||||||
|
if remove_adult_content and parsed.adult:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if parsed.parsed_title and not title_match(
|
||||||
|
title, parsed.parsed_title, aliases=aliases
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if year and parsed.year:
|
||||||
|
if year_end is not None:
|
||||||
|
if not (year <= parsed.year <= year_end):
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
if year < (parsed.year - 1) or year > (parsed.year + 1):
|
||||||
|
continue
|
||||||
|
|
||||||
|
torrent["parsed"] = parsed
|
||||||
|
self.ready_to_cache.append(torrent)
|
||||||
|
|
||||||
|
async def filter_manager(self, torrents: list):
|
||||||
|
new_torrents = [
|
||||||
|
torrent
|
||||||
|
for torrent in torrents
|
||||||
|
if (torrent["infoHash"], torrent["title"]) not in self.seen_hashes
|
||||||
|
]
|
||||||
|
self.seen_hashes.update(
|
||||||
|
(torrent["infoHash"], torrent["title"]) for torrent in new_torrents
|
||||||
|
)
|
||||||
|
|
||||||
|
chunk_size = 50
|
||||||
|
tasks = [
|
||||||
|
self.filter(new_torrents[i : i + chunk_size])
|
||||||
|
for i in range(0, len(new_torrents), chunk_size)
|
||||||
|
]
|
||||||
|
await asyncio.gather(*tasks)
|
||||||
|
|
||||||
|
def rank_torrents(
|
||||||
|
self,
|
||||||
|
rtn_settings: CometSettingsModel,
|
||||||
|
rtn_ranking: BestRanking,
|
||||||
|
max_results_per_resolution: int,
|
||||||
|
max_size: int,
|
||||||
|
cached_only: int,
|
||||||
|
remove_trash: int,
|
||||||
|
):
|
||||||
|
ranked_torrents = set()
|
||||||
|
for info_hash, torrent in self.torrents.items():
|
||||||
|
if (
|
||||||
|
cached_only
|
||||||
|
and self.debrid_service != "torrent"
|
||||||
|
and not torrent["cached"]
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if max_size != 0 and torrent["size"] > max_size:
|
||||||
|
continue
|
||||||
|
|
||||||
|
parsed = torrent["parsed"]
|
||||||
|
|
||||||
|
raw_title = torrent["title"]
|
||||||
|
|
||||||
|
is_fetchable, failed_keys = check_fetch(parsed, rtn_settings)
|
||||||
|
rank = get_rank(parsed, rtn_settings, rtn_ranking)
|
||||||
|
|
||||||
|
if remove_trash:
|
||||||
|
if (
|
||||||
|
not is_fetchable
|
||||||
|
or rank < rtn_settings.options["remove_ranks_under"]
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
ranked_torrents.add(
|
||||||
|
Torrent(
|
||||||
|
infohash=info_hash,
|
||||||
|
raw_title=raw_title,
|
||||||
|
data=parsed,
|
||||||
|
fetch=is_fetchable,
|
||||||
|
rank=rank,
|
||||||
|
lev_ratio=0.0,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
self.ranked_torrents = sort_torrents(
|
||||||
|
ranked_torrents, max_results_per_resolution
|
||||||
|
)
|
||||||
|
|
||||||
|
async def get_and_cache_debrid_availability(self, session: aiohttp.ClientSession):
|
||||||
|
info_hashes = list(self.torrents.keys())
|
||||||
|
|
||||||
|
seeders_map = {hash: self.torrents[hash]["seeders"] for hash in info_hashes}
|
||||||
|
tracker_map = {hash: self.torrents[hash]["tracker"] for hash in info_hashes}
|
||||||
|
sources_map = {hash: self.torrents[hash]["sources"] for hash in info_hashes}
|
||||||
|
|
||||||
|
availability = await retrieve_debrid_availability(
|
||||||
|
session,
|
||||||
|
self.media_id,
|
||||||
|
self.media_only_id,
|
||||||
|
self.debrid_service,
|
||||||
|
self.debrid_api_key,
|
||||||
|
self.ip,
|
||||||
|
info_hashes,
|
||||||
|
seeders_map,
|
||||||
|
tracker_map,
|
||||||
|
sources_map,
|
||||||
|
)
|
||||||
|
|
||||||
|
if len(availability) == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
for file in availability:
|
||||||
|
season = file["season"]
|
||||||
|
episode = file["episode"]
|
||||||
|
if (season is not None and season != self.season) or (
|
||||||
|
episode is not None and episode != self.episode
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
|
||||||
|
info_hash = file["info_hash"]
|
||||||
|
self.torrents[info_hash]["cached"] = True
|
||||||
|
|
||||||
|
if file["parsed"] is not None:
|
||||||
|
self.torrents[info_hash]["parsed"] = file["parsed"]
|
||||||
|
if file["index"] is not None:
|
||||||
|
self.torrents[info_hash]["fileIndex"] = file["index"]
|
||||||
|
if file["title"] is not None:
|
||||||
|
self.torrents[info_hash]["title"] = file["title"]
|
||||||
|
if file["size"] is not None:
|
||||||
|
self.torrents[info_hash]["size"] = file["size"]
|
||||||
|
|
||||||
|
asyncio.create_task(cache_availability(self.debrid_service, availability))
|
||||||
|
|
||||||
|
async def get_cached_availability(self):
|
||||||
|
info_hashes = list(self.torrents.keys())
|
||||||
|
for hash in info_hashes:
|
||||||
|
self.torrents[hash]["cached"] = False
|
||||||
|
|
||||||
|
if self.debrid_service == "torrent" or len(self.torrents) == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
rows = await get_cached_availability(
|
||||||
|
self.debrid_service, info_hashes, self.season, self.episode
|
||||||
|
)
|
||||||
|
|
||||||
|
for row in rows:
|
||||||
|
info_hash = row["info_hash"]
|
||||||
|
self.torrents[info_hash]["cached"] = True
|
||||||
|
|
||||||
|
if row["parsed"] is not None:
|
||||||
|
self.torrents[info_hash]["parsed"] = ParsedData(
|
||||||
|
**orjson.loads(row["parsed"])
|
||||||
|
)
|
||||||
|
if row["file_index"] is not None:
|
||||||
|
self.torrents[info_hash]["fileIndex"] = row["file_index"]
|
||||||
|
if row["title"] is not None:
|
||||||
|
self.torrents[info_hash]["title"] = row["title"]
|
||||||
|
if row["size"] is not None:
|
||||||
|
self.torrents[info_hash]["size"] = row["size"]
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
from curl_cffi import requests
|
||||||
|
|
||||||
|
from comet.utils.models import settings
|
||||||
|
from comet.utils.logger import logger
|
||||||
|
|
||||||
|
|
||||||
|
async def get_mediafusion(manager, media_type: str, media_id: str):
|
||||||
|
torrents = []
|
||||||
|
try:
|
||||||
|
try:
|
||||||
|
get_mediafusion = requests.get(
|
||||||
|
f"{settings.MEDIAFUSION_URL}/D-zn4qJLK4wUZVWscY9ESCnoZBEiNJCZ9uwfCvmxuliDjY7vkc-fu0OdxUPxwsP3_A/stream/{media_type}/{media_id}.json"
|
||||||
|
).json()
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(
|
||||||
|
f"Failed to get MediaFusion results without proxy for {media_id}: {e}"
|
||||||
|
)
|
||||||
|
|
||||||
|
get_mediafusion = requests.get(
|
||||||
|
f"{settings.MEDIAFUSION_URL}/stream/{media_type}/{media_id}.json",
|
||||||
|
proxies={
|
||||||
|
"http": settings.DEBRID_PROXY_URL,
|
||||||
|
"https": settings.DEBRID_PROXY_URL,
|
||||||
|
},
|
||||||
|
).json()
|
||||||
|
|
||||||
|
for torrent in get_mediafusion["streams"]:
|
||||||
|
title_full = torrent["description"]
|
||||||
|
lines = title_full.split("\n")
|
||||||
|
|
||||||
|
title = lines[0].replace("📂 ", "").replace("/", "")
|
||||||
|
|
||||||
|
seeders = None
|
||||||
|
if "👤" in lines[1]:
|
||||||
|
seeders = int(lines[1].split("👤 ")[1].split("\n")[0])
|
||||||
|
|
||||||
|
tracker = lines[-1].split("🔗 ")[1]
|
||||||
|
|
||||||
|
torrents.append(
|
||||||
|
{
|
||||||
|
"title": title,
|
||||||
|
"infoHash": torrent["infoHash"].lower(),
|
||||||
|
"fileIndex": torrent["fileIdx"] if "fileIdx" in torrent else None,
|
||||||
|
"seeders": seeders,
|
||||||
|
"size": torrent["behaviorHints"][
|
||||||
|
"videoSize"
|
||||||
|
], # not the pack size but still useful for prowlarr userss
|
||||||
|
"tracker": f"MediaFusion|{tracker}",
|
||||||
|
"sources": torrent["sources"] if "sources" in torrent else [],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(
|
||||||
|
f"Exception while getting torrents for {media_id} with MediaFusion, your IP is most likely blacklisted (you should try proxying Comet): {e}"
|
||||||
|
)
|
||||||
|
pass
|
||||||
|
|
||||||
|
await manager.filter_manager(torrents)
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
import aiohttp
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
from comet.utils.models import settings
|
||||||
|
from comet.utils.logger import logger
|
||||||
|
from comet.utils.torrent import (
|
||||||
|
download_torrent,
|
||||||
|
extract_torrent_metadata,
|
||||||
|
extract_trackers_from_magnet,
|
||||||
|
add_torrent_queue,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def process_torrent(
|
||||||
|
session: aiohttp.ClientSession, result: dict, media_id: str, season: int
|
||||||
|
):
|
||||||
|
base_torrent = {
|
||||||
|
"title": result["title"],
|
||||||
|
"infoHash": None,
|
||||||
|
"fileIndex": None,
|
||||||
|
"seeders": result["seeders"],
|
||||||
|
"size": result["size"],
|
||||||
|
"tracker": result["indexer"],
|
||||||
|
"sources": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
torrents = []
|
||||||
|
|
||||||
|
if "downloadUrl" in result:
|
||||||
|
content, magnet_hash, magnet_url = await download_torrent(
|
||||||
|
session, result["downloadUrl"]
|
||||||
|
)
|
||||||
|
|
||||||
|
if content:
|
||||||
|
metadata = extract_torrent_metadata(content)
|
||||||
|
if metadata:
|
||||||
|
for file in metadata["files"]:
|
||||||
|
torrent = base_torrent.copy()
|
||||||
|
torrent["title"] = file["name"]
|
||||||
|
torrent["infoHash"] = metadata["info_hash"].lower()
|
||||||
|
torrent["fileIndex"] = file["index"]
|
||||||
|
torrent["size"] = file["size"]
|
||||||
|
torrent["sources"] = metadata["announce_list"]
|
||||||
|
torrents.append(torrent)
|
||||||
|
return torrents
|
||||||
|
|
||||||
|
if magnet_hash:
|
||||||
|
base_torrent["infoHash"] = magnet_hash.lower()
|
||||||
|
base_torrent["sources"] = extract_trackers_from_magnet(magnet_url)
|
||||||
|
|
||||||
|
await add_torrent_queue.add_torrent(
|
||||||
|
magnet_url,
|
||||||
|
base_torrent["seeders"],
|
||||||
|
base_torrent["tracker"],
|
||||||
|
media_id,
|
||||||
|
season,
|
||||||
|
)
|
||||||
|
|
||||||
|
torrents.append(base_torrent)
|
||||||
|
return torrents
|
||||||
|
|
||||||
|
if "infoHash" in result and result["infoHash"]:
|
||||||
|
base_torrent["infoHash"] = result["infoHash"].lower()
|
||||||
|
if "guid" in result and result["guid"].startswith("magnet:"):
|
||||||
|
base_torrent["sources"] = extract_trackers_from_magnet(result["guid"])
|
||||||
|
|
||||||
|
await add_torrent_queue.add_torrent(
|
||||||
|
result["guid"],
|
||||||
|
base_torrent["seeders"],
|
||||||
|
base_torrent["tracker"],
|
||||||
|
media_id,
|
||||||
|
season,
|
||||||
|
)
|
||||||
|
|
||||||
|
torrents.append(base_torrent)
|
||||||
|
|
||||||
|
return torrents
|
||||||
|
|
||||||
|
|
||||||
|
async def get_prowlarr(manager, session: aiohttp.ClientSession, title: str, seen: set):
|
||||||
|
torrents = []
|
||||||
|
try:
|
||||||
|
indexers = [indexer.lower() for indexer in settings.INDEXER_MANAGER_INDEXERS]
|
||||||
|
|
||||||
|
get_indexers = await session.get(
|
||||||
|
f"{settings.INDEXER_MANAGER_URL}/api/v1/indexer",
|
||||||
|
headers={"X-Api-Key": settings.INDEXER_MANAGER_API_KEY},
|
||||||
|
)
|
||||||
|
get_indexers = await get_indexers.json()
|
||||||
|
|
||||||
|
indexers_id = []
|
||||||
|
for indexer in get_indexers:
|
||||||
|
if (
|
||||||
|
indexer["name"].lower() in indexers
|
||||||
|
or indexer["definitionName"].lower() in indexers
|
||||||
|
):
|
||||||
|
indexers_id.append(indexer["id"])
|
||||||
|
|
||||||
|
response = await session.get(
|
||||||
|
f"{settings.INDEXER_MANAGER_URL}/api/v1/search?query={title}&indexerIds={'&indexerIds='.join(str(indexer_id) for indexer_id in indexers_id)}&type=search",
|
||||||
|
headers={"X-Api-Key": settings.INDEXER_MANAGER_API_KEY},
|
||||||
|
)
|
||||||
|
response = await response.json()
|
||||||
|
|
||||||
|
torrent_tasks = []
|
||||||
|
for result in response:
|
||||||
|
if result["infoUrl"] in seen:
|
||||||
|
continue
|
||||||
|
|
||||||
|
seen.add(result["infoUrl"])
|
||||||
|
torrent_tasks.append(
|
||||||
|
process_torrent(session, result, manager.media_only_id, manager.season)
|
||||||
|
)
|
||||||
|
|
||||||
|
processed_torrents = await asyncio.gather(*torrent_tasks)
|
||||||
|
torrents = [
|
||||||
|
t for sublist in processed_torrents for t in sublist if t["infoHash"]
|
||||||
|
]
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(
|
||||||
|
f"Exception while getting torrents for {title} with Prowlarr: {e}"
|
||||||
|
)
|
||||||
|
|
||||||
|
await manager.filter_manager(torrents)
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import re
|
||||||
|
|
||||||
|
from curl_cffi import requests
|
||||||
|
|
||||||
|
from comet.utils.models import settings
|
||||||
|
from comet.utils.logger import logger
|
||||||
|
from comet.utils.general import size_to_bytes
|
||||||
|
|
||||||
|
|
||||||
|
data_pattern = re.compile(
|
||||||
|
r"(?:👤 (\d+) )?💾 ([\d.]+ [KMGT]B)(?: ⚙️ (\w+))?", re.IGNORECASE
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_torrentio(manager, media_type: str, media_id: str):
|
||||||
|
torrents = []
|
||||||
|
try:
|
||||||
|
try:
|
||||||
|
get_torrentio = requests.get(
|
||||||
|
f"{settings.TORRENTIO_URL}/stream/{media_type}/{media_id}.json"
|
||||||
|
).json()
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(
|
||||||
|
f"Failed to get Torrentio results without proxy for {media_id}: {e}"
|
||||||
|
)
|
||||||
|
|
||||||
|
get_torrentio = requests.get(
|
||||||
|
f"{settings.TORRENTIO_URL}/stream/{media_type}/{media_id}.json",
|
||||||
|
proxies={
|
||||||
|
"http": settings.DEBRID_PROXY_URL,
|
||||||
|
"https": settings.DEBRID_PROXY_URL,
|
||||||
|
},
|
||||||
|
).json()
|
||||||
|
|
||||||
|
for torrent in get_torrentio["streams"]:
|
||||||
|
title_full = torrent["title"]
|
||||||
|
title = (
|
||||||
|
title_full.split("\n")[0]
|
||||||
|
if settings.TORRENTIO_URL == "https://torrentio.strem.fun"
|
||||||
|
else title_full.split("\n💾")[0].split("\n")[-1]
|
||||||
|
)
|
||||||
|
|
||||||
|
match = data_pattern.search(title_full)
|
||||||
|
|
||||||
|
seeders = int(match.group(1)) if match.group(1) else None
|
||||||
|
size = size_to_bytes(match.group(2))
|
||||||
|
tracker = match.group(3) if match.group(3) else "KnightCrawler"
|
||||||
|
|
||||||
|
torrents.append(
|
||||||
|
{
|
||||||
|
"title": title,
|
||||||
|
"infoHash": torrent["infoHash"].lower(),
|
||||||
|
"fileIndex": torrent["fileIdx"] if "fileIdx" in torrent else None,
|
||||||
|
"seeders": seeders,
|
||||||
|
"size": size,
|
||||||
|
"tracker": f"Torrentio|{tracker}",
|
||||||
|
"sources": torrent["sources"] if "sources" in torrent else [],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(
|
||||||
|
f"Exception while getting torrents for {media_id} with Torrentio, your IP is most likely blacklisted (you should try proxying Comet): {e}"
|
||||||
|
)
|
||||||
|
|
||||||
|
await manager.filter_manager(torrents)
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import aiohttp
|
||||||
|
|
||||||
|
from comet.utils.models import settings
|
||||||
|
from comet.utils.logger import logger
|
||||||
|
|
||||||
|
|
||||||
|
async def get_zilean(
|
||||||
|
manager, session: aiohttp.ClientSession, title: str, season: int, episode: int
|
||||||
|
):
|
||||||
|
torrents = []
|
||||||
|
try:
|
||||||
|
show = f"&season={season}&episode={episode}"
|
||||||
|
get_dmm = await session.get(
|
||||||
|
f"{settings.ZILEAN_URL}/dmm/filtered?query={title}{show if season else ''}"
|
||||||
|
)
|
||||||
|
get_dmm = await get_dmm.json()
|
||||||
|
|
||||||
|
for result in get_dmm:
|
||||||
|
object = {
|
||||||
|
"title": result["raw_title"],
|
||||||
|
"infoHash": result["info_hash"].lower(),
|
||||||
|
"fileIndex": None,
|
||||||
|
"seeders": None,
|
||||||
|
"size": int(result["size"]),
|
||||||
|
"tracker": "DMM",
|
||||||
|
"sources": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
torrents.append(object)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Exception while getting torrents for {title} with Zilean: {e}")
|
||||||
|
|
||||||
|
await manager.filter_manager(torrents)
|
||||||
+232
-119
@@ -12,8 +12,8 @@
|
|||||||
<title>Comet - Stremio's fastest torrent/debrid search add-on.</title>
|
<title>Comet - Stremio's fastest torrent/debrid search add-on.</title>
|
||||||
<link id="favicon" rel="icon" type="image/x-icon" href="https://i.imgur.com/jmVoVMu.jpeg">
|
<link id="favicon" rel="icon" type="image/x-icon" href="https://i.imgur.com/jmVoVMu.jpeg">
|
||||||
|
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.15.1/cdn/themes/dark.css" />
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.19.0/cdn/themes/dark.css" />
|
||||||
<script type="module" src="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.15.1/cdn/shoelace-autoloader.js"></script>
|
<script type="module" src="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.19.0/cdn/shoelace-autoloader.js"></script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
:not(:defined) {
|
:not(:defined) {
|
||||||
@@ -512,19 +512,26 @@
|
|||||||
|
|
||||||
<div class="form-container">
|
<div class="form-container">
|
||||||
<div class="form-item">
|
<div class="form-item">
|
||||||
<sl-select id="indexers" multiple clearable label="Indexers" placeholder="Select indexers" {% if not indexerManager %} disabled {% endif %}></sl-select>
|
<sl-select id="languages_required" multiple clearable label="Required Languages" placeholder="Select required languages"></sl-select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-item">
|
<div class="form-item">
|
||||||
<sl-select id="languages" multiple clearable label="Languages" placeholder="Select languages"></sl-select>
|
<sl-select id="languages_preferred" multiple clearable label="Preferred Languages" placeholder="Select preferred languages"></sl-select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-item">
|
<div class="form-item">
|
||||||
<sl-select id="resolutions" multiple clearable label="Resolutions" placeholder="Select resolutions"></sl-select>
|
<sl-select id="languages_excluded" multiple clearable label="Excluded Languages" placeholder="Select excluded languages"></sl-select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-item">
|
<div class="form-item">
|
||||||
<sl-input id="maxResults" type="number" min=0 value=0 label="Max Results" placeholder="Enter max results"></sl-input>
|
<sl-select id="resolutions" multiple clearable label="Resolutions" placeholder="Select resolutions">
|
||||||
|
<sl-option value="r2160p">4K - 2160p</sl-option>
|
||||||
|
<sl-option value="r1080p">Full HD - 1080p</sl-option>
|
||||||
|
<sl-option value="r720p">HD - 720p</sl-option>
|
||||||
|
<sl-option value="r480p">SD - 480p</sl-option>
|
||||||
|
<sl-option value="r360p">LD - 360p</sl-option>
|
||||||
|
<sl-option value="unknown">Unknown</sl-option>
|
||||||
|
</sl-select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-item">
|
<div class="form-item">
|
||||||
@@ -535,37 +542,39 @@
|
|||||||
<sl-input id="maxSize" type="number" min=0 value=0 label="Max Size (GB)" placeholder="Enter max size in gigabytes"></sl-input>
|
<sl-input id="maxSize" type="number" min=0 value=0 label="Max Size (GB)" placeholder="Enter max size in gigabytes"></sl-input>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-item">
|
<div class="form-item" {% if not proxyDebridStream %}style="display: none"{% endif %}>
|
||||||
<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>
|
<sl-input id="debridStreamProxyPassword" label="Debrid Stream Proxy Password" placeholder="Enter password" help-text="Debrid Stream Proxying allows you to use your Debrid Service from multiple IPs at same time!"></sl-input>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-item">
|
<div class="form-item">
|
||||||
<sl-select id="debridService" value="torbox" label="Debrid Service" placeholder="Select debrid service">
|
<sl-select id="debridService" value="torrent" label="Debrid Service" placeholder="Select debrid service">
|
||||||
|
<sl-option value="torrent">Torrent</sl-option>
|
||||||
<sl-option value="torbox">TorBox</sl-option>
|
<sl-option value="torbox">TorBox</sl-option>
|
||||||
|
<sl-option value="easydebrid">EasyDebrid</sl-option>
|
||||||
|
<sl-option value="realdebrid">Real-Debrid</sl-option>
|
||||||
<sl-option value="debridlink">Debrid-Link</sl-option>
|
<sl-option value="debridlink">Debrid-Link</sl-option>
|
||||||
<sl-option value="alldebrid">All-Debrid</sl-option>
|
<sl-option value="alldebrid">All-Debrid</sl-option>
|
||||||
<sl-option value="premiumize">Premiumize</sl-option>
|
<sl-option value="premiumize">Premiumize</sl-option>
|
||||||
<sl-option value="realdebrid">Real-Debrid</sl-option>
|
<sl-option value="offcloud">Offcloud</sl-option>
|
||||||
<sl-option value="stremthru">StremThru</sl-option>
|
<sl-option value="offcloud">PikPak</sl-option>
|
||||||
</sl-select>
|
</sl-select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-item">
|
<div class="form-item">
|
||||||
<label for="debridService">
|
<label for="debridService">
|
||||||
Debrid API Key -
|
Debrid API Key
|
||||||
<a id="apiKeyLink" href="https://torbox.app/settings" target="_blank">Get It Here</a>
|
<a id="apiKeyLink" href="" target="_blank"></a>
|
||||||
</label>
|
</label>
|
||||||
<sl-input id="debridApiKey" placeholder="Enter API key" help-text="If no key is specified, you'll get direct torrents!"></sl-input>
|
<sl-input id="debridApiKey" placeholder="Enter API key"></sl-input>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-item">
|
|
||||||
<sl-input id="stremthruUrl" label="StremThru URL" placeholder="{{stremthruDefaultUrl}}" help-text="URL for StremThru."></sl-input>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<sl-details summary="Advanced Settings">
|
<sl-details summary="Advanced Settings">
|
||||||
<div class="form-item">
|
<div class="form-item">
|
||||||
<sl-checkbox checked id="removeTrash" help-text="Remove all trash from results (Adult Content, CAM, Clean Audio, PDTV, R5, Screener, Size, Telecine and Telesync)">Remove Trash</sl-checkbox>
|
<sl-input id="rankThreshold" type="number" value="-10000000000" label="Minimum Rank Threshold" placeholder="Enter minimum rank threshold" help-text="Torrents ranked below this value will be excluded from results."></sl-input>
|
||||||
<sl-checkbox id="reverseResultOrder" help-text="Reverse the order of the results for each resolution (useful for those who need small file sizes)">Reverse Result Order</sl-checkbox>
|
<sl-checkbox id="cachedOnly" help-text="Show only content that has already been cached.">Show Cached Only</sl-checkbox>
|
||||||
|
<sl-checkbox id="allowEnglishInLanguages" help-text="Allows English language torrents to be included even if you excluded English from your language preferences.">Allow English in Languages</sl-checkbox>
|
||||||
|
<sl-checkbox id="removeUnknownLanguages" help-text="Exclude torrents that don't have a language in the title.">Remove Unknown Languages</sl-checkbox>
|
||||||
|
<sl-checkbox checked id="removeTrash" help-text="Remove all trash from results (if disabled, your filters will not be applied - Adult Content, CAM, Clean Audio, PDTV, R5, Screener, Size, Telecine and Telesync)">Remove Trash</sl-checkbox>
|
||||||
<sl-select id="resultFormat" multiple label="Result Format" placeholder="Select what to show in result title" hoist max-options-visible=10>
|
<sl-select id="resultFormat" multiple label="Result Format" placeholder="Select what to show in result title" hoist max-options-visible=10>
|
||||||
</sl-select>
|
</sl-select>
|
||||||
</div>
|
</div>
|
||||||
@@ -576,24 +585,51 @@
|
|||||||
const selectedService = event.target.value;
|
const selectedService = event.target.value;
|
||||||
const apiKeyLink = document.getElementById("apiKeyLink");
|
const apiKeyLink = document.getElementById("apiKeyLink");
|
||||||
const apiKeyInput = document.getElementById("debridApiKey");
|
const apiKeyInput = document.getElementById("debridApiKey");
|
||||||
|
debridStreamProxyPasswordInput = document.getElementById("debridStreamProxyPassword");
|
||||||
|
|
||||||
if (selectedService === "realdebrid") {
|
if (selectedService === "realdebrid") {
|
||||||
apiKeyLink.href = "https://real-debrid.com/apitoken";
|
apiKeyLink.href = "https://real-debrid.com/apitoken";
|
||||||
|
apiKeyLink.textContent = "Get It Here"
|
||||||
} else if (selectedService === "alldebrid") {
|
} else if (selectedService === "alldebrid") {
|
||||||
apiKeyLink.href = "https://alldebrid.com/apikeys";
|
apiKeyLink.href = "https://alldebrid.com/apikeys";
|
||||||
|
apiKeyLink.textContent = "Get It Here"
|
||||||
} else if (selectedService === "premiumize") {
|
} else if (selectedService === "premiumize") {
|
||||||
apiKeyLink.href = "https://premiumize.me/account";
|
apiKeyLink.href = "https://premiumize.me/account";
|
||||||
|
apiKeyLink.textContent = "Get It Here"
|
||||||
} else if (selectedService === "torbox") {
|
} else if (selectedService === "torbox") {
|
||||||
apiKeyLink.href = "https://torbox.app/settings";
|
apiKeyLink.href = "https://torbox.app/settings";
|
||||||
|
apiKeyLink.textContent = "Get It Here"
|
||||||
|
} else if (selectedService === "easydebrid") {
|
||||||
|
apiKeyLink.href = "https://paradise-cloud.com/products/easydebrid";
|
||||||
|
apiKeyLink.textContent = "Get It Here"
|
||||||
} else if (selectedService === "debridlink") {
|
} else if (selectedService === "debridlink") {
|
||||||
apiKeyLink.href = "https://debrid-link.com/webapp/apikey";
|
apiKeyLink.href = "https://debrid-link.com/webapp/apikey";
|
||||||
} else if (selectedService === "stremthru") {
|
apiKeyLink.textContent = "Get It Here"
|
||||||
apiKeyLink.href = "https://github.com/MunifTanjim/stremthru?tab=readme-ov-file#configuration";
|
} else if (selectedService === "offcloud") {
|
||||||
|
apiKeyLink.href = "https://offcloud.com";
|
||||||
|
apiKeyLink.textContent = "Get It Here"
|
||||||
|
} else if (selectedService === "pikpak") {
|
||||||
|
apiKeyLink.href = "https://mypikpak.com";
|
||||||
|
apiKeyLink.textContent = "Get It Here"
|
||||||
|
} else if (selectedService === "torrent") {
|
||||||
|
apiKeyLink.href = "";
|
||||||
|
apiKeyLink.textContent = ""
|
||||||
}
|
}
|
||||||
if (selectedService === "stremthru") {
|
|
||||||
apiKeyInput.helpText = "Enter Credential ('store_name:store_token' or base64 encoded basic token from 'STREMTHRU_PROXY_AUTH' config)"
|
if (selectedService === "offcloud" || selectedService === "pikpak") {
|
||||||
|
apiKeyInput.helpText = "Format: `email:password`"
|
||||||
|
} else if (selectedService != "torrent") {
|
||||||
|
apiKeyInput.helpText = "Format: `api-key`"
|
||||||
} else {
|
} else {
|
||||||
apiKeyInput.helpText = "If no key is specified, you'll get direct torrents!"
|
apiKeyInput.helpText = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedService === "torrent") {
|
||||||
|
debridStreamProxyPasswordInput.disabled = true;
|
||||||
|
apiKeyInput.disabled = true;
|
||||||
|
} else {
|
||||||
|
debridStreamProxyPasswordInput.disabled = false;
|
||||||
|
apiKeyInput.disabled = false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@@ -613,59 +649,55 @@
|
|||||||
|
|
||||||
<script type="module">
|
<script type="module">
|
||||||
const languagesEmojis = {
|
const languagesEmojis = {
|
||||||
"Unknown": "❓",
|
"multi": "🌎",
|
||||||
"Multi": "🌎",
|
"en": "🇬🇧", // English
|
||||||
"English": "🇬🇧",
|
"ja": "🇯🇵", // Japanese
|
||||||
"Japanese": "🇯🇵",
|
"zh": "🇨🇳", // Chinese
|
||||||
"Chinese": "🇨🇳",
|
"ru": "🇷🇺", // Russian
|
||||||
"Russian": "🇷🇺",
|
"ar": "🇸🇦", // Arabic
|
||||||
"Arabic": "🇸🇦",
|
"pt": "🇵🇹", // Portuguese
|
||||||
"Portuguese": "🇵🇹",
|
"es": "🇪🇸", // Spanish
|
||||||
"Spanish": "🇪🇸",
|
"fr": "🇫🇷", // French
|
||||||
"French": "🇫🇷",
|
"de": "🇩🇪", // German
|
||||||
"German": "🇩🇪",
|
"it": "🇮🇹", // Italian
|
||||||
"Italian": "🇮🇹",
|
"ko": "🇰🇷", // Korean
|
||||||
"Korean": "🇰🇷",
|
"hi": "🇮🇳", // Hindi
|
||||||
"Hindi": "🇮🇳",
|
"bn": "🇧🇩", // Bengali
|
||||||
"Bengali": "🇧🇩",
|
"pa": "🇵🇰", // Punjabi
|
||||||
"Punjabi": "🇵🇰",
|
"mr": "🇮🇳", // Marathi
|
||||||
"Marathi": "🇮🇳",
|
"gu": "🇮🇳", // Gujarati
|
||||||
"Gujarati": "🇮🇳",
|
"ta": "🇮🇳", // Tamil
|
||||||
"Tamil": "🇮🇳",
|
"te": "🇮🇳", // Telugu
|
||||||
"Telugu": "🇮🇳",
|
"kn": "🇮🇳", // Kannada
|
||||||
"Kannada": "🇮🇳",
|
"ml": "🇮🇳", // Malayalam
|
||||||
"Malayalam": "🇮🇳",
|
"th": "🇹🇭", // Thai
|
||||||
"Thai": "🇹🇭",
|
"vi": "🇻🇳", // Vietnamese
|
||||||
"Vietnamese": "🇻🇳",
|
"id": "🇮🇩", // Indonesian
|
||||||
"Indonesian": "🇮🇩",
|
"tr": "🇹🇷", // Turkish
|
||||||
"Turkish": "🇹🇷",
|
"he": "🇮🇱", // Hebrew
|
||||||
"Hebrew": "🇮🇱",
|
"fa": "🇮🇷", // Persian
|
||||||
"Persian": "🇮🇷",
|
"uk": "🇺🇦", // Ukrainian
|
||||||
"Ukrainian": "🇺🇦",
|
"el": "🇬🇷", // Greek
|
||||||
"Greek": "🇬🇷",
|
"lt": "🇱🇹", // Lithuanian
|
||||||
"Lithuanian": "🇱🇹",
|
"lv": "🇱🇻", // Latvian
|
||||||
"Latvian": "🇱🇻",
|
"et": "🇪🇪", // Estonian
|
||||||
"Estonian": "🇪🇪",
|
"pl": "🇵🇱", // Polish
|
||||||
"Polish": "🇵🇱",
|
"cs": "🇨🇿", // Czech
|
||||||
"Czech": "🇨🇿",
|
"sk": "🇸🇰", // Slovak
|
||||||
"Slovak": "🇸🇰",
|
"hu": "🇭🇺", // Hungarian
|
||||||
"Hungarian": "🇭🇺",
|
"ro": "🇷🇴", // Romanian
|
||||||
"Romanian": "🇷🇴",
|
"bg": "🇧🇬", // Bulgarian
|
||||||
"Bulgarian": "🇧🇬",
|
"sr": "🇷🇸", // Serbian
|
||||||
"Serbian": "🇷🇸",
|
"hr": "🇭🇷", // Croatian
|
||||||
"Croatian": "🇭🇷",
|
"sl": "🇸🇮", // Slovenian
|
||||||
"Slovenian": "🇸🇮",
|
"nl": "🇳🇱", // Dutch
|
||||||
"Dutch": "🇳🇱",
|
"da": "🇩🇰", // Danish
|
||||||
"Danish": "🇩🇰",
|
"fi": "🇫🇮", // Finnish
|
||||||
"Finnish": "🇫🇮",
|
"sv": "🇸🇪", // Swedish
|
||||||
"Swedish": "🇸🇪",
|
"no": "🇳🇴", // Norwegian
|
||||||
"Norwegian": "🇳🇴",
|
"ms": "🇲🇾", // Malay
|
||||||
"Malay": "🇲🇾",
|
"la": "💃🏻", // Latino
|
||||||
"Latino": "💃🏻",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let defaultLanguages = [];
|
|
||||||
let defaultResolutions = [];
|
|
||||||
let defaultResultFormat = [];
|
let defaultResultFormat = [];
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", async () => {
|
document.addEventListener("DOMContentLoaded", async () => {
|
||||||
@@ -680,14 +712,14 @@
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
const webConfig = {{webConfig|tojson}};
|
const webConfig = {{webConfig|tojson}};
|
||||||
populateSelect("indexers", webConfig.indexers);
|
|
||||||
populateSelect("languages", webConfig.languages);
|
const resolutionsSelect = document.getElementById("resolutions");
|
||||||
populateSelect("resolutions", webConfig.resolutions);
|
resolutionsSelect.value = ["r2160p", "r1080p", "r720p", "r480p", "r360p", "unknown"];
|
||||||
|
|
||||||
populateSelect("resultFormat", webConfig.resultFormat);
|
populateSelect("resultFormat", webConfig.resultFormat);
|
||||||
document.body.classList.add("ready");
|
|
||||||
defaultLanguages = webConfig.languages;
|
|
||||||
defaultResolutions = webConfig.resolutions;
|
|
||||||
defaultResultFormat = webConfig.resultFormat;
|
defaultResultFormat = webConfig.resultFormat;
|
||||||
|
const resultFormatSelect = document.getElementById("resultFormat");
|
||||||
|
resultFormatSelect.value = defaultResultFormat;
|
||||||
|
|
||||||
// try populate the form from previous settings
|
// try populate the form from previous settings
|
||||||
try {
|
try {
|
||||||
@@ -697,28 +729,86 @@
|
|||||||
console.log("Failed to retrieve or parse settings from URL:", error);
|
console.log("Failed to retrieve or parse settings from URL:", error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// trigger sl-change event on debridService to set initial state
|
||||||
|
const debridServiceSelect = document.getElementById("debridService");
|
||||||
|
debridServiceSelect.dispatchEvent(new Event("sl-change"));
|
||||||
|
|
||||||
|
// Populate language selects
|
||||||
|
populateSelect("languages_required", Object.keys(languagesEmojis));
|
||||||
|
populateSelect("languages_excluded", Object.keys(languagesEmojis));
|
||||||
|
populateSelect("languages_preferred", Object.keys(languagesEmojis));
|
||||||
|
|
||||||
|
document.body.classList.add("ready");
|
||||||
});
|
});
|
||||||
|
|
||||||
function populateSelect(selectId, options) {
|
function populateSelect(selectId, options) {
|
||||||
const selectElement = document.getElementById(selectId);
|
const selectElement = document.getElementById(selectId);
|
||||||
|
|
||||||
|
const languageNames = {
|
||||||
|
"multi": "Multi",
|
||||||
|
"en": "English",
|
||||||
|
"ja": "Japanese",
|
||||||
|
"zh": "Chinese",
|
||||||
|
"ru": "Russian",
|
||||||
|
"ar": "Arabic",
|
||||||
|
"pt": "Portuguese",
|
||||||
|
"es": "Spanish",
|
||||||
|
"fr": "French",
|
||||||
|
"de": "German",
|
||||||
|
"it": "Italian",
|
||||||
|
"ko": "Korean",
|
||||||
|
"hi": "Hindi",
|
||||||
|
"bn": "Bengali",
|
||||||
|
"pa": "Punjabi",
|
||||||
|
"mr": "Marathi",
|
||||||
|
"gu": "Gujarati",
|
||||||
|
"ta": "Tamil",
|
||||||
|
"te": "Telugu",
|
||||||
|
"kn": "Kannada",
|
||||||
|
"ml": "Malayalam",
|
||||||
|
"th": "Thai",
|
||||||
|
"vi": "Vietnamese",
|
||||||
|
"id": "Indonesian",
|
||||||
|
"tr": "Turkish",
|
||||||
|
"he": "Hebrew",
|
||||||
|
"fa": "Persian",
|
||||||
|
"uk": "Ukrainian",
|
||||||
|
"el": "Greek",
|
||||||
|
"lt": "Lithuanian",
|
||||||
|
"lv": "Latvian",
|
||||||
|
"et": "Estonian",
|
||||||
|
"pl": "Polish",
|
||||||
|
"cs": "Czech",
|
||||||
|
"sk": "Slovak",
|
||||||
|
"hu": "Hungarian",
|
||||||
|
"ro": "Romanian",
|
||||||
|
"bg": "Bulgarian",
|
||||||
|
"sr": "Serbian",
|
||||||
|
"hr": "Croatian",
|
||||||
|
"sl": "Slovenian",
|
||||||
|
"nl": "Dutch",
|
||||||
|
"da": "Danish",
|
||||||
|
"fi": "Finnish",
|
||||||
|
"sv": "Swedish",
|
||||||
|
"no": "Norwegian",
|
||||||
|
"ms": "Malay",
|
||||||
|
"la": "Latino"
|
||||||
|
};
|
||||||
|
|
||||||
options.forEach(option => {
|
options.forEach(option => {
|
||||||
const optionElement = document.createElement("sl-option");
|
const optionElement = document.createElement("sl-option");
|
||||||
optionElement.value = option;
|
optionElement.value = option;
|
||||||
|
|
||||||
if (selectId === "languages") {
|
if (selectId.startsWith('languages')) {
|
||||||
// For languages, prepend the flag emoji if it exists
|
|
||||||
const flag = languagesEmojis[option] || '';
|
const flag = languagesEmojis[option] || '';
|
||||||
optionElement.textContent = `${flag} ${option}`;
|
const fullName = languageNames[option] || option;
|
||||||
|
optionElement.textContent = `${flag} ${fullName}`;
|
||||||
} else {
|
} else {
|
||||||
// For other selects, just use the option text as is
|
|
||||||
optionElement.textContent = option;
|
optionElement.textContent = option;
|
||||||
}
|
}
|
||||||
|
|
||||||
selectElement.appendChild(optionElement);
|
selectElement.appendChild(optionElement);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Set the default value
|
|
||||||
selectElement.value = options; // Assuming first option as default
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const installButton = document.querySelector("#install");
|
const installButton = document.querySelector("#install");
|
||||||
@@ -727,37 +817,52 @@
|
|||||||
const copyAlert = document.querySelector('#copyAlert');
|
const copyAlert = document.querySelector('#copyAlert');
|
||||||
|
|
||||||
function getSettings() {
|
function getSettings() {
|
||||||
const indexers = Array.from(document.getElementById("indexers").selectedOptions).map(option => option.value);
|
const languages_required = Array.from(document.getElementById("languages_required").selectedOptions).map(option => option.value);
|
||||||
const languages = Array.from(document.getElementById("languages").selectedOptions).map(option => option.value);
|
const languages_excluded = Array.from(document.getElementById("languages_excluded").selectedOptions).map(option => option.value);
|
||||||
const resolutions = Array.from(document.getElementById("resolutions").selectedOptions).map(option => option.value);
|
const languages_preferred = Array.from(document.getElementById("languages_preferred").selectedOptions).map(option => option.value);
|
||||||
const maxResults = document.getElementById("maxResults").value;
|
const selectedResolutions = Array.from(document.getElementById("resolutions").selectedOptions).map(option => option.value);
|
||||||
|
const allResolutions = ["r2160p", "r1080p", "r720p", "r480p", "r360p", "unknown"];
|
||||||
|
const resolutions = allResolutions.reduce((obj, res) => {
|
||||||
|
// Only include resolutions that are NOT selected (disabled) as false
|
||||||
|
if (!selectedResolutions.includes(res)) {
|
||||||
|
obj[res] = false;
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
}, {});
|
||||||
|
const rankThreshold = document.getElementById("rankThreshold").value;
|
||||||
const maxResultsPerResolution = document.getElementById("maxResultsPerResolution").value;
|
const maxResultsPerResolution = document.getElementById("maxResultsPerResolution").value;
|
||||||
const maxSize = document.getElementById("maxSize").value;
|
const maxSize = document.getElementById("maxSize").value;
|
||||||
const reverseResultOrder = document.getElementById("reverseResultOrder").checked;
|
const cachedOnly = document.getElementById("cachedOnly").checked;
|
||||||
const removeTrash = document.getElementById("removeTrash").checked;
|
const removeTrash = document.getElementById("removeTrash").checked;
|
||||||
|
const allowEnglishInLanguages = document.getElementById("allowEnglishInLanguages").checked;
|
||||||
|
const removeUnknownLanguages = document.getElementById("removeUnknownLanguages").checked;
|
||||||
const resultFormat = Array.from(document.getElementById("resultFormat").selectedOptions).map(option => option.value);
|
const resultFormat = Array.from(document.getElementById("resultFormat").selectedOptions).map(option => option.value);
|
||||||
const debridService = document.getElementById("debridService").value;
|
const debridService = document.getElementById("debridService").value;
|
||||||
const debridApiKey = document.getElementById("debridApiKey").value;
|
const debridApiKey = document.getElementById("debridApiKey").value;
|
||||||
const stremthruUrl = document.getElementById("stremthruUrl").value;
|
|
||||||
const debridStreamProxyPassword = document.getElementById("debridStreamProxyPassword").value;
|
const debridStreamProxyPassword = document.getElementById("debridStreamProxyPassword").value;
|
||||||
const selectedLanguages = languages.length === defaultLanguages.length && languages.every((val, index) => val === defaultLanguages[index]) ? ["All"] : languages;
|
|
||||||
const selectedResolutions = resolutions.length === defaultResolutions.length && resolutions.every((val, index) => val === defaultResolutions[index]) ? ["All"] : resolutions;
|
const selectedResultFormat = resultFormat.length === defaultResultFormat.length && resultFormat.every((val, index) => val === defaultResultFormat[index]) ? ["all"] : resultFormat;
|
||||||
const selectedResultFormat = resultFormat.length === defaultResultFormat.length && resultFormat.every((val, index) => val === defaultResultFormat[index]) ? ["All"] : resultFormat;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
indexers: indexers,
|
|
||||||
maxResults: parseInt(maxResults),
|
|
||||||
maxResultsPerResolution: parseInt(maxResultsPerResolution),
|
maxResultsPerResolution: parseInt(maxResultsPerResolution),
|
||||||
maxSize: parseFloat(maxSize * 1073741824),
|
maxSize: parseFloat(maxSize * 1073741824),
|
||||||
reverseResultOrder: reverseResultOrder,
|
cachedOnly: cachedOnly,
|
||||||
removeTrash: removeTrash,
|
removeTrash: removeTrash,
|
||||||
resultFormat: selectedResultFormat,
|
resultFormat: selectedResultFormat,
|
||||||
resolutions: selectedResolutions,
|
|
||||||
languages: selectedLanguages,
|
|
||||||
debridService: debridService,
|
debridService: debridService,
|
||||||
debridApiKey: debridApiKey,
|
debridApiKey: debridApiKey,
|
||||||
stremthruUrl: stremthruUrl,
|
|
||||||
debridStreamProxyPassword: debridStreamProxyPassword,
|
debridStreamProxyPassword: debridStreamProxyPassword,
|
||||||
|
languages: {
|
||||||
|
required: languages_required,
|
||||||
|
exclude: languages_excluded,
|
||||||
|
preferred: languages_preferred
|
||||||
|
},
|
||||||
|
resolutions: resolutions,
|
||||||
|
options: {
|
||||||
|
remove_ranks_under: parseFloat(rankThreshold),
|
||||||
|
allow_english_in_languages: allowEnglishInLanguages,
|
||||||
|
remove_unknown_languages: removeUnknownLanguages
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -802,10 +907,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function populateFormFromSettings(settings) {
|
function populateFormFromSettings(settings) {
|
||||||
if (settings.maxResults !== null)
|
if (settings.cachedOnly !== null)
|
||||||
document.getElementById("maxResults").value = settings.maxResults;
|
document.getElementById("cachedOnly").checked = settings.cachedOnly;
|
||||||
if (settings.reverseResultOrder !== null)
|
|
||||||
document.getElementById("reverseResultOrder").checked = settings.reverseResultOrder;
|
|
||||||
if (settings.removeTrash !== null)
|
if (settings.removeTrash !== null)
|
||||||
document.getElementById("removeTrash").checked = settings.removeTrash;
|
document.getElementById("removeTrash").checked = settings.removeTrash;
|
||||||
if (settings.maxResultsPerResolution !== null)
|
if (settings.maxResultsPerResolution !== null)
|
||||||
@@ -818,18 +921,28 @@
|
|||||||
document.getElementById("debridApiKey").value = settings.debridApiKey;
|
document.getElementById("debridApiKey").value = settings.debridApiKey;
|
||||||
if (settings.debridStreamProxyPassword !== null)
|
if (settings.debridStreamProxyPassword !== null)
|
||||||
document.getElementById("debridStreamProxyPassword").value = settings.debridStreamProxyPassword;
|
document.getElementById("debridStreamProxyPassword").value = settings.debridStreamProxyPassword;
|
||||||
if (settings.indexers !== null)
|
if (settings.resultFormat !== null && settings.resultFormat != "all")
|
||||||
document.getElementById("indexers").value = settings.indexers;
|
|
||||||
if (settings.languages !== null && settings.languages != "All")
|
|
||||||
document.getElementById("languages").value = settings.languages;
|
|
||||||
if (settings.stremthruUrl !== null)
|
|
||||||
document.getElementById("stremthruUrl").value = settings.stremthruUrl;
|
|
||||||
if (settings.resolutions !== null && settings.resolutions != "All")
|
|
||||||
document.getElementById("resolutions").value = settings.resolutions;
|
|
||||||
if (settings.resultFormat !== null && settings.resultFormat != "All")
|
|
||||||
document.getElementById("resultFormat").value = settings.resultFormat;
|
document.getElementById("resultFormat").value = settings.resultFormat;
|
||||||
|
if (settings.languages) {
|
||||||
|
if (settings.languages.required)
|
||||||
|
document.getElementById("languages_required").value = settings.languages.required;
|
||||||
|
if (settings.languages.exclude)
|
||||||
|
document.getElementById("languages_excluded").value = settings.languages.exclude;
|
||||||
|
if (settings.languages.preferred)
|
||||||
|
document.getElementById("languages_preferred").value = settings.languages.preferred;
|
||||||
|
}
|
||||||
|
if (settings.resolutions) {
|
||||||
|
const allResolutions = ["r2160p", "r1080p", "r720p", "r480p", "r360p", "unknown"];
|
||||||
|
const enabledResolutions = allResolutions.filter(res => settings.resolutions[res] !== false);
|
||||||
|
document.getElementById("resolutions").value = enabledResolutions;
|
||||||
|
}
|
||||||
|
if (settings.options?.remove_ranks_under !== undefined)
|
||||||
|
document.getElementById("rankThreshold").value = settings.options.remove_ranks_under;
|
||||||
|
if (settings.options?.allow_english_in_languages !== undefined)
|
||||||
|
document.getElementById("allowEnglishInLanguages").checked = settings.options.allow_english_in_languages;
|
||||||
|
if (settings.options?.remove_unknown_languages !== undefined)
|
||||||
|
document.getElementById("removeUnknownLanguages").checked = settings.options.remove_unknown_languages;
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,318 @@
|
|||||||
|
import os
|
||||||
|
import time
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
from comet.utils.logger import logger
|
||||||
|
from comet.utils.models import database, settings
|
||||||
|
|
||||||
|
DATABASE_VERSION = "1.0"
|
||||||
|
|
||||||
|
async def setup_database():
|
||||||
|
try:
|
||||||
|
if settings.DATABASE_TYPE == "sqlite":
|
||||||
|
os.makedirs(os.path.dirname(settings.DATABASE_PATH), exist_ok=True)
|
||||||
|
|
||||||
|
if not os.path.exists(settings.DATABASE_PATH):
|
||||||
|
open(settings.DATABASE_PATH, "a").close()
|
||||||
|
|
||||||
|
await database.connect()
|
||||||
|
|
||||||
|
await database.execute(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS db_version (
|
||||||
|
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||||||
|
version TEXT
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
current_version = await database.fetch_val(
|
||||||
|
"""
|
||||||
|
SELECT version FROM db_version WHERE id = 1
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
if current_version != DATABASE_VERSION:
|
||||||
|
logger.log("COMET", f"Database: Migration from {current_version} to {DATABASE_VERSION} version")
|
||||||
|
|
||||||
|
if settings.DATABASE_TYPE == "sqlite":
|
||||||
|
tables = await database.fetch_all(
|
||||||
|
"""
|
||||||
|
SELECT name FROM sqlite_master
|
||||||
|
WHERE type='table' AND name != 'db_version' AND name != 'sqlite_sequence'
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
for table in tables:
|
||||||
|
await database.execute(f"DROP TABLE IF EXISTS {table['name']}")
|
||||||
|
else:
|
||||||
|
await database.execute(
|
||||||
|
"""
|
||||||
|
DO $$ DECLARE
|
||||||
|
r RECORD;
|
||||||
|
BEGIN
|
||||||
|
FOR r IN (SELECT tablename FROM pg_tables WHERE schemaname = current_schema() AND tablename != 'db_version') LOOP
|
||||||
|
EXECUTE 'DROP TABLE IF EXISTS ' || quote_ident(r.tablename) || ' CASCADE';
|
||||||
|
END LOOP;
|
||||||
|
END $$;
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
await database.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO db_version VALUES (1, :version)
|
||||||
|
ON CONFLICT (id) DO UPDATE SET version = :version
|
||||||
|
""",
|
||||||
|
{"version": DATABASE_VERSION},
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.log("COMET", f"Database: Migration to version {DATABASE_VERSION} completed")
|
||||||
|
|
||||||
|
await database.execute(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS ongoing_searches (
|
||||||
|
media_id TEXT PRIMARY KEY,
|
||||||
|
timestamp INTEGER
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
await database.execute(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS first_searches (
|
||||||
|
media_id TEXT PRIMARY KEY,
|
||||||
|
timestamp INTEGER
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
await database.execute(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS metadata_cache (
|
||||||
|
media_id TEXT PRIMARY KEY,
|
||||||
|
title TEXT,
|
||||||
|
year INTEGER,
|
||||||
|
year_end INTEGER,
|
||||||
|
aliases TEXT,
|
||||||
|
timestamp INTEGER
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
await database.execute(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS torrents (
|
||||||
|
media_id TEXT,
|
||||||
|
info_hash TEXT,
|
||||||
|
file_index INTEGER,
|
||||||
|
season INTEGER,
|
||||||
|
episode INTEGER,
|
||||||
|
title TEXT,
|
||||||
|
seeders INTEGER,
|
||||||
|
size BIGINT,
|
||||||
|
tracker TEXT,
|
||||||
|
sources TEXT,
|
||||||
|
parsed TEXT,
|
||||||
|
timestamp INTEGER
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
await database.execute(
|
||||||
|
"""
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS torrents_series_both_idx
|
||||||
|
ON torrents (media_id, info_hash, season, episode)
|
||||||
|
WHERE season IS NOT NULL AND episode IS NOT NULL
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
await database.execute(
|
||||||
|
"""
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS torrents_season_only_idx
|
||||||
|
ON torrents (media_id, info_hash, season)
|
||||||
|
WHERE season IS NOT NULL AND episode IS NULL
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
await database.execute(
|
||||||
|
"""
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS torrents_episode_only_idx
|
||||||
|
ON torrents (media_id, info_hash, episode)
|
||||||
|
WHERE season IS NULL AND episode IS NOT NULL
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
await database.execute(
|
||||||
|
"""
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS torrents_no_season_episode_idx
|
||||||
|
ON torrents (media_id, info_hash)
|
||||||
|
WHERE season IS NULL AND episode IS NULL
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
await database.execute(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS debrid_availability (
|
||||||
|
debrid_service TEXT,
|
||||||
|
info_hash TEXT,
|
||||||
|
file_index TEXT,
|
||||||
|
title TEXT,
|
||||||
|
season INTEGER,
|
||||||
|
episode INTEGER,
|
||||||
|
size BIGINT,
|
||||||
|
parsed TEXT,
|
||||||
|
timestamp INTEGER
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
await database.execute(
|
||||||
|
"""
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS debrid_series_both_idx
|
||||||
|
ON debrid_availability (debrid_service, info_hash, season, episode)
|
||||||
|
WHERE season IS NOT NULL AND episode IS NOT NULL
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
await database.execute(
|
||||||
|
"""
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS debrid_season_only_idx
|
||||||
|
ON debrid_availability (debrid_service, info_hash, season)
|
||||||
|
WHERE season IS NOT NULL AND episode IS NULL
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
await database.execute(
|
||||||
|
"""
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS debrid_episode_only_idx
|
||||||
|
ON debrid_availability (debrid_service, info_hash, episode)
|
||||||
|
WHERE season IS NULL AND episode IS NOT NULL
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
await database.execute(
|
||||||
|
"""
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS debrid_no_season_episode_idx
|
||||||
|
ON debrid_availability (debrid_service, info_hash)
|
||||||
|
WHERE season IS NULL AND episode IS NULL
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
await database.execute(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS download_links_cache (
|
||||||
|
debrid_key TEXT,
|
||||||
|
info_hash TEXT,
|
||||||
|
season INTEGER,
|
||||||
|
episode INTEGER,
|
||||||
|
download_url TEXT,
|
||||||
|
timestamp INTEGER
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
await database.execute(
|
||||||
|
"""
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS download_links_series_both_idx
|
||||||
|
ON download_links_cache (debrid_key, info_hash, season, episode)
|
||||||
|
WHERE season IS NOT NULL AND episode IS NOT NULL
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
await database.execute(
|
||||||
|
"""
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS download_links_season_only_idx
|
||||||
|
ON download_links_cache (debrid_key, info_hash, season)
|
||||||
|
WHERE season IS NOT NULL AND episode IS NULL
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
await database.execute(
|
||||||
|
"""
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS download_links_episode_only_idx
|
||||||
|
ON download_links_cache (debrid_key, info_hash, episode)
|
||||||
|
WHERE season IS NULL AND episode IS NOT NULL
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
await database.execute(
|
||||||
|
"""
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS download_links_no_season_episode_idx
|
||||||
|
ON download_links_cache (debrid_key, info_hash)
|
||||||
|
WHERE season IS NULL AND episode IS NULL
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
await database.execute(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS active_connections (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
ip TEXT,
|
||||||
|
content TEXT,
|
||||||
|
timestamp INTEGER
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
if settings.DATABASE_TYPE == "sqlite":
|
||||||
|
await database.execute("PRAGMA busy_timeout=30000") # 30 seconds timeout
|
||||||
|
await database.execute("PRAGMA journal_mode=OFF")
|
||||||
|
await database.execute("PRAGMA synchronous=OFF")
|
||||||
|
await database.execute("PRAGMA temp_store=MEMORY")
|
||||||
|
await database.execute("PRAGMA mmap_size=30000000000")
|
||||||
|
await database.execute("PRAGMA page_size=4096")
|
||||||
|
await database.execute("PRAGMA cache_size=-2000")
|
||||||
|
await database.execute("PRAGMA foreign_keys=OFF")
|
||||||
|
await database.execute("PRAGMA count_changes=OFF")
|
||||||
|
await database.execute("PRAGMA secure_delete=OFF")
|
||||||
|
await database.execute("PRAGMA auto_vacuum=OFF")
|
||||||
|
|
||||||
|
await database.execute("DELETE FROM ongoing_searches")
|
||||||
|
|
||||||
|
await database.execute(
|
||||||
|
"""
|
||||||
|
DELETE FROM first_searches
|
||||||
|
WHERE timestamp + :cache_ttl < :current_time;
|
||||||
|
""",
|
||||||
|
{"cache_ttl": settings.TORRENT_CACHE_TTL, "current_time": time.time()},
|
||||||
|
)
|
||||||
|
|
||||||
|
await database.execute(
|
||||||
|
"""
|
||||||
|
DELETE FROM metadata_cache
|
||||||
|
WHERE timestamp + :cache_ttl < :current_time;
|
||||||
|
""",
|
||||||
|
{"cache_ttl": settings.METADATA_CACHE_TTL, "current_time": time.time()},
|
||||||
|
)
|
||||||
|
|
||||||
|
await database.execute(
|
||||||
|
"""
|
||||||
|
DELETE FROM torrents
|
||||||
|
WHERE timestamp + :cache_ttl < :current_time;
|
||||||
|
""",
|
||||||
|
{"cache_ttl": settings.TORRENT_CACHE_TTL, "current_time": time.time()},
|
||||||
|
)
|
||||||
|
|
||||||
|
await database.execute(
|
||||||
|
"""
|
||||||
|
DELETE FROM debrid_availability
|
||||||
|
WHERE timestamp + :cache_ttl < :current_time;
|
||||||
|
""",
|
||||||
|
{"cache_ttl": settings.DEBRID_CACHE_TTL, "current_time": time.time()},
|
||||||
|
)
|
||||||
|
|
||||||
|
await database.execute("DELETE FROM download_links_cache")
|
||||||
|
|
||||||
|
await database.execute("DELETE FROM active_connections")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error setting up the database: {e}")
|
||||||
|
logger.exception(traceback.format_exc())
|
||||||
|
|
||||||
|
|
||||||
|
async def teardown_database():
|
||||||
|
try:
|
||||||
|
await database.disconnect()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error tearing down the database: {e}")
|
||||||
|
logger.exception(traceback.format_exc())
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
import time
|
||||||
|
import orjson
|
||||||
|
|
||||||
|
from comet.utils.models import settings, database
|
||||||
|
from comet.utils.general import default_dump
|
||||||
|
|
||||||
|
|
||||||
|
async def cache_availability(debrid_service: str, availability: list):
|
||||||
|
current_time = time.time()
|
||||||
|
|
||||||
|
values = [
|
||||||
|
{
|
||||||
|
"debrid_service": debrid_service,
|
||||||
|
"info_hash": file["info_hash"],
|
||||||
|
"file_index": str(file["index"]) if file["index"] is not None else None,
|
||||||
|
"title": file["title"],
|
||||||
|
"season": file["season"],
|
||||||
|
"episode": file["episode"],
|
||||||
|
"size": file["size"] if file["index"] is not None else None,
|
||||||
|
"parsed": orjson.dumps(file["parsed"], default_dump).decode("utf-8")
|
||||||
|
if file["parsed"] is not None
|
||||||
|
else None,
|
||||||
|
"timestamp": current_time,
|
||||||
|
}
|
||||||
|
for file in availability
|
||||||
|
]
|
||||||
|
|
||||||
|
if settings.DATABASE_TYPE == "sqlite":
|
||||||
|
query = """
|
||||||
|
INSERT OR REPLACE
|
||||||
|
INTO debrid_availability
|
||||||
|
VALUES (:debrid_service, :info_hash, :file_index, :title, :season, :episode, :size, :parsed, :timestamp)
|
||||||
|
"""
|
||||||
|
await database.execute_many(query, values)
|
||||||
|
elif settings.DATABASE_TYPE == "postgresql":
|
||||||
|
both_values = []
|
||||||
|
season_only_values = []
|
||||||
|
episode_only_values = []
|
||||||
|
no_season_episode_values = []
|
||||||
|
|
||||||
|
for val in values:
|
||||||
|
if val["season"] is not None and val["episode"] is not None:
|
||||||
|
both_values.append(val)
|
||||||
|
elif val["season"] is not None and val["episode"] is None:
|
||||||
|
season_only_values.append(val)
|
||||||
|
elif val["season"] is None and val["episode"] is not None:
|
||||||
|
episode_only_values.append(val)
|
||||||
|
else:
|
||||||
|
no_season_episode_values.append(val)
|
||||||
|
|
||||||
|
# handle each case separately with appropriate ON CONFLICT clauses
|
||||||
|
if both_values:
|
||||||
|
query = """
|
||||||
|
INSERT INTO debrid_availability
|
||||||
|
VALUES (:debrid_service, :info_hash, :file_index, :title, :season, :episode, :size, :parsed, :timestamp)
|
||||||
|
ON CONFLICT (debrid_service, info_hash, season, episode)
|
||||||
|
WHERE season IS NOT NULL AND episode IS NOT NULL
|
||||||
|
DO UPDATE SET
|
||||||
|
title = EXCLUDED.title,
|
||||||
|
file_index = EXCLUDED.file_index,
|
||||||
|
size = EXCLUDED.size,
|
||||||
|
parsed = EXCLUDED.parsed,
|
||||||
|
timestamp = EXCLUDED.timestamp
|
||||||
|
"""
|
||||||
|
await database.execute_many(query, both_values)
|
||||||
|
|
||||||
|
if season_only_values:
|
||||||
|
query = """
|
||||||
|
INSERT INTO debrid_availability
|
||||||
|
VALUES (:debrid_service, :info_hash, :file_index, :title, :season, :episode, :size, :parsed, :timestamp)
|
||||||
|
ON CONFLICT (debrid_service, info_hash, season)
|
||||||
|
WHERE season IS NOT NULL AND episode IS NULL
|
||||||
|
DO UPDATE SET
|
||||||
|
title = EXCLUDED.title,
|
||||||
|
file_index = EXCLUDED.file_index,
|
||||||
|
size = EXCLUDED.size,
|
||||||
|
parsed = EXCLUDED.parsed,
|
||||||
|
timestamp = EXCLUDED.timestamp
|
||||||
|
"""
|
||||||
|
await database.execute_many(query, season_only_values)
|
||||||
|
|
||||||
|
if episode_only_values:
|
||||||
|
query = """
|
||||||
|
INSERT INTO debrid_availability
|
||||||
|
VALUES (:debrid_service, :info_hash, :file_index, :title, :season, :episode, :size, :parsed, :timestamp)
|
||||||
|
ON CONFLICT (debrid_service, info_hash, episode)
|
||||||
|
WHERE season IS NULL AND episode IS NOT NULL
|
||||||
|
DO UPDATE SET
|
||||||
|
title = EXCLUDED.title,
|
||||||
|
file_index = EXCLUDED.file_index,
|
||||||
|
size = EXCLUDED.size,
|
||||||
|
parsed = EXCLUDED.parsed,
|
||||||
|
timestamp = EXCLUDED.timestamp
|
||||||
|
"""
|
||||||
|
await database.execute_many(query, episode_only_values)
|
||||||
|
|
||||||
|
if no_season_episode_values:
|
||||||
|
query = """
|
||||||
|
INSERT INTO debrid_availability
|
||||||
|
VALUES (:debrid_service, :info_hash, :file_index, :title, :season, :episode, :size, :parsed, :timestamp)
|
||||||
|
ON CONFLICT (debrid_service, info_hash)
|
||||||
|
WHERE season IS NULL AND episode IS NULL
|
||||||
|
DO UPDATE SET
|
||||||
|
title = EXCLUDED.title,
|
||||||
|
file_index = EXCLUDED.file_index,
|
||||||
|
size = EXCLUDED.size,
|
||||||
|
parsed = EXCLUDED.parsed,
|
||||||
|
timestamp = EXCLUDED.timestamp
|
||||||
|
"""
|
||||||
|
await database.execute_many(query, no_season_episode_values)
|
||||||
|
else:
|
||||||
|
query = """
|
||||||
|
INSERT
|
||||||
|
INTO debrid_availability
|
||||||
|
VALUES (:debrid_service, :info_hash, :file_index, :title, :season, :episode, :size, :parsed, :timestamp)
|
||||||
|
"""
|
||||||
|
await database.execute_many(query, values)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_cached_availability(
|
||||||
|
debrid_service: str, info_hashes: list, season: int = None, episode: int = None
|
||||||
|
):
|
||||||
|
base_query = f"""
|
||||||
|
SELECT info_hash, file_index, title, size, parsed
|
||||||
|
FROM debrid_availability
|
||||||
|
WHERE info_hash IN (SELECT cast(value as TEXT) FROM {"json_array_elements_text" if settings.DATABASE_TYPE == "postgresql" else "json_each"}(:info_hashes))
|
||||||
|
AND debrid_service = :debrid_service
|
||||||
|
AND timestamp + :cache_ttl >= :current_time
|
||||||
|
"""
|
||||||
|
|
||||||
|
params = {
|
||||||
|
"info_hashes": orjson.dumps(info_hashes).decode("utf-8"),
|
||||||
|
"debrid_service": debrid_service,
|
||||||
|
"cache_ttl": settings.DEBRID_CACHE_TTL,
|
||||||
|
"current_time": time.time(),
|
||||||
|
"season": season,
|
||||||
|
"episode": episode,
|
||||||
|
}
|
||||||
|
|
||||||
|
if debrid_service == "offcloud":
|
||||||
|
query = (
|
||||||
|
base_query
|
||||||
|
+ """
|
||||||
|
AND ((cast(:season as INTEGER) IS NULL AND season IS NULL) OR season = cast(:season as INTEGER))
|
||||||
|
AND ((cast(:episode as INTEGER) IS NULL AND episode IS NULL) OR episode = cast(:episode as INTEGER))
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
results = await database.fetch_all(query, params)
|
||||||
|
|
||||||
|
found_hashes = {r["info_hash"] for r in results}
|
||||||
|
remaining_hashes = [h for h in info_hashes if h not in found_hashes]
|
||||||
|
|
||||||
|
if remaining_hashes:
|
||||||
|
null_title_params = {
|
||||||
|
"info_hashes": orjson.dumps(remaining_hashes).decode("utf-8"),
|
||||||
|
"debrid_service": debrid_service,
|
||||||
|
"cache_ttl": settings.DEBRID_CACHE_TTL,
|
||||||
|
"current_time": time.time(),
|
||||||
|
}
|
||||||
|
null_title_query = base_query + " AND title IS NULL"
|
||||||
|
null_results = await database.fetch_all(null_title_query, null_title_params)
|
||||||
|
results.extend(null_results)
|
||||||
|
else:
|
||||||
|
query = (
|
||||||
|
base_query
|
||||||
|
+ """
|
||||||
|
AND ((cast(:season as INTEGER) IS NULL AND season IS NULL) OR season = cast(:season as INTEGER))
|
||||||
|
AND ((cast(:episode as INTEGER) IS NULL AND episode IS NULL) OR episode = cast(:episode as INTEGER))
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
results = await database.fetch_all(query, params)
|
||||||
|
|
||||||
|
return results
|
||||||
+179
-657
@@ -1,20 +1,101 @@
|
|||||||
import base64
|
import base64
|
||||||
import hashlib
|
|
||||||
import re
|
|
||||||
import aiohttp
|
|
||||||
import bencodepy
|
|
||||||
import PTT
|
|
||||||
import asyncio
|
|
||||||
import orjson
|
import orjson
|
||||||
import time
|
|
||||||
import copy
|
|
||||||
|
|
||||||
from RTN import parse, title_match
|
from RTN import ParsedData
|
||||||
from curl_cffi import requests
|
|
||||||
from fastapi import Request
|
from fastapi import Request
|
||||||
|
|
||||||
from comet.utils.logger import logger
|
from comet.utils.models import (
|
||||||
from comet.utils.models import database, settings, ConfigModel
|
ConfigModel,
|
||||||
|
default_config,
|
||||||
|
settings,
|
||||||
|
rtn_settings_default,
|
||||||
|
rtn_ranking_default,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def config_check(b64config: str):
|
||||||
|
try:
|
||||||
|
config = orjson.loads(base64.b64decode(b64config).decode())
|
||||||
|
|
||||||
|
if "indexers" in config:
|
||||||
|
return False
|
||||||
|
|
||||||
|
validated_config = ConfigModel(**config)
|
||||||
|
validated_config = validated_config.model_dump()
|
||||||
|
|
||||||
|
for key in list(validated_config["options"].keys()):
|
||||||
|
if key not in [
|
||||||
|
"remove_ranks_under",
|
||||||
|
"allow_english_in_languages",
|
||||||
|
"remove_unknown_languages",
|
||||||
|
]:
|
||||||
|
validated_config["options"].pop(key)
|
||||||
|
|
||||||
|
validated_config["options"]["remove_all_trash"] = validated_config[
|
||||||
|
"removeTrash"
|
||||||
|
]
|
||||||
|
|
||||||
|
rtn_settings = rtn_settings_default.model_copy(
|
||||||
|
update={
|
||||||
|
"resolutions": rtn_settings_default.resolutions.model_copy(
|
||||||
|
update=validated_config["resolutions"]
|
||||||
|
),
|
||||||
|
"options": rtn_settings_default.options.model_copy(
|
||||||
|
update=validated_config["options"]
|
||||||
|
),
|
||||||
|
"languages": rtn_settings_default.languages.model_copy(
|
||||||
|
update=validated_config["languages"]
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
validated_config["rtnSettings"] = rtn_settings
|
||||||
|
validated_config["rtnRanking"] = rtn_ranking_default
|
||||||
|
|
||||||
|
if (
|
||||||
|
settings.PROXY_DEBRID_STREAM
|
||||||
|
and settings.PROXY_DEBRID_STREAM_PASSWORD
|
||||||
|
== validated_config["debridStreamProxyPassword"]
|
||||||
|
and validated_config["debridApiKey"] == ""
|
||||||
|
):
|
||||||
|
validated_config["debridService"] = (
|
||||||
|
settings.PROXY_DEBRID_STREAM_DEBRID_DEFAULT_SERVICE
|
||||||
|
)
|
||||||
|
validated_config["debridApiKey"] = (
|
||||||
|
settings.PROXY_DEBRID_STREAM_DEBRID_DEFAULT_APIKEY
|
||||||
|
)
|
||||||
|
|
||||||
|
return validated_config
|
||||||
|
except Exception:
|
||||||
|
return default_config # if it doesn't pass, return default config
|
||||||
|
|
||||||
|
|
||||||
|
def bytes_to_size(bytes: int):
|
||||||
|
sizes = ["Bytes", "KB", "MB", "GB", "TB"]
|
||||||
|
if bytes == 0:
|
||||||
|
return "0 Byte"
|
||||||
|
|
||||||
|
i = 0
|
||||||
|
while bytes >= 1024 and i < len(sizes) - 1:
|
||||||
|
bytes /= 1024
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
return f"{round(bytes, 2)} {sizes[i]}"
|
||||||
|
|
||||||
|
|
||||||
|
def size_to_bytes(size_str: str):
|
||||||
|
sizes = ["b", "kb", "mb", "gb", "tb"]
|
||||||
|
|
||||||
|
value, unit = size_str.split()
|
||||||
|
value = float(value)
|
||||||
|
unit = unit.lower()
|
||||||
|
|
||||||
|
if unit not in sizes:
|
||||||
|
return None
|
||||||
|
|
||||||
|
multiplier = 1024 ** sizes.index(unit)
|
||||||
|
return int(value * multiplier)
|
||||||
|
|
||||||
|
|
||||||
languages_emojis = {
|
languages_emojis = {
|
||||||
"unknown": "❓", # Unknown
|
"unknown": "❓", # Unknown
|
||||||
@@ -78,104 +159,78 @@ def get_language_emoji(language: str):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
translation_table = {
|
def format_metadata(data: ParsedData):
|
||||||
"ā": "a",
|
extras = []
|
||||||
"ă": "a",
|
if data.quality:
|
||||||
"ą": "a",
|
extras.append(data.quality)
|
||||||
"ć": "c",
|
if data.hdr:
|
||||||
"č": "c",
|
extras.extend(data.hdr)
|
||||||
"ç": "c",
|
if data.codec:
|
||||||
"ĉ": "c",
|
extras.append(data.codec)
|
||||||
"ċ": "c",
|
if data.audio:
|
||||||
"ď": "d",
|
extras.extend(data.audio)
|
||||||
"đ": "d",
|
if data.channels:
|
||||||
"è": "e",
|
extras.extend(data.channels)
|
||||||
"é": "e",
|
if data.bit_depth:
|
||||||
"ê": "e",
|
extras.append(data.bit_depth)
|
||||||
"ë": "e",
|
if data.network:
|
||||||
"ē": "e",
|
extras.append(data.network)
|
||||||
"ĕ": "e",
|
if data.group:
|
||||||
"ę": "e",
|
extras.append(data.group)
|
||||||
"ě": "e",
|
|
||||||
"ĝ": "g",
|
|
||||||
"ğ": "g",
|
|
||||||
"ġ": "g",
|
|
||||||
"ģ": "g",
|
|
||||||
"ĥ": "h",
|
|
||||||
"î": "i",
|
|
||||||
"ï": "i",
|
|
||||||
"ì": "i",
|
|
||||||
"í": "i",
|
|
||||||
"ī": "i",
|
|
||||||
"ĩ": "i",
|
|
||||||
"ĭ": "i",
|
|
||||||
"ı": "i",
|
|
||||||
"ĵ": "j",
|
|
||||||
"ķ": "k",
|
|
||||||
"ĺ": "l",
|
|
||||||
"ļ": "l",
|
|
||||||
"ł": "l",
|
|
||||||
"ń": "n",
|
|
||||||
"ň": "n",
|
|
||||||
"ñ": "n",
|
|
||||||
"ņ": "n",
|
|
||||||
"ʼn": "n",
|
|
||||||
"ó": "o",
|
|
||||||
"ô": "o",
|
|
||||||
"õ": "o",
|
|
||||||
"ö": "o",
|
|
||||||
"ø": "o",
|
|
||||||
"ō": "o",
|
|
||||||
"ő": "o",
|
|
||||||
"œ": "oe",
|
|
||||||
"ŕ": "r",
|
|
||||||
"ř": "r",
|
|
||||||
"ŗ": "r",
|
|
||||||
"š": "s",
|
|
||||||
"ş": "s",
|
|
||||||
"ś": "s",
|
|
||||||
"ș": "s",
|
|
||||||
"ß": "ss",
|
|
||||||
"ť": "t",
|
|
||||||
"ţ": "t",
|
|
||||||
"ū": "u",
|
|
||||||
"ŭ": "u",
|
|
||||||
"ũ": "u",
|
|
||||||
"û": "u",
|
|
||||||
"ü": "u",
|
|
||||||
"ù": "u",
|
|
||||||
"ú": "u",
|
|
||||||
"ų": "u",
|
|
||||||
"ű": "u",
|
|
||||||
"ŵ": "w",
|
|
||||||
"ý": "y",
|
|
||||||
"ÿ": "y",
|
|
||||||
"ŷ": "y",
|
|
||||||
"ž": "z",
|
|
||||||
"ż": "z",
|
|
||||||
"ź": "z",
|
|
||||||
"æ": "ae",
|
|
||||||
"ǎ": "a",
|
|
||||||
"ǧ": "g",
|
|
||||||
"ə": "e",
|
|
||||||
"ƒ": "f",
|
|
||||||
"ǐ": "i",
|
|
||||||
"ǒ": "o",
|
|
||||||
"ǔ": "u",
|
|
||||||
"ǚ": "u",
|
|
||||||
"ǜ": "u",
|
|
||||||
"ǹ": "n",
|
|
||||||
"ǻ": "a",
|
|
||||||
"ǽ": "ae",
|
|
||||||
"ǿ": "o",
|
|
||||||
}
|
|
||||||
|
|
||||||
translation_table = str.maketrans(translation_table)
|
return "|".join(extras)
|
||||||
info_hash_pattern = re.compile(r"\b([a-fA-F0-9]{40})\b")
|
|
||||||
|
|
||||||
|
|
||||||
def translate(title: str):
|
def format_title(
|
||||||
return title.translate(translation_table)
|
data: ParsedData,
|
||||||
|
ttitle: str,
|
||||||
|
seeders: int,
|
||||||
|
size: int,
|
||||||
|
tracker: str,
|
||||||
|
result_format: list,
|
||||||
|
):
|
||||||
|
has_all = "all" in result_format
|
||||||
|
|
||||||
|
title = ""
|
||||||
|
if has_all or "title" in result_format:
|
||||||
|
title += f"{ttitle}\n"
|
||||||
|
|
||||||
|
if has_all or "metadata" in result_format:
|
||||||
|
metadata = format_metadata(data)
|
||||||
|
if metadata != "":
|
||||||
|
title += f"💿 {metadata}\n"
|
||||||
|
|
||||||
|
if (has_all or "seeders" in result_format) and seeders is not None:
|
||||||
|
title += f"👤 {seeders} "
|
||||||
|
|
||||||
|
if has_all or "size" in result_format:
|
||||||
|
title += f"💾 {bytes_to_size(size)} "
|
||||||
|
|
||||||
|
if has_all or "tracker" in result_format:
|
||||||
|
title += f"🔎 {tracker}"
|
||||||
|
|
||||||
|
if has_all or "languages" in result_format:
|
||||||
|
languages = data.languages
|
||||||
|
if languages:
|
||||||
|
formatted_languages = "/".join(
|
||||||
|
get_language_emoji(language) for language in languages
|
||||||
|
)
|
||||||
|
languages_str = "\n" + formatted_languages
|
||||||
|
title += f"{languages_str}"
|
||||||
|
|
||||||
|
if title == "":
|
||||||
|
# Without this, Streamio shows SD as the result, which is confusing
|
||||||
|
title = "Empty result format configuration"
|
||||||
|
|
||||||
|
return title
|
||||||
|
|
||||||
|
|
||||||
|
def get_client_ip(request: Request):
|
||||||
|
return (
|
||||||
|
request.headers["cf-connecting-ip"]
|
||||||
|
if "cf-connecting-ip" in request.headers
|
||||||
|
else request.client.host
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def is_video(title: str):
|
def is_video(title: str):
|
||||||
@@ -221,553 +276,20 @@ def is_video(title: str):
|
|||||||
return title.endswith(video_extensions)
|
return title.endswith(video_extensions)
|
||||||
|
|
||||||
|
|
||||||
def bytes_to_size(bytes: int):
|
def default_dump(obj):
|
||||||
sizes = ["Bytes", "KB", "MB", "GB", "TB"]
|
if isinstance(obj, ParsedData):
|
||||||
if bytes == 0:
|
return obj.model_dump()
|
||||||
return "0 Byte"
|
|
||||||
|
|
||||||
i = 0
|
def parse_media_id(media_type: str, media_id: str):
|
||||||
while bytes >= 1024 and i < len(sizes) - 1:
|
if media_type == "series":
|
||||||
bytes /= 1024
|
info = media_id.split(":")
|
||||||
i += 1
|
|
||||||
|
if "kitsu" in media_id:
|
||||||
return f"{round(bytes, 2)} {sizes[i]}"
|
return info[1], 1, int(info[2])
|
||||||
|
|
||||||
|
return info[0], int(info[1]), int(info[2])
|
||||||
def size_to_bytes(size_str: str):
|
elif media_type == "movie" and "kitsu" in media_id:
|
||||||
sizes = ["bytes", "kb", "mb", "gb", "tb"]
|
info = media_id.split(":")
|
||||||
try:
|
return info[0], int(info[1]), None
|
||||||
value, unit = size_str.split()
|
return media_id, None, None
|
||||||
value = float(value)
|
|
||||||
unit = unit.lower()
|
|
||||||
|
|
||||||
if unit not in sizes:
|
|
||||||
return None
|
|
||||||
|
|
||||||
multiplier = 1024 ** sizes.index(unit)
|
|
||||||
return int(value * multiplier)
|
|
||||||
except:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def config_check(b64config: str):
|
|
||||||
try:
|
|
||||||
config = orjson.loads(base64.b64decode(b64config).decode())
|
|
||||||
validated_config = ConfigModel(**config)
|
|
||||||
return validated_config.model_dump()
|
|
||||||
except:
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def get_debrid_extension(debridService: str, debridApiKey: str = None):
|
|
||||||
if debridApiKey == "":
|
|
||||||
return "TORRENT"
|
|
||||||
|
|
||||||
debrid_extensions = {
|
|
||||||
"realdebrid": "RD",
|
|
||||||
"alldebrid": "AD",
|
|
||||||
"premiumize": "PM",
|
|
||||||
"torbox": "TB",
|
|
||||||
"debridlink": "DL",
|
|
||||||
"easydebrid": "ED",
|
|
||||||
"stremthru": "ST",
|
|
||||||
}
|
|
||||||
|
|
||||||
extension = debrid_extensions.get(debridService, None)
|
|
||||||
if extension == "ST" and debridApiKey and ":" in debridApiKey:
|
|
||||||
ext = debrid_extensions[debridApiKey.split(":")[0]]
|
|
||||||
return f"{extension}({ext})" if extension != ext else extension
|
|
||||||
return extension
|
|
||||||
|
|
||||||
|
|
||||||
async def get_indexer_manager(
|
|
||||||
session: aiohttp.ClientSession,
|
|
||||||
indexer_manager_type: str,
|
|
||||||
indexers: list,
|
|
||||||
query: str,
|
|
||||||
):
|
|
||||||
results = []
|
|
||||||
try:
|
|
||||||
indexers = [indexer.replace("_", " ") for indexer in indexers]
|
|
||||||
|
|
||||||
if indexer_manager_type == "jackett":
|
|
||||||
|
|
||||||
async def fetch_jackett_results(
|
|
||||||
session: aiohttp.ClientSession, indexer: str, query: str
|
|
||||||
):
|
|
||||||
try:
|
|
||||||
async with session.get(
|
|
||||||
f"{settings.INDEXER_MANAGER_URL}/api/v2.0/indexers/all/results?apikey={settings.INDEXER_MANAGER_API_KEY}&Query={query}&Tracker[]={indexer}",
|
|
||||||
timeout=aiohttp.ClientTimeout(
|
|
||||||
total=settings.INDEXER_MANAGER_TIMEOUT
|
|
||||||
),
|
|
||||||
) as response:
|
|
||||||
response_json = await response.json()
|
|
||||||
return response_json.get("Results", [])
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(
|
|
||||||
f"Exception while fetching Jackett results for indexer {indexer}: {e}"
|
|
||||||
)
|
|
||||||
return []
|
|
||||||
|
|
||||||
tasks = [
|
|
||||||
fetch_jackett_results(session, indexer, query) for indexer in indexers
|
|
||||||
]
|
|
||||||
all_results = await asyncio.gather(*tasks)
|
|
||||||
|
|
||||||
for result_set in all_results:
|
|
||||||
results.extend(result_set)
|
|
||||||
|
|
||||||
elif indexer_manager_type == "prowlarr":
|
|
||||||
get_indexers = await session.get(
|
|
||||||
f"{settings.INDEXER_MANAGER_URL}/api/v1/indexer",
|
|
||||||
headers={"X-Api-Key": settings.INDEXER_MANAGER_API_KEY},
|
|
||||||
)
|
|
||||||
get_indexers = await get_indexers.json()
|
|
||||||
|
|
||||||
indexers_id = []
|
|
||||||
for indexer in get_indexers:
|
|
||||||
if (
|
|
||||||
indexer["name"].lower() in indexers
|
|
||||||
or indexer["definitionName"].lower() in indexers
|
|
||||||
):
|
|
||||||
indexers_id.append(indexer["id"])
|
|
||||||
|
|
||||||
response = await session.get(
|
|
||||||
f"{settings.INDEXER_MANAGER_URL}/api/v1/search?query={query}&indexerIds={'&indexerIds='.join(str(indexer_id) for indexer_id in indexers_id)}&type=search",
|
|
||||||
headers={"X-Api-Key": settings.INDEXER_MANAGER_API_KEY},
|
|
||||||
)
|
|
||||||
response = await response.json()
|
|
||||||
|
|
||||||
for result in response:
|
|
||||||
result["InfoHash"] = (
|
|
||||||
result["infoHash"] if "infoHash" in result else None
|
|
||||||
)
|
|
||||||
result["Title"] = result["title"]
|
|
||||||
result["Size"] = result["size"]
|
|
||||||
result["Link"] = (
|
|
||||||
result["downloadUrl"] if "downloadUrl" in result else None
|
|
||||||
)
|
|
||||||
result["Tracker"] = result["indexer"]
|
|
||||||
|
|
||||||
results.append(result)
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(
|
|
||||||
f"Exception while getting {indexer_manager_type} results for {query} with {indexers}: {e}"
|
|
||||||
)
|
|
||||||
pass
|
|
||||||
|
|
||||||
return results
|
|
||||||
|
|
||||||
|
|
||||||
async def get_zilean(
|
|
||||||
session: aiohttp.ClientSession, name: str, log_name: str, season: int, episode: int
|
|
||||||
):
|
|
||||||
results = []
|
|
||||||
try:
|
|
||||||
show = f"&season={season}&episode={episode}"
|
|
||||||
get_dmm = await session.get(
|
|
||||||
f"{settings.ZILEAN_URL}/dmm/filtered?query={name}{show if season else ''}"
|
|
||||||
)
|
|
||||||
get_dmm = await get_dmm.json()
|
|
||||||
|
|
||||||
if isinstance(get_dmm, list):
|
|
||||||
take_first = get_dmm[: settings.ZILEAN_TAKE_FIRST]
|
|
||||||
for result in take_first:
|
|
||||||
object = {
|
|
||||||
"Title": result["raw_title"],
|
|
||||||
"InfoHash": result["info_hash"],
|
|
||||||
"Size": int(result["size"]),
|
|
||||||
"Tracker": "DMM",
|
|
||||||
}
|
|
||||||
|
|
||||||
results.append(object)
|
|
||||||
|
|
||||||
logger.info(f"{len(results)} torrents found for {log_name} with Zilean")
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(
|
|
||||||
f"Exception while getting torrents for {log_name} with Zilean: {e}"
|
|
||||||
)
|
|
||||||
pass
|
|
||||||
|
|
||||||
return results
|
|
||||||
|
|
||||||
|
|
||||||
async def get_torrentio(log_name: str, type: str, full_id: str):
|
|
||||||
results = []
|
|
||||||
try:
|
|
||||||
try:
|
|
||||||
get_torrentio = requests.get(
|
|
||||||
f"https://torrentio.strem.fun/stream/{type}/{full_id}.json"
|
|
||||||
).json()
|
|
||||||
except:
|
|
||||||
get_torrentio = requests.get(
|
|
||||||
f"https://torrentio.strem.fun/stream/{type}/{full_id}.json",
|
|
||||||
proxies={
|
|
||||||
"http": settings.DEBRID_PROXY_URL,
|
|
||||||
"https": settings.DEBRID_PROXY_URL,
|
|
||||||
},
|
|
||||||
).json()
|
|
||||||
|
|
||||||
for torrent in get_torrentio["streams"]:
|
|
||||||
title_full = torrent["title"]
|
|
||||||
title = title_full.split("\n")[0]
|
|
||||||
tracker = title_full.split("⚙️ ")[1].split("\n")[0]
|
|
||||||
size = size_to_bytes(title_full.split("💾 ")[1].split(" ⚙️")[0])
|
|
||||||
|
|
||||||
results.append(
|
|
||||||
{
|
|
||||||
"Title": title,
|
|
||||||
"InfoHash": torrent["infoHash"],
|
|
||||||
"Size": size,
|
|
||||||
"Tracker": f"Torrentio|{tracker}",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
logger.info(f"{len(results)} torrents found for {log_name} with Torrentio")
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(
|
|
||||||
f"Exception while getting torrents for {log_name} with Torrentio, your IP is most likely blacklisted (you should try proxying Comet): {e}"
|
|
||||||
)
|
|
||||||
pass
|
|
||||||
|
|
||||||
return results
|
|
||||||
|
|
||||||
|
|
||||||
async def get_mediafusion(log_name: str, type: str, full_id: str):
|
|
||||||
results = []
|
|
||||||
try:
|
|
||||||
try:
|
|
||||||
get_mediafusion = requests.get(
|
|
||||||
f"{settings.MEDIAFUSION_URL}/stream/{type}/{full_id}.json"
|
|
||||||
).json()
|
|
||||||
except:
|
|
||||||
get_mediafusion = requests.get(
|
|
||||||
f"{settings.MEDIAFUSION_URL}/stream/{type}/{full_id}.json",
|
|
||||||
proxies={
|
|
||||||
"http": settings.DEBRID_PROXY_URL,
|
|
||||||
"https": settings.DEBRID_PROXY_URL,
|
|
||||||
},
|
|
||||||
).json()
|
|
||||||
|
|
||||||
for torrent in get_mediafusion["streams"]:
|
|
||||||
title_full = torrent["description"]
|
|
||||||
title = title_full.split("\n")[0].replace("📂 ", "").replace("/", "")
|
|
||||||
tracker = title_full.split("🔗 ")[1]
|
|
||||||
|
|
||||||
results.append(
|
|
||||||
{
|
|
||||||
"Title": title,
|
|
||||||
"InfoHash": torrent["infoHash"],
|
|
||||||
"Size": torrent["behaviorHints"][
|
|
||||||
"videoSize"
|
|
||||||
], # not the pack size but still useful for prowlarr userss
|
|
||||||
"Tracker": f"MediaFusion|{tracker}",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
logger.info(f"{len(results)} torrents found for {log_name} with MediaFusion")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(
|
|
||||||
f"Exception while getting torrents for {log_name} with MediaFusion, your IP is most likely blacklisted (you should try proxying Comet): {e}"
|
|
||||||
)
|
|
||||||
pass
|
|
||||||
|
|
||||||
return results
|
|
||||||
|
|
||||||
|
|
||||||
async def filter(
|
|
||||||
torrents: list,
|
|
||||||
name: str,
|
|
||||||
year: int,
|
|
||||||
year_end: int,
|
|
||||||
aliases: dict,
|
|
||||||
remove_adult_content: bool,
|
|
||||||
):
|
|
||||||
results = []
|
|
||||||
for torrent in torrents:
|
|
||||||
index = torrent[0]
|
|
||||||
title = torrent[1]
|
|
||||||
|
|
||||||
if "\n" in title: # Torrentio title parsing
|
|
||||||
title = title.split("\n")[1]
|
|
||||||
|
|
||||||
parsed = parse(title)
|
|
||||||
|
|
||||||
if remove_adult_content and parsed.adult:
|
|
||||||
results.append((index, False))
|
|
||||||
continue
|
|
||||||
|
|
||||||
if parsed.parsed_title and not title_match(
|
|
||||||
name, parsed.parsed_title, aliases=aliases
|
|
||||||
):
|
|
||||||
results.append((index, False))
|
|
||||||
continue
|
|
||||||
|
|
||||||
if year and parsed.year:
|
|
||||||
if year_end is not None:
|
|
||||||
if not (year <= parsed.year <= year_end):
|
|
||||||
results.append((index, False))
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
if year < (parsed.year - 1) or year > (parsed.year + 1):
|
|
||||||
results.append((index, False))
|
|
||||||
continue
|
|
||||||
|
|
||||||
results.append((index, True))
|
|
||||||
|
|
||||||
return results
|
|
||||||
|
|
||||||
|
|
||||||
async def get_torrent_hash(session: aiohttp.ClientSession, torrent: tuple):
|
|
||||||
index = torrent[0]
|
|
||||||
torrent = torrent[1]
|
|
||||||
if "InfoHash" in torrent and torrent["InfoHash"] is not None:
|
|
||||||
return (index, torrent["InfoHash"].lower())
|
|
||||||
|
|
||||||
url = torrent["Link"]
|
|
||||||
|
|
||||||
try:
|
|
||||||
timeout = aiohttp.ClientTimeout(total=settings.GET_TORRENT_TIMEOUT)
|
|
||||||
response = await session.get(url, allow_redirects=False, timeout=timeout)
|
|
||||||
if response.status == 200:
|
|
||||||
torrent_data = await response.read()
|
|
||||||
torrent_dict = bencodepy.decode(torrent_data)
|
|
||||||
info = bencodepy.encode(torrent_dict[b"info"])
|
|
||||||
hash = hashlib.sha1(info).hexdigest()
|
|
||||||
else:
|
|
||||||
location = response.headers.get("Location", "")
|
|
||||||
if not location:
|
|
||||||
return (index, None)
|
|
||||||
|
|
||||||
match = info_hash_pattern.search(location)
|
|
||||||
if not match:
|
|
||||||
return (index, None)
|
|
||||||
|
|
||||||
hash = match.group(1).upper()
|
|
||||||
|
|
||||||
return (index, hash.lower())
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(
|
|
||||||
f"Exception while getting torrent info hash for {torrent['indexer'] if 'indexer' in torrent else (torrent['Tracker'] if 'Tracker' in torrent else '')}|{url}: {e}"
|
|
||||||
)
|
|
||||||
|
|
||||||
return (index, None)
|
|
||||||
|
|
||||||
|
|
||||||
def get_balanced_hashes(hashes: dict, config: dict):
|
|
||||||
max_results = config["maxResults"]
|
|
||||||
max_results_per_resolution = config["maxResultsPerResolution"]
|
|
||||||
|
|
||||||
max_size = config["maxSize"]
|
|
||||||
config_resolutions = [resolution.lower() for resolution in config["resolutions"]]
|
|
||||||
include_all_resolutions = "all" in config_resolutions
|
|
||||||
remove_trash = config["removeTrash"]
|
|
||||||
|
|
||||||
languages = [language.lower() for language in config["languages"]]
|
|
||||||
include_all_languages = "all" in languages
|
|
||||||
if not include_all_languages:
|
|
||||||
config_languages = [
|
|
||||||
code
|
|
||||||
for code, name in PTT.parse.LANGUAGES_TRANSLATION_TABLE.items()
|
|
||||||
if name.lower() in languages
|
|
||||||
]
|
|
||||||
|
|
||||||
hashes_by_resolution = {}
|
|
||||||
for hash, hash_data in hashes.items():
|
|
||||||
if remove_trash and not hash_data["fetch"]:
|
|
||||||
continue
|
|
||||||
|
|
||||||
hash_info = hash_data["data"]
|
|
||||||
|
|
||||||
if max_size != 0 and hash_info["size"] > max_size:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if (
|
|
||||||
not include_all_languages
|
|
||||||
and not any(lang in hash_info["languages"] for lang in config_languages)
|
|
||||||
and ("multi" not in languages if hash_info["dubbed"] else True)
|
|
||||||
and not (len(hash_info["languages"]) == 0 and "unknown" in languages)
|
|
||||||
):
|
|
||||||
continue
|
|
||||||
|
|
||||||
resolution = hash_info["resolution"]
|
|
||||||
if not include_all_resolutions and resolution not in config_resolutions:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if resolution not in hashes_by_resolution:
|
|
||||||
hashes_by_resolution[resolution] = []
|
|
||||||
hashes_by_resolution[resolution].append(hash)
|
|
||||||
|
|
||||||
if config["reverseResultOrder"]:
|
|
||||||
hashes_by_resolution = {
|
|
||||||
res: lst[::-1] for res, lst in hashes_by_resolution.items()
|
|
||||||
}
|
|
||||||
|
|
||||||
total_resolutions = len(hashes_by_resolution)
|
|
||||||
if max_results == 0 and max_results_per_resolution == 0 or total_resolutions == 0:
|
|
||||||
return hashes_by_resolution
|
|
||||||
|
|
||||||
hashes_per_resolution = (
|
|
||||||
max_results // total_resolutions
|
|
||||||
if max_results > 0
|
|
||||||
else max_results_per_resolution
|
|
||||||
)
|
|
||||||
extra_hashes = max_results % total_resolutions
|
|
||||||
|
|
||||||
balanced_hashes = {}
|
|
||||||
for resolution, hash_list in hashes_by_resolution.items():
|
|
||||||
selected_count = hashes_per_resolution + (1 if extra_hashes > 0 else 0)
|
|
||||||
if max_results_per_resolution > 0:
|
|
||||||
selected_count = min(selected_count, max_results_per_resolution)
|
|
||||||
balanced_hashes[resolution] = hash_list[:selected_count]
|
|
||||||
if extra_hashes > 0:
|
|
||||||
extra_hashes -= 1
|
|
||||||
|
|
||||||
selected_total = sum(len(hashes) for hashes in balanced_hashes.values())
|
|
||||||
if selected_total < max_results:
|
|
||||||
missing_hashes = max_results - selected_total
|
|
||||||
for resolution, hash_list in hashes_by_resolution.items():
|
|
||||||
if missing_hashes <= 0:
|
|
||||||
break
|
|
||||||
current_count = len(balanced_hashes[resolution])
|
|
||||||
available_hashes = hash_list[current_count : current_count + missing_hashes]
|
|
||||||
balanced_hashes[resolution].extend(available_hashes)
|
|
||||||
missing_hashes -= len(available_hashes)
|
|
||||||
|
|
||||||
return balanced_hashes
|
|
||||||
|
|
||||||
|
|
||||||
def format_metadata(data: dict):
|
|
||||||
extras = []
|
|
||||||
if data["quality"]:
|
|
||||||
extras.append(data["quality"])
|
|
||||||
if data["hdr"]:
|
|
||||||
extras.extend(data["hdr"])
|
|
||||||
if data["codec"]:
|
|
||||||
extras.append(data["codec"])
|
|
||||||
if data["audio"]:
|
|
||||||
extras.extend(data["audio"])
|
|
||||||
if data["channels"]:
|
|
||||||
extras.extend(data["channels"])
|
|
||||||
if data["bit_depth"]:
|
|
||||||
extras.append(data["bit_depth"])
|
|
||||||
if data["network"]:
|
|
||||||
extras.append(data["network"])
|
|
||||||
if data["group"]:
|
|
||||||
extras.append(data["group"])
|
|
||||||
|
|
||||||
return "|".join(extras)
|
|
||||||
|
|
||||||
|
|
||||||
def format_title(data: dict, config: dict):
|
|
||||||
result_format = config["resultFormat"]
|
|
||||||
has_all = "All" in result_format
|
|
||||||
|
|
||||||
title = ""
|
|
||||||
if has_all or "Title" in result_format:
|
|
||||||
title += f"{data['title']}\n"
|
|
||||||
|
|
||||||
if has_all or "Metadata" in result_format:
|
|
||||||
metadata = format_metadata(data)
|
|
||||||
if metadata != "":
|
|
||||||
title += f"💿 {metadata}\n"
|
|
||||||
|
|
||||||
if has_all or "Size" in result_format:
|
|
||||||
title += f"💾 {bytes_to_size(data['size'])} "
|
|
||||||
|
|
||||||
if has_all or "Tracker" in result_format:
|
|
||||||
title += f"🔎 {data['tracker'] if 'tracker' in data else '?'}"
|
|
||||||
|
|
||||||
if has_all or "Languages" in result_format:
|
|
||||||
languages = data["languages"]
|
|
||||||
if data["dubbed"]:
|
|
||||||
languages.insert(0, "multi")
|
|
||||||
if languages:
|
|
||||||
formatted_languages = "/".join(
|
|
||||||
get_language_emoji(language) for language in languages
|
|
||||||
)
|
|
||||||
languages_str = "\n" + formatted_languages
|
|
||||||
title += f"{languages_str}"
|
|
||||||
|
|
||||||
if title == "":
|
|
||||||
# Without this, Streamio shows SD as the result, which is confusing
|
|
||||||
title = "Empty result format configuration"
|
|
||||||
|
|
||||||
return title
|
|
||||||
|
|
||||||
|
|
||||||
def get_client_ip(request: Request):
|
|
||||||
return (
|
|
||||||
request.headers["cf-connecting-ip"]
|
|
||||||
if "cf-connecting-ip" in request.headers
|
|
||||||
else request.client.host
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def get_aliases(session: aiohttp.ClientSession, media_type: str, media_id: str):
|
|
||||||
aliases = {}
|
|
||||||
try:
|
|
||||||
response = await session.get(
|
|
||||||
f"https://api.trakt.tv/{media_type}/{media_id}/aliases"
|
|
||||||
)
|
|
||||||
|
|
||||||
for aliase in await response.json():
|
|
||||||
country = aliase["country"]
|
|
||||||
if country not in aliases:
|
|
||||||
aliases[country] = []
|
|
||||||
|
|
||||||
aliases[country].append(aliase["title"])
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
return aliases
|
|
||||||
|
|
||||||
|
|
||||||
async def add_torrent_to_cache(
|
|
||||||
config: dict, name: str, season: int, episode: int, sorted_ranked_files: dict
|
|
||||||
):
|
|
||||||
# trace of which indexers were used when cache was created - not optimal
|
|
||||||
indexers = config["indexers"].copy()
|
|
||||||
if settings.SCRAPE_TORRENTIO:
|
|
||||||
indexers.append("torrentio")
|
|
||||||
if settings.SCRAPE_MEDIAFUSION:
|
|
||||||
indexers.append("mediafusion")
|
|
||||||
if settings.ZILEAN_URL:
|
|
||||||
indexers.append("dmm")
|
|
||||||
for indexer in indexers:
|
|
||||||
hash = f"searched-{indexer}-{name}-{season}-{episode}"
|
|
||||||
|
|
||||||
searched = copy.deepcopy(
|
|
||||||
sorted_ranked_files[list(sorted_ranked_files.keys())[0]]
|
|
||||||
)
|
|
||||||
searched["infohash"] = hash
|
|
||||||
searched["data"]["tracker"] = indexer
|
|
||||||
|
|
||||||
sorted_ranked_files[hash] = searched
|
|
||||||
|
|
||||||
values = [
|
|
||||||
{
|
|
||||||
"debridService": config["debridService"],
|
|
||||||
"info_hash": sorted_ranked_files[torrent]["infohash"],
|
|
||||||
"name": name,
|
|
||||||
"season": season,
|
|
||||||
"episode": episode,
|
|
||||||
"tracker": sorted_ranked_files[torrent]["data"]["tracker"]
|
|
||||||
.split("|")[0]
|
|
||||||
.lower(),
|
|
||||||
"data": orjson.dumps(sorted_ranked_files[torrent]).decode("utf-8"),
|
|
||||||
"timestamp": time.time(),
|
|
||||||
}
|
|
||||||
for torrent in sorted_ranked_files
|
|
||||||
]
|
|
||||||
|
|
||||||
query = f"""
|
|
||||||
INSERT {'OR IGNORE ' if settings.DATABASE_TYPE == 'sqlite' else ''}
|
|
||||||
INTO cache (debridService, info_hash, name, season, episode, tracker, data, timestamp)
|
|
||||||
VALUES (:debridService, :info_hash, :name, :season, :episode, :tracker, :data, :timestamp)
|
|
||||||
{' ON CONFLICT DO NOTHING' if settings.DATABASE_TYPE == 'postgresql' else ''}
|
|
||||||
"""
|
|
||||||
|
|
||||||
await database.execute_many(query, values)
|
|
||||||
|
|||||||
+41
-34
@@ -1,34 +1,41 @@
|
|||||||
import sys
|
import sys
|
||||||
|
import logging
|
||||||
from loguru import logger
|
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
def setupLogger(level: str):
|
logging.getLogger("demagnetize").setLevel(
|
||||||
logger.level("COMET", no=50, icon="🌠", color="<fg #7871d6>")
|
logging.CRITICAL
|
||||||
logger.level("API", no=40, icon="👾", color="<fg #7871d6>")
|
) # disable demagnetize logging
|
||||||
|
|
||||||
logger.level("INFO", icon="📰", color="<fg #FC5F39>")
|
|
||||||
logger.level("DEBUG", icon="🕸️", color="<fg #DC5F00>")
|
def setupLogger(level: str):
|
||||||
logger.level("WARNING", icon="⚠️", color="<fg #DC5F00>")
|
logger.level("COMET", no=50, icon="🌠", color="<fg #7871d6>")
|
||||||
|
logger.level("API", no=45, icon="👾", color="<fg #006989>")
|
||||||
log_format = (
|
logger.level("SCRAPER", no=40, icon="👻", color="<fg #d6bb71>")
|
||||||
"<white>{time:YYYY-MM-DD}</white> <magenta>{time:HH:mm:ss}</magenta> | "
|
logger.level("STREAM", no=35, icon="🎬", color="<fg #d171d6>")
|
||||||
"<level>{level.icon}</level> <level>{level}</level> | "
|
|
||||||
"<cyan>{module}</cyan>.<cyan>{function}</cyan> - <level>{message}</level>"
|
logger.level("INFO", icon="📰", color="<fg #FC5F39>")
|
||||||
)
|
logger.level("DEBUG", icon="🕸️", color="<fg #DC5F00>")
|
||||||
|
logger.level("WARNING", icon="⚠️", color="<fg #DC5F00>")
|
||||||
logger.configure(
|
|
||||||
handlers=[
|
log_format = (
|
||||||
{
|
"<white>{time:YYYY-MM-DD}</white> <magenta>{time:HH:mm:ss}</magenta> | "
|
||||||
"sink": sys.stderr,
|
"<level>{level.icon}</level> <level>{level}</level> | "
|
||||||
"level": level,
|
"<cyan>{module}</cyan>.<cyan>{function}</cyan> - <level>{message}</level>"
|
||||||
"format": log_format,
|
)
|
||||||
"backtrace": False,
|
|
||||||
"diagnose": False,
|
logger.configure(
|
||||||
"enqueue": True,
|
handlers=[
|
||||||
}
|
{
|
||||||
]
|
"sink": sys.stderr,
|
||||||
)
|
"level": level,
|
||||||
|
"format": log_format,
|
||||||
|
"backtrace": False,
|
||||||
setupLogger("DEBUG")
|
"diagnose": False,
|
||||||
|
"enqueue": True,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
setupLogger("DEBUG")
|
||||||
|
|||||||
+541
-219
@@ -1,27 +1,46 @@
|
|||||||
import os
|
|
||||||
import random
|
import random
|
||||||
import string
|
import string
|
||||||
|
import RTN
|
||||||
|
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
from databases import Database
|
from databases import Database
|
||||||
from pydantic import BaseModel, field_validator
|
from pydantic import BaseModel, field_validator
|
||||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||||
from RTN import RTN, BestRanking, SettingsModel
|
from RTN import BestRanking, SettingsModel
|
||||||
|
from RTN.models import (
|
||||||
|
ResolutionConfig,
|
||||||
|
OptionsConfig,
|
||||||
|
LanguagesConfig,
|
||||||
|
CustomRanksConfig,
|
||||||
|
CustomRank,
|
||||||
|
QualityRankModel,
|
||||||
|
RipsRankModel,
|
||||||
|
HdrRankModel,
|
||||||
|
AudioRankModel,
|
||||||
|
ExtrasRankModel,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class AppSettings(BaseSettings):
|
class AppSettings(BaseSettings):
|
||||||
model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8")
|
model_config = SettingsConfigDict(
|
||||||
|
env_file=".env", env_file_encoding="utf-8", extra="ignore"
|
||||||
|
)
|
||||||
|
|
||||||
ADDON_ID: Optional[str] = "stremio.comet.fast"
|
ADDON_ID: Optional[str] = "stremio.comet.fast"
|
||||||
ADDON_NAME: Optional[str] = "Comet"
|
ADDON_NAME: Optional[str] = "Comet"
|
||||||
FASTAPI_HOST: Optional[str] = "0.0.0.0"
|
FASTAPI_HOST: Optional[str] = "0.0.0.0"
|
||||||
FASTAPI_PORT: Optional[int] = 8000
|
FASTAPI_PORT: Optional[int] = 8000
|
||||||
FASTAPI_WORKERS: Optional[int] = 2 * (os.cpu_count() or 1)
|
FASTAPI_WORKERS: Optional[int] = 1
|
||||||
DASHBOARD_ADMIN_PASSWORD: Optional[str] = None
|
USE_GUNICORN: Optional[bool] = True
|
||||||
|
DASHBOARD_ADMIN_PASSWORD: Optional[str] = "".join(
|
||||||
|
random.choices(string.ascii_letters + string.digits, k=16)
|
||||||
|
)
|
||||||
DATABASE_TYPE: Optional[str] = "sqlite"
|
DATABASE_TYPE: Optional[str] = "sqlite"
|
||||||
DATABASE_URL: Optional[str] = "username:password@hostname:port"
|
DATABASE_URL: Optional[str] = "username:password@hostname:port"
|
||||||
DATABASE_PATH: Optional[str] = "data/comet.db"
|
DATABASE_PATH: Optional[str] = "data/comet.db"
|
||||||
CACHE_TTL: Optional[int] = 86400
|
METADATA_CACHE_TTL: Optional[int] = 2592000 # 30 days
|
||||||
|
TORRENT_CACHE_TTL: Optional[int] = 1296000 # 15 days
|
||||||
|
DEBRID_CACHE_TTL: Optional[int] = 86400 # 1 day
|
||||||
DEBRID_PROXY_URL: Optional[str] = None
|
DEBRID_PROXY_URL: Optional[str] = None
|
||||||
INDEXER_MANAGER_TYPE: Optional[str] = None
|
INDEXER_MANAGER_TYPE: Optional[str] = None
|
||||||
INDEXER_MANAGER_URL: Optional[str] = "http://127.0.0.1:9117"
|
INDEXER_MANAGER_URL: Optional[str] = "http://127.0.0.1:9117"
|
||||||
@@ -29,86 +48,498 @@ class AppSettings(BaseSettings):
|
|||||||
INDEXER_MANAGER_TIMEOUT: Optional[int] = 30
|
INDEXER_MANAGER_TIMEOUT: Optional[int] = 30
|
||||||
INDEXER_MANAGER_INDEXERS: List[str] = []
|
INDEXER_MANAGER_INDEXERS: List[str] = []
|
||||||
GET_TORRENT_TIMEOUT: Optional[int] = 5
|
GET_TORRENT_TIMEOUT: Optional[int] = 5
|
||||||
ZILEAN_URL: Optional[str] = None
|
DOWNLOAD_TORRENT_FILES: Optional[bool] = False
|
||||||
ZILEAN_TAKE_FIRST: Optional[int] = 500
|
SCRAPE_MEDIAFUSION: Optional[bool] = False
|
||||||
|
SCRAPE_ZILEAN: Optional[bool] = False
|
||||||
|
ZILEAN_URL: Optional[str] = "https://zilean.elfhosted.com"
|
||||||
SCRAPE_TORRENTIO: Optional[bool] = False
|
SCRAPE_TORRENTIO: Optional[bool] = False
|
||||||
|
TORRENTIO_URL: Optional[str] = "https://torrentio.strem.fun"
|
||||||
SCRAPE_MEDIAFUSION: Optional[bool] = False
|
SCRAPE_MEDIAFUSION: Optional[bool] = False
|
||||||
MEDIAFUSION_URL: Optional[str] = "https://mediafusion.elfhosted.com"
|
MEDIAFUSION_URL: Optional[str] = "https://mediafusion.elfhosted.com"
|
||||||
CUSTOM_HEADER_HTML: Optional[str] = None
|
CUSTOM_HEADER_HTML: Optional[str] = None
|
||||||
PROXY_DEBRID_STREAM: Optional[bool] = False
|
PROXY_DEBRID_STREAM: Optional[bool] = False
|
||||||
PROXY_DEBRID_STREAM_PASSWORD: Optional[str] = None
|
PROXY_DEBRID_STREAM_PASSWORD: Optional[str] = "".join(
|
||||||
|
random.choices(string.ascii_letters + string.digits, k=16)
|
||||||
|
)
|
||||||
PROXY_DEBRID_STREAM_MAX_CONNECTIONS: Optional[int] = -1
|
PROXY_DEBRID_STREAM_MAX_CONNECTIONS: Optional[int] = -1
|
||||||
PROXY_DEBRID_STREAM_DEBRID_DEFAULT_SERVICE: Optional[str] = "realdebrid"
|
PROXY_DEBRID_STREAM_DEBRID_DEFAULT_SERVICE: Optional[str] = "realdebrid"
|
||||||
PROXY_DEBRID_STREAM_DEBRID_DEFAULT_APIKEY: Optional[str] = None
|
PROXY_DEBRID_STREAM_DEBRID_DEFAULT_APIKEY: Optional[str] = None
|
||||||
TITLE_MATCH_CHECK: Optional[bool] = True
|
STREMTHRU_URL: Optional[str] = "https://stremthru.13377001.xyz"
|
||||||
REMOVE_ADULT_CONTENT: Optional[bool] = False
|
REMOVE_ADULT_CONTENT: Optional[bool] = False
|
||||||
STREMTHRU_DEFAULT_URL: Optional[str] = "https://stremthru.mooo.com"
|
|
||||||
STREMTHRU_AUTO_ENABLED_DEBRID_SERVICES: List[str] = [
|
|
||||||
"realdebrid",
|
|
||||||
"alldebrid",
|
|
||||||
"debridlink",
|
|
||||||
]
|
|
||||||
|
|
||||||
@field_validator("DASHBOARD_ADMIN_PASSWORD")
|
@field_validator(
|
||||||
def set_dashboard_admin_password(cls, v, values):
|
"INDEXER_MANAGER_URL",
|
||||||
if v is None:
|
"ZILEAN_URL",
|
||||||
return "".join(random.choices(string.ascii_letters + string.digits, k=16))
|
"TORRENTIO_URL",
|
||||||
|
"MEDIAFUSION_URL",
|
||||||
|
"STREMTHRU_URL",
|
||||||
|
)
|
||||||
|
def remove_trailing_slash(cls, v):
|
||||||
|
if v and v.endswith("/"):
|
||||||
|
return v[:-1]
|
||||||
return v
|
return v
|
||||||
|
|
||||||
@field_validator("INDEXER_MANAGER_TYPE")
|
@field_validator("INDEXER_MANAGER_TYPE")
|
||||||
def set_indexer_manager_type(cls, v, values):
|
def set_indexer_manager_type(cls, v, values):
|
||||||
if v == "None":
|
if v is not None and v.lower() == "none":
|
||||||
return None
|
return None
|
||||||
return v
|
return v
|
||||||
|
|
||||||
@field_validator("PROXY_DEBRID_STREAM_PASSWORD")
|
@field_validator("INDEXER_MANAGER_INDEXERS")
|
||||||
def set_debrid_stream_proxy_password(cls, v, values):
|
def indexer_manager_indexers_normalization(cls, v, values):
|
||||||
if v is None:
|
v = [indexer.replace(" ", "").lower() for indexer in v]
|
||||||
return "".join(random.choices(string.ascii_letters + string.digits, k=16))
|
|
||||||
return v
|
return v
|
||||||
|
|
||||||
|
|
||||||
settings = AppSettings()
|
settings = AppSettings()
|
||||||
|
|
||||||
|
|
||||||
|
class CometSettingsModel(SettingsModel):
|
||||||
|
model_config = SettingsConfigDict()
|
||||||
|
|
||||||
|
resolutions: ResolutionConfig = ResolutionConfig(
|
||||||
|
r2160p=True, r480p=True, r360p=True
|
||||||
|
)
|
||||||
|
|
||||||
|
options: OptionsConfig = OptionsConfig(remove_ranks_under=-10000000000)
|
||||||
|
|
||||||
|
languages: LanguagesConfig = LanguagesConfig(exclude=[])
|
||||||
|
|
||||||
|
custom_ranks: CustomRanksConfig = CustomRanksConfig(
|
||||||
|
quality=QualityRankModel(
|
||||||
|
av1=CustomRank(fetch=True),
|
||||||
|
dvd=CustomRank(fetch=True),
|
||||||
|
mpeg=CustomRank(fetch=True),
|
||||||
|
remux=CustomRank(fetch=True),
|
||||||
|
vhs=CustomRank(fetch=True),
|
||||||
|
webmux=CustomRank(fetch=True),
|
||||||
|
xvid=CustomRank(fetch=True),
|
||||||
|
),
|
||||||
|
rips=RipsRankModel(
|
||||||
|
bdrip=CustomRank(fetch=True),
|
||||||
|
dvdrip=CustomRank(fetch=True),
|
||||||
|
ppvrip=CustomRank(fetch=True),
|
||||||
|
satrip=CustomRank(fetch=True),
|
||||||
|
tvrip=CustomRank(fetch=True),
|
||||||
|
uhdrip=CustomRank(fetch=True),
|
||||||
|
vhsrip=CustomRank(fetch=True),
|
||||||
|
webdlrip=CustomRank(fetch=True),
|
||||||
|
),
|
||||||
|
hdr=HdrRankModel(
|
||||||
|
dolby_vision=CustomRank(fetch=True),
|
||||||
|
),
|
||||||
|
audio=AudioRankModel(
|
||||||
|
mono=CustomRank(fetch=True),
|
||||||
|
mp3=CustomRank(fetch=True),
|
||||||
|
),
|
||||||
|
extras=ExtrasRankModel(
|
||||||
|
three_d=CustomRank(fetch=True),
|
||||||
|
converted=CustomRank(fetch=True),
|
||||||
|
documentary=CustomRank(fetch=True),
|
||||||
|
site=CustomRank(fetch=True),
|
||||||
|
upscaled=CustomRank(fetch=True),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
rtn_settings_default = CometSettingsModel()
|
||||||
|
rtn_settings_default_dumped = rtn_settings_default.model_dump()
|
||||||
|
# {
|
||||||
|
# "profile":"default",
|
||||||
|
# "require":[
|
||||||
|
|
||||||
|
# ],
|
||||||
|
# "exclude":[
|
||||||
|
|
||||||
|
# ],
|
||||||
|
# "preferred":[
|
||||||
|
|
||||||
|
# ],
|
||||||
|
# "resolutions":{
|
||||||
|
# "r2160p":true,
|
||||||
|
# "r1080p":true,
|
||||||
|
# "r720p":true,
|
||||||
|
# "r480p":true,
|
||||||
|
# "r360p":true,
|
||||||
|
# "unknown":true
|
||||||
|
# },
|
||||||
|
# "options":{
|
||||||
|
# "title_similarity":0.85,
|
||||||
|
# "remove_all_trash":true,
|
||||||
|
# "remove_ranks_under":-10000000000,
|
||||||
|
# "remove_unknown_languages":false,
|
||||||
|
# "allow_english_in_languages":false,
|
||||||
|
# "enable_fetch_speed_mode":true,
|
||||||
|
# "remove_adult_content":true
|
||||||
|
# },
|
||||||
|
# "languages":{
|
||||||
|
# "required":[
|
||||||
|
|
||||||
|
# ],
|
||||||
|
# "exclude":[
|
||||||
|
|
||||||
|
# ],
|
||||||
|
# "preferred":[
|
||||||
|
|
||||||
|
# ]
|
||||||
|
# },
|
||||||
|
# "custom_ranks":{
|
||||||
|
# "quality":{
|
||||||
|
# "av1":{
|
||||||
|
# "fetch":true,
|
||||||
|
# "use_custom_rank":false,
|
||||||
|
# "rank":0
|
||||||
|
# },
|
||||||
|
# "avc":{
|
||||||
|
# "fetch":true,
|
||||||
|
# "use_custom_rank":false,
|
||||||
|
# "rank":0
|
||||||
|
# },
|
||||||
|
# "bluray":{
|
||||||
|
# "fetch":true,
|
||||||
|
# "use_custom_rank":false,
|
||||||
|
# "rank":0
|
||||||
|
# },
|
||||||
|
# "dvd":{
|
||||||
|
# "fetch":true,
|
||||||
|
# "use_custom_rank":false,
|
||||||
|
# "rank":0
|
||||||
|
# },
|
||||||
|
# "hdtv":{
|
||||||
|
# "fetch":true,
|
||||||
|
# "use_custom_rank":false,
|
||||||
|
# "rank":0
|
||||||
|
# },
|
||||||
|
# "hevc":{
|
||||||
|
# "fetch":true,
|
||||||
|
# "use_custom_rank":false,
|
||||||
|
# "rank":0
|
||||||
|
# },
|
||||||
|
# "mpeg":{
|
||||||
|
# "fetch":true,
|
||||||
|
# "use_custom_rank":false,
|
||||||
|
# "rank":0
|
||||||
|
# },
|
||||||
|
# "remux":{
|
||||||
|
# "fetch":true,
|
||||||
|
# "use_custom_rank":false,
|
||||||
|
# "rank":0
|
||||||
|
# },
|
||||||
|
# "vhs":{
|
||||||
|
# "fetch":true,
|
||||||
|
# "use_custom_rank":false,
|
||||||
|
# "rank":0
|
||||||
|
# },
|
||||||
|
# "web":{
|
||||||
|
# "fetch":true,
|
||||||
|
# "use_custom_rank":false,
|
||||||
|
# "rank":0
|
||||||
|
# },
|
||||||
|
# "webdl":{
|
||||||
|
# "fetch":true,
|
||||||
|
# "use_custom_rank":false,
|
||||||
|
# "rank":0
|
||||||
|
# },
|
||||||
|
# "webmux":{
|
||||||
|
# "fetch":true,
|
||||||
|
# "use_custom_rank":false,
|
||||||
|
# "rank":0
|
||||||
|
# },
|
||||||
|
# "xvid":{
|
||||||
|
# "fetch":true,
|
||||||
|
# "use_custom_rank":false,
|
||||||
|
# "rank":0
|
||||||
|
# }
|
||||||
|
# },
|
||||||
|
# "rips":{
|
||||||
|
# "bdrip":{
|
||||||
|
# "fetch":true,
|
||||||
|
# "use_custom_rank":false,
|
||||||
|
# "rank":0
|
||||||
|
# },
|
||||||
|
# "brrip":{
|
||||||
|
# "fetch":true,
|
||||||
|
# "use_custom_rank":false,
|
||||||
|
# "rank":0
|
||||||
|
# },
|
||||||
|
# "dvdrip":{
|
||||||
|
# "fetch":true,
|
||||||
|
# "use_custom_rank":false,
|
||||||
|
# "rank":0
|
||||||
|
# },
|
||||||
|
# "hdrip":{
|
||||||
|
# "fetch":true,
|
||||||
|
# "use_custom_rank":false,
|
||||||
|
# "rank":0
|
||||||
|
# },
|
||||||
|
# "ppvrip":{
|
||||||
|
# "fetch":true,
|
||||||
|
# "use_custom_rank":false,
|
||||||
|
# "rank":0
|
||||||
|
# },
|
||||||
|
# "satrip":{
|
||||||
|
# "fetch":true,
|
||||||
|
# "use_custom_rank":false,
|
||||||
|
# "rank":0
|
||||||
|
# },
|
||||||
|
# "tvrip":{
|
||||||
|
# "fetch":true,
|
||||||
|
# "use_custom_rank":false,
|
||||||
|
# "rank":0
|
||||||
|
# },
|
||||||
|
# "uhdrip":{
|
||||||
|
# "fetch":true,
|
||||||
|
# "use_custom_rank":false,
|
||||||
|
# "rank":0
|
||||||
|
# },
|
||||||
|
# "vhsrip":{
|
||||||
|
# "fetch":true,
|
||||||
|
# "use_custom_rank":false,
|
||||||
|
# "rank":0
|
||||||
|
# },
|
||||||
|
# "webdlrip":{
|
||||||
|
# "fetch":true,
|
||||||
|
# "use_custom_rank":false,
|
||||||
|
# "rank":0
|
||||||
|
# },
|
||||||
|
# "webrip":{
|
||||||
|
# "fetch":true,
|
||||||
|
# "use_custom_rank":false,
|
||||||
|
# "rank":0
|
||||||
|
# }
|
||||||
|
# },
|
||||||
|
# "hdr":{
|
||||||
|
# "bit10":{
|
||||||
|
# "fetch":true,
|
||||||
|
# "use_custom_rank":false,
|
||||||
|
# "rank":0
|
||||||
|
# },
|
||||||
|
# "dolby_vision":{
|
||||||
|
# "fetch":true,
|
||||||
|
# "use_custom_rank":false,
|
||||||
|
# "rank":0
|
||||||
|
# },
|
||||||
|
# "hdr":{
|
||||||
|
# "fetch":true,
|
||||||
|
# "use_custom_rank":false,
|
||||||
|
# "rank":0
|
||||||
|
# },
|
||||||
|
# "hdr10plus":{
|
||||||
|
# "fetch":true,
|
||||||
|
# "use_custom_rank":false,
|
||||||
|
# "rank":0
|
||||||
|
# },
|
||||||
|
# "sdr":{
|
||||||
|
# "fetch":true,
|
||||||
|
# "use_custom_rank":false,
|
||||||
|
# "rank":0
|
||||||
|
# }
|
||||||
|
# },
|
||||||
|
# "audio":{
|
||||||
|
# "aac":{
|
||||||
|
# "fetch":true,
|
||||||
|
# "use_custom_rank":false,
|
||||||
|
# "rank":0
|
||||||
|
# },
|
||||||
|
# "ac3":{
|
||||||
|
# "fetch":true,
|
||||||
|
# "use_custom_rank":false,
|
||||||
|
# "rank":0
|
||||||
|
# },
|
||||||
|
# "atmos":{
|
||||||
|
# "fetch":true,
|
||||||
|
# "use_custom_rank":false,
|
||||||
|
# "rank":0
|
||||||
|
# },
|
||||||
|
# "dolby_digital":{
|
||||||
|
# "fetch":true,
|
||||||
|
# "use_custom_rank":false,
|
||||||
|
# "rank":0
|
||||||
|
# },
|
||||||
|
# "dolby_digital_plus":{
|
||||||
|
# "fetch":true,
|
||||||
|
# "use_custom_rank":false,
|
||||||
|
# "rank":0
|
||||||
|
# },
|
||||||
|
# "dts_lossy":{
|
||||||
|
# "fetch":true,
|
||||||
|
# "use_custom_rank":false,
|
||||||
|
# "rank":0
|
||||||
|
# },
|
||||||
|
# "dts_lossless":{
|
||||||
|
# "fetch":true,
|
||||||
|
# "use_custom_rank":false,
|
||||||
|
# "rank":0
|
||||||
|
# },
|
||||||
|
# "eac3":{
|
||||||
|
# "fetch":true,
|
||||||
|
# "use_custom_rank":false,
|
||||||
|
# "rank":0
|
||||||
|
# },
|
||||||
|
# "flac":{
|
||||||
|
# "fetch":true,
|
||||||
|
# "use_custom_rank":false,
|
||||||
|
# "rank":0
|
||||||
|
# },
|
||||||
|
# "mono":{
|
||||||
|
# "fetch":true,
|
||||||
|
# "use_custom_rank":false,
|
||||||
|
# "rank":0
|
||||||
|
# },
|
||||||
|
# "mp3":{
|
||||||
|
# "fetch":true,
|
||||||
|
# "use_custom_rank":false,
|
||||||
|
# "rank":0
|
||||||
|
# },
|
||||||
|
# "stereo":{
|
||||||
|
# "fetch":true,
|
||||||
|
# "use_custom_rank":false,
|
||||||
|
# "rank":0
|
||||||
|
# },
|
||||||
|
# "surround":{
|
||||||
|
# "fetch":true,
|
||||||
|
# "use_custom_rank":false,
|
||||||
|
# "rank":0
|
||||||
|
# },
|
||||||
|
# "truehd":{
|
||||||
|
# "fetch":true,
|
||||||
|
# "use_custom_rank":false,
|
||||||
|
# "rank":0
|
||||||
|
# }
|
||||||
|
# },
|
||||||
|
# "extras":{
|
||||||
|
# "three_d":{
|
||||||
|
# "fetch":true,
|
||||||
|
# "use_custom_rank":false,
|
||||||
|
# "rank":0
|
||||||
|
# },
|
||||||
|
# "converted":{
|
||||||
|
# "fetch":true,
|
||||||
|
# "use_custom_rank":false,
|
||||||
|
# "rank":0
|
||||||
|
# },
|
||||||
|
# "documentary":{
|
||||||
|
# "fetch":true,
|
||||||
|
# "use_custom_rank":false,
|
||||||
|
# "rank":0
|
||||||
|
# },
|
||||||
|
# "dubbed":{
|
||||||
|
# "fetch":true,
|
||||||
|
# "use_custom_rank":false,
|
||||||
|
# "rank":0
|
||||||
|
# },
|
||||||
|
# "edition":{
|
||||||
|
# "fetch":true,
|
||||||
|
# "use_custom_rank":false,
|
||||||
|
# "rank":0
|
||||||
|
# },
|
||||||
|
# "hardcoded":{
|
||||||
|
# "fetch":true,
|
||||||
|
# "use_custom_rank":false,
|
||||||
|
# "rank":0
|
||||||
|
# },
|
||||||
|
# "network":{
|
||||||
|
# "fetch":true,
|
||||||
|
# "use_custom_rank":false,
|
||||||
|
# "rank":0
|
||||||
|
# },
|
||||||
|
# "proper":{
|
||||||
|
# "fetch":true,
|
||||||
|
# "use_custom_rank":false,
|
||||||
|
# "rank":0
|
||||||
|
# },
|
||||||
|
# "repack":{
|
||||||
|
# "fetch":true,
|
||||||
|
# "use_custom_rank":false,
|
||||||
|
# "rank":0
|
||||||
|
# },
|
||||||
|
# "retail":{
|
||||||
|
# "fetch":true,
|
||||||
|
# "use_custom_rank":false,
|
||||||
|
# "rank":0
|
||||||
|
# },
|
||||||
|
# "site":{
|
||||||
|
# "fetch":true,
|
||||||
|
# "use_custom_rank":false,
|
||||||
|
# "rank":0
|
||||||
|
# },
|
||||||
|
# "subbed":{
|
||||||
|
# "fetch":true,
|
||||||
|
# "use_custom_rank":false,
|
||||||
|
# "rank":0
|
||||||
|
# },
|
||||||
|
# "upscaled":{
|
||||||
|
# "fetch":true,
|
||||||
|
# "use_custom_rank":false,
|
||||||
|
# "rank":0
|
||||||
|
# },
|
||||||
|
# "scene":{
|
||||||
|
# "fetch":true,
|
||||||
|
# "use_custom_rank":false,
|
||||||
|
# "rank":0
|
||||||
|
# }
|
||||||
|
# },
|
||||||
|
# "trash":{
|
||||||
|
# "cam":{
|
||||||
|
# "fetch":false,
|
||||||
|
# "use_custom_rank":false,
|
||||||
|
# "rank":0
|
||||||
|
# },
|
||||||
|
# "clean_audio":{
|
||||||
|
# "fetch":false,
|
||||||
|
# "use_custom_rank":false,
|
||||||
|
# "rank":0
|
||||||
|
# },
|
||||||
|
# "pdtv":{
|
||||||
|
# "fetch":false,
|
||||||
|
# "use_custom_rank":false,
|
||||||
|
# "rank":0
|
||||||
|
# },
|
||||||
|
# "r5":{
|
||||||
|
# "fetch":false,
|
||||||
|
# "use_custom_rank":false,
|
||||||
|
# "rank":0
|
||||||
|
# },
|
||||||
|
# "screener":{
|
||||||
|
# "fetch":false,
|
||||||
|
# "use_custom_rank":false,
|
||||||
|
# "rank":0
|
||||||
|
# },
|
||||||
|
# "size":{
|
||||||
|
# "fetch":false,
|
||||||
|
# "use_custom_rank":false,
|
||||||
|
# "rank":0
|
||||||
|
# },
|
||||||
|
# "telecine":{
|
||||||
|
# "fetch":false,
|
||||||
|
# "use_custom_rank":false,
|
||||||
|
# "rank":0
|
||||||
|
# },
|
||||||
|
# "telesync":{
|
||||||
|
# "fetch":false,
|
||||||
|
# "use_custom_rank":false,
|
||||||
|
# "rank":0
|
||||||
|
# }
|
||||||
|
# }
|
||||||
|
# }
|
||||||
|
# }
|
||||||
|
rtn_ranking_default = BestRanking()
|
||||||
|
|
||||||
|
|
||||||
class ConfigModel(BaseModel):
|
class ConfigModel(BaseModel):
|
||||||
indexers: List[str]
|
cachedOnly: Optional[bool] = False
|
||||||
languages: Optional[List[str]] = ["All"]
|
|
||||||
resolutions: Optional[List[str]] = ["All"]
|
|
||||||
reverseResultOrder: Optional[bool] = False
|
|
||||||
removeTrash: Optional[bool] = True
|
removeTrash: Optional[bool] = True
|
||||||
resultFormat: Optional[List[str]] = ["All"]
|
resultFormat: Optional[List[str]] = ["all"]
|
||||||
maxResults: Optional[int] = 0
|
|
||||||
maxResultsPerResolution: Optional[int] = 0
|
maxResultsPerResolution: Optional[int] = 0
|
||||||
maxSize: Optional[float] = 0
|
maxSize: Optional[float] = 0
|
||||||
debridService: str
|
debridService: Optional[str] = "torrent"
|
||||||
debridApiKey: str
|
debridApiKey: Optional[str] = ""
|
||||||
debridStreamProxyPassword: Optional[str] = ""
|
debridStreamProxyPassword: Optional[str] = ""
|
||||||
stremthruUrl: Optional[str] = None
|
languages: Optional[dict] = rtn_settings_default_dumped["languages"]
|
||||||
|
resolutions: Optional[dict] = rtn_settings_default_dumped["resolutions"]
|
||||||
@field_validator("indexers")
|
options: Optional[dict] = rtn_settings_default_dumped["options"]
|
||||||
def check_indexers(cls, v, values):
|
rtnSettings: Optional[CometSettingsModel] = rtn_settings_default
|
||||||
settings.INDEXER_MANAGER_INDEXERS = [
|
rtnRanking: Optional[BestRanking] = rtn_ranking_default
|
||||||
indexer.replace(" ", "_").lower()
|
|
||||||
for indexer in settings.INDEXER_MANAGER_INDEXERS
|
|
||||||
] # to equal webui
|
|
||||||
valid_indexers = [
|
|
||||||
indexer for indexer in v if indexer in settings.INDEXER_MANAGER_INDEXERS
|
|
||||||
]
|
|
||||||
# if not valid_indexers: # For only Zilean mode
|
|
||||||
# raise ValueError(
|
|
||||||
# f"At least one indexer must be from {settings.INDEXER_MANAGER_INDEXERS}"
|
|
||||||
# )
|
|
||||||
return valid_indexers
|
|
||||||
|
|
||||||
@field_validator("maxResults")
|
|
||||||
def check_max_results(cls, v):
|
|
||||||
if not isinstance(v, int):
|
|
||||||
v = 0
|
|
||||||
|
|
||||||
if v < 0:
|
|
||||||
v = 0
|
|
||||||
return v
|
|
||||||
|
|
||||||
@field_validator("maxResultsPerResolution")
|
@field_validator("maxResultsPerResolution")
|
||||||
def check_max_results_per_resolution(cls, v):
|
def check_max_results_per_resolution(cls, v):
|
||||||
@@ -130,132 +561,35 @@ class ConfigModel(BaseModel):
|
|||||||
|
|
||||||
@field_validator("debridService")
|
@field_validator("debridService")
|
||||||
def check_debrid_service(cls, v):
|
def check_debrid_service(cls, v):
|
||||||
if v not in ["realdebrid", "alldebrid", "premiumize", "torbox", "debridlink", "stremthru"]:
|
if v not in [
|
||||||
|
"realdebrid",
|
||||||
|
"alldebrid",
|
||||||
|
"premiumize",
|
||||||
|
"torbox",
|
||||||
|
"easydebrid",
|
||||||
|
"debridlink",
|
||||||
|
"offcloud",
|
||||||
|
"pikpak",
|
||||||
|
"torrent",
|
||||||
|
]:
|
||||||
raise ValueError("Invalid debridService")
|
raise ValueError("Invalid debridService")
|
||||||
return v
|
return v
|
||||||
|
|
||||||
|
|
||||||
default_settings = {
|
default_config = ConfigModel().model_dump()
|
||||||
"profile": "default",
|
default_config["rtnSettings"] = rtn_settings_default
|
||||||
"require": [],
|
default_config["rtnRanking"] = rtn_ranking_default
|
||||||
"exclude": [],
|
|
||||||
"preferred": [],
|
|
||||||
"resolutions": {
|
|
||||||
"r2160p": True,
|
|
||||||
"r1080p": True,
|
|
||||||
"r720p": True,
|
|
||||||
"r480p": True,
|
|
||||||
"r360p": True,
|
|
||||||
"unknown": True,
|
|
||||||
},
|
|
||||||
"options": {
|
|
||||||
"title_similarity": 0.85,
|
|
||||||
"remove_all_trash": True,
|
|
||||||
"remove_ranks_under": -1000000000000000,
|
|
||||||
"remove_unknown_languages": False,
|
|
||||||
"allow_english_in_languages": True,
|
|
||||||
"enable_fetch_speed_mode": True,
|
|
||||||
"remove_adult_content": settings.REMOVE_ADULT_CONTENT,
|
|
||||||
},
|
|
||||||
"languages": {
|
|
||||||
"required": [],
|
|
||||||
"exclude": [
|
|
||||||
# "ar",
|
|
||||||
# "hi",
|
|
||||||
# "fr",
|
|
||||||
# "es",
|
|
||||||
# "de",
|
|
||||||
# "ru",
|
|
||||||
# "pt",
|
|
||||||
# "it"
|
|
||||||
],
|
|
||||||
"preferred": [],
|
|
||||||
},
|
|
||||||
"custom_ranks": {
|
|
||||||
"quality": {
|
|
||||||
"av1": {"fetch": True, "use_custom_rank": False, "rank": 0},
|
|
||||||
"avc": {"fetch": True, "use_custom_rank": False, "rank": 0},
|
|
||||||
"bluray": {"fetch": True, "use_custom_rank": False, "rank": 0},
|
|
||||||
"dvd": {"fetch": True, "use_custom_rank": False, "rank": 0},
|
|
||||||
"hdtv": {"fetch": True, "use_custom_rank": False, "rank": 0},
|
|
||||||
"hevc": {"fetch": True, "use_custom_rank": False, "rank": 0},
|
|
||||||
"mpeg": {"fetch": True, "use_custom_rank": False, "rank": 0},
|
|
||||||
"remux": {"fetch": True, "use_custom_rank": False, "rank": 0},
|
|
||||||
"vhs": {"fetch": True, "use_custom_rank": False, "rank": 0},
|
|
||||||
"web": {"fetch": True, "use_custom_rank": False, "rank": 0},
|
|
||||||
"webdl": {"fetch": True, "use_custom_rank": False, "rank": 0},
|
|
||||||
"webmux": {"fetch": True, "use_custom_rank": False, "rank": 0},
|
|
||||||
"xvid": {"fetch": True, "use_custom_rank": False, "rank": 0},
|
|
||||||
},
|
|
||||||
"rips": {
|
|
||||||
"bdrip": {"fetch": True, "use_custom_rank": False, "rank": 0},
|
|
||||||
"brrip": {"fetch": True, "use_custom_rank": False, "rank": 0},
|
|
||||||
"dvdrip": {"fetch": True, "use_custom_rank": False, "rank": 0},
|
|
||||||
"hdrip": {"fetch": True, "use_custom_rank": False, "rank": 0},
|
|
||||||
"ppvrip": {"fetch": True, "use_custom_rank": False, "rank": 0},
|
|
||||||
"satrip": {"fetch": True, "use_custom_rank": False, "rank": 0},
|
|
||||||
"tvrip": {"fetch": True, "use_custom_rank": False, "rank": 0},
|
|
||||||
"uhdrip": {"fetch": True, "use_custom_rank": False, "rank": 0},
|
|
||||||
"vhsrip": {"fetch": True, "use_custom_rank": False, "rank": 0},
|
|
||||||
"webdlrip": {"fetch": True, "use_custom_rank": False, "rank": 0},
|
|
||||||
"webrip": {"fetch": True, "use_custom_rank": False, "rank": 0},
|
|
||||||
},
|
|
||||||
"hdr": {
|
|
||||||
"bit10": {"fetch": True, "use_custom_rank": False, "rank": 0},
|
|
||||||
"dolby_vision": {"fetch": True, "use_custom_rank": False, "rank": 0},
|
|
||||||
"hdr": {"fetch": True, "use_custom_rank": False, "rank": 0},
|
|
||||||
"hdr10plus": {"fetch": True, "use_custom_rank": False, "rank": 0},
|
|
||||||
"sdr": {"fetch": True, "use_custom_rank": False, "rank": 0},
|
|
||||||
},
|
|
||||||
"audio": {
|
|
||||||
"aac": {"fetch": True, "use_custom_rank": False, "rank": 0},
|
|
||||||
"ac3": {"fetch": True, "use_custom_rank": False, "rank": 0},
|
|
||||||
"atmos": {"fetch": True, "use_custom_rank": False, "rank": 0},
|
|
||||||
"dolby_digital": {"fetch": True, "use_custom_rank": False, "rank": 0},
|
|
||||||
"dolby_digital_plus": {"fetch": True, "use_custom_rank": False, "rank": 0},
|
|
||||||
"dts_lossy": {"fetch": True, "use_custom_rank": False, "rank": 0},
|
|
||||||
"dts_lossless": {"fetch": True, "use_custom_rank": False, "rank": 0},
|
|
||||||
"eac3": {"fetch": True, "use_custom_rank": False, "rank": 0},
|
|
||||||
"flac": {"fetch": True, "use_custom_rank": False, "rank": 0},
|
|
||||||
"mono": {"fetch": True, "use_custom_rank": False, "rank": 0},
|
|
||||||
"mp3": {"fetch": True, "use_custom_rank": False, "rank": 0},
|
|
||||||
"stereo": {"fetch": True, "use_custom_rank": False, "rank": 0},
|
|
||||||
"surround": {"fetch": True, "use_custom_rank": False, "rank": 0},
|
|
||||||
"Truehd": {"fetch": True, "use_custom_rank": False, "rank": 0},
|
|
||||||
},
|
|
||||||
"extras": {
|
|
||||||
"three_d": {"fetch": True, "use_custom_rank": False, "rank": 0},
|
|
||||||
"converted": {"fetch": True, "use_custom_rank": False, "rank": 0},
|
|
||||||
"documentary": {"fetch": True, "use_custom_rank": False, "rank": 0},
|
|
||||||
"dubbed": {"fetch": True, "use_custom_rank": False, "rank": 0},
|
|
||||||
"edition": {"fetch": True, "use_custom_rank": False, "rank": 0},
|
|
||||||
"hardcoded": {"fetch": True, "use_custom_rank": False, "rank": 0},
|
|
||||||
"network": {"fetch": True, "use_custom_rank": False, "rank": 0},
|
|
||||||
"proper": {"fetch": True, "use_custom_rank": False, "rank": 0},
|
|
||||||
"repack": {"fetch": True, "use_custom_rank": False, "rank": 0},
|
|
||||||
"retail": {"fetch": True, "use_custom_rank": False, "rank": 0},
|
|
||||||
"site": {"fetch": True, "use_custom_rank": False, "rank": 0},
|
|
||||||
"subbed": {"fetch": True, "use_custom_rank": False, "rank": 0},
|
|
||||||
"upscaled": {"fetch": True, "use_custom_rank": False, "rank": 0},
|
|
||||||
"scene": {"fetch": True, "use_custom_rank": False, "rank": 0},
|
|
||||||
},
|
|
||||||
"trash": {
|
|
||||||
"cam": {"fetch": False, "use_custom_rank": False, "rank": 0},
|
|
||||||
"clean_audio": {"fetch": False, "use_custom_rank": False, "rank": 0},
|
|
||||||
"pdtv": {"fetch": False, "use_custom_rank": False, "rank": 0},
|
|
||||||
"r5": {"fetch": False, "use_custom_rank": False, "rank": 0},
|
|
||||||
"screener": {"fetch": False, "use_custom_rank": False, "rank": 0},
|
|
||||||
"size": {"fetch": False, "use_custom_rank": False, "rank": 0},
|
|
||||||
"telecine": {"fetch": False, "use_custom_rank": False, "rank": 0},
|
|
||||||
"telesync": {"fetch": False, "use_custom_rank": False, "rank": 0},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
rtn_settings = SettingsModel(**default_settings)
|
|
||||||
rtn_ranking = BestRanking()
|
|
||||||
|
|
||||||
# For use anywhere
|
|
||||||
rtn = RTN(settings=rtn_settings, ranking_model=rtn_ranking)
|
# Web Config Initialization
|
||||||
|
# languages = [language for language in PTT.parse.LANGUAGES_TRANSLATION_TABLE.values()]
|
||||||
|
# languages.insert(0, "Unknown")
|
||||||
|
# languages.insert(1, "Multi")
|
||||||
|
web_config = {
|
||||||
|
# "languages": languages,
|
||||||
|
"resolutions": [resolution.value for resolution in RTN.models.Resolution],
|
||||||
|
"resultFormat": ["title", "metadata", "seeders", "size", "tracker", "languages"],
|
||||||
|
}
|
||||||
|
|
||||||
database_url = (
|
database_url = (
|
||||||
settings.DATABASE_PATH
|
settings.DATABASE_PATH
|
||||||
@@ -267,44 +601,32 @@ database = Database(
|
|||||||
)
|
)
|
||||||
|
|
||||||
trackers = [
|
trackers = [
|
||||||
"tracker:https://tracker.gbitt.info:443/announce",
|
"udp://tracker-udp.gbitt.info:80/announce",
|
||||||
"tracker:udp://discord.heihachi.pw:6969/announce",
|
"udp://tracker.0x7c0.com:6969/announce",
|
||||||
"tracker:http://tracker.corpscorp.online:80/announce",
|
"udp://opentracker.io:6969/announce",
|
||||||
"tracker:udp://tracker.leechers-paradise.org:6969/announce",
|
"udp://leet-tracker.moe:1337/announce",
|
||||||
"tracker:https://tracker.renfei.net:443/announce",
|
"udp://tracker.torrent.eu.org:451/announce",
|
||||||
"tracker:udp://exodus.desync.com:6969/announce",
|
"udp://tracker.tiny-vps.com:6969/announce",
|
||||||
"tracker:http://tracker.xiaoduola.xyz:6969/announce",
|
"udp://tracker.leechers-paradise.org:6969/announce",
|
||||||
"tracker:udp://ipv4.tracker.harry.lu:80/announce",
|
"udp://tracker.pomf.se:80/announce",
|
||||||
"tracker:udp://tracker.torrent.eu.org:451/announce",
|
"udp://9.rarbg.me:2710/announce",
|
||||||
"tracker:udp://tracker.coppersurfer.tk:6969/announce",
|
"http://tracker.gbitt.info:80/announce",
|
||||||
"tracker:http://tracker.dmcomic.org:2710/announce",
|
"udp://tracker.bittor.pw:1337/announce",
|
||||||
"tracker:http://www.genesis-sp.org:2710/announce",
|
"udp://open.free-tracker.ga:6969/announce",
|
||||||
"tracker:http://t.jaekr.sh:6969/announce",
|
"udp://open.stealth.si:80/announce",
|
||||||
"tracker:http://tracker.bt-hash.com:80/announce",
|
"udp://retracker01-msk-virt.corbina.net:80/announce",
|
||||||
"tracker:https://tracker.tamersunion.org:443/announce",
|
"udp://tracker.openbittorrent.com:80/announce",
|
||||||
"tracker:udp://open.stealth.si:80/announce",
|
"udp://tracker.opentrackr.org:1337/announce",
|
||||||
"tracker:udp://tracker.opentrackr.org:1337/announce",
|
"udp://isk.richardsw.club:6969/announce",
|
||||||
"tracker:udp://leet-tracker.moe:1337/announce",
|
"https://tracker.gbitt.info:443/announce",
|
||||||
"tracker:udp://oh.fuuuuuck.com:6969/announce",
|
"udp://tracker.coppersurfer.tk:6969/announce",
|
||||||
"tracker:udp://tracker.bittor.pw:1337/announce",
|
"udp://oh.fuuuuuck.com:6969/announce",
|
||||||
"tracker:udp://explodie.org:6969/announce",
|
"udp://ipv4.tracker.harry.lu:80/announce",
|
||||||
"tracker:http://finbytes.org:80/announce.php",
|
"udp://open.demonii.com:1337/announce",
|
||||||
"tracker:udp://tracker.dump.cl:6969/announce",
|
"https://tracker.tamersunion.org:443/announce",
|
||||||
"tracker:udp://open.free-tracker.ga:6969/announce",
|
"https://tracker.renfei.net:443/announce",
|
||||||
"tracker:http://tracker.gbitt.info:80/announce",
|
"udp://open.tracker.cl:1337/announce",
|
||||||
"tracker:udp://isk.richardsw.club:6969/announce",
|
"udp://tracker.internetwarriors.net:1337/announce",
|
||||||
"tracker:http://bt1.xxxxbt.cc:6969/announce",
|
"udp://exodus.desync.com:6969/announce",
|
||||||
"tracker:udp://tracker.qu.ax:6969/announce",
|
"udp://tracker.dump.cl:6969/announce",
|
||||||
"tracker:udp://opentracker.io:6969/announce",
|
|
||||||
"tracker:udp://tracker.internetwarriors.net:1337/announce",
|
|
||||||
"tracker:udp://tracker.0x7c0.com:6969/announce",
|
|
||||||
"tracker:udp://9.rarbg.me:2710/announce",
|
|
||||||
"tracker:udp://tracker.pomf.se:80/announce",
|
|
||||||
"tracker:udp://tracker.openbittorrent.com:80/announce",
|
|
||||||
"tracker:udp://open.tracker.cl:1337/announce",
|
|
||||||
"tracker:http://www.torrentsnipe.info:2701/announce",
|
|
||||||
"tracker:udp://retracker01-msk-virt.corbina.net:80/announce",
|
|
||||||
"tracker:udp://open.demonii.com:1337/announce",
|
|
||||||
"tracker:udp://tracker-udp.gbitt.info:80/announce",
|
|
||||||
"tracker:udp://tracker.tiny-vps.com:6969/announce",
|
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
import time
|
||||||
|
import uuid
|
||||||
|
from starlette.background import BackgroundTask
|
||||||
|
from fastapi.responses import FileResponse
|
||||||
|
|
||||||
|
from comet.utils.models import settings, database
|
||||||
|
from comet.utils.logger import logger
|
||||||
|
import mediaflow_proxy.handlers
|
||||||
|
import mediaflow_proxy.utils.http_utils
|
||||||
|
|
||||||
|
|
||||||
|
async def on_stream_end(connection_id: str, ip: str):
|
||||||
|
try:
|
||||||
|
await database.execute(
|
||||||
|
"DELETE FROM active_connections WHERE id = :connection_id AND ip = :ip",
|
||||||
|
{"connection_id": connection_id, "ip": ip},
|
||||||
|
)
|
||||||
|
logger.log(
|
||||||
|
"STREAM", f"Stream ended - Connection: {connection_id} from IP: {ip}"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(
|
||||||
|
f"Error handling stream end for connection {connection_id} from IP {ip}: {e}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def check_ip_connections(ip: str):
|
||||||
|
if settings.PROXY_DEBRID_STREAM_MAX_CONNECTIONS <= -1:
|
||||||
|
return True
|
||||||
|
|
||||||
|
try:
|
||||||
|
count = await database.fetch_val(
|
||||||
|
"SELECT COUNT(*) FROM active_connections WHERE ip = :ip",
|
||||||
|
{"ip": ip},
|
||||||
|
)
|
||||||
|
if count >= settings.PROXY_DEBRID_STREAM_MAX_CONNECTIONS:
|
||||||
|
logger.log(
|
||||||
|
"STREAM",
|
||||||
|
f"Connection limit reached for IP: {ip} ({count} active connections)",
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Error checking IP connections for {ip}: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
async def add_active_connection(media_id: str, ip: str):
|
||||||
|
connection_id = str(uuid.uuid4())
|
||||||
|
await database.execute(
|
||||||
|
"INSERT INTO active_connections (id, ip, content, timestamp) VALUES (:connection_id, :ip, :content, :timestamp)",
|
||||||
|
{
|
||||||
|
"connection_id": connection_id,
|
||||||
|
"ip": ip,
|
||||||
|
"content": media_id,
|
||||||
|
"timestamp": time.time(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
logger.log(
|
||||||
|
"STREAM",
|
||||||
|
f"New stream connection - ID: {connection_id}, IP: {ip}, Content: {media_id}",
|
||||||
|
)
|
||||||
|
return connection_id
|
||||||
|
|
||||||
|
|
||||||
|
async def combined_background_tasks(
|
||||||
|
connection_id: str, ip: str, streamer_close_task: BackgroundTask
|
||||||
|
):
|
||||||
|
await streamer_close_task()
|
||||||
|
await on_stream_end(connection_id, ip)
|
||||||
|
|
||||||
|
|
||||||
|
async def custom_handle_stream_request(
|
||||||
|
method: str,
|
||||||
|
video_url: str,
|
||||||
|
proxy_headers: mediaflow_proxy.utils.http_utils.ProxyRequestHeaders,
|
||||||
|
media_id: str,
|
||||||
|
ip: str,
|
||||||
|
):
|
||||||
|
if not await check_ip_connections(ip):
|
||||||
|
return FileResponse("comet/assets/proxylimit.mp4")
|
||||||
|
|
||||||
|
connection_id = await add_active_connection(media_id, ip)
|
||||||
|
|
||||||
|
response = await mediaflow_proxy.handlers.handle_stream_request(
|
||||||
|
method, video_url, proxy_headers
|
||||||
|
)
|
||||||
|
original_background_task = response.background
|
||||||
|
response.background = BackgroundTask(
|
||||||
|
combined_background_tasks,
|
||||||
|
connection_id=connection_id,
|
||||||
|
ip=ip,
|
||||||
|
streamer_close_task=original_background_task,
|
||||||
|
)
|
||||||
|
return response
|
||||||
@@ -0,0 +1,490 @@
|
|||||||
|
import hashlib
|
||||||
|
import re
|
||||||
|
import bencodepy
|
||||||
|
import aiohttp
|
||||||
|
import anyio
|
||||||
|
import asyncio
|
||||||
|
import orjson
|
||||||
|
import time
|
||||||
|
|
||||||
|
from urllib.parse import parse_qs, urlparse
|
||||||
|
from demagnetize.core import Demagnetizer
|
||||||
|
from torf import Magnet
|
||||||
|
from RTN import ParsedData, parse
|
||||||
|
|
||||||
|
from comet.utils.logger import logger
|
||||||
|
from comet.utils.models import settings, database
|
||||||
|
from comet.utils.general import is_video, default_dump
|
||||||
|
|
||||||
|
info_hash_pattern = re.compile(r"btih:([a-fA-F0-9]{40})")
|
||||||
|
|
||||||
|
|
||||||
|
def extract_trackers_from_magnet(magnet_uri: str):
|
||||||
|
try:
|
||||||
|
parsed = urlparse(magnet_uri)
|
||||||
|
params = parse_qs(parsed.query)
|
||||||
|
return params.get("tr", [])
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to extract trackers from magnet URI: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
async def download_torrent(session: aiohttp.ClientSession, url: str):
|
||||||
|
try:
|
||||||
|
timeout = aiohttp.ClientTimeout(total=settings.GET_TORRENT_TIMEOUT)
|
||||||
|
async with session.get(url, allow_redirects=False, timeout=timeout) as response:
|
||||||
|
if response.status == 200:
|
||||||
|
return (await response.read(), None, None)
|
||||||
|
|
||||||
|
location = response.headers.get("Location", "")
|
||||||
|
if location:
|
||||||
|
match = info_hash_pattern.search(location)
|
||||||
|
if match:
|
||||||
|
return (None, match.group(1), location)
|
||||||
|
return (None, None, None)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(
|
||||||
|
f"Failed to download torrent from {url}: {e} (in most cases, you can ignore this error)"
|
||||||
|
)
|
||||||
|
return (None, None, None)
|
||||||
|
|
||||||
|
|
||||||
|
demagnetizer = Demagnetizer()
|
||||||
|
|
||||||
|
|
||||||
|
async def get_torrent_from_magnet(magnet_uri: str):
|
||||||
|
try:
|
||||||
|
magnet = Magnet.from_string(magnet_uri)
|
||||||
|
with anyio.fail_after(60):
|
||||||
|
torrent_data = await demagnetizer.demagnetize(magnet)
|
||||||
|
if torrent_data:
|
||||||
|
return torrent_data.dump()
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to get torrent from magnet: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def extract_torrent_metadata(content: bytes):
|
||||||
|
try:
|
||||||
|
torrent_data = bencodepy.decode(content)
|
||||||
|
info = torrent_data[b"info"]
|
||||||
|
info_encoded = bencodepy.encode(info)
|
||||||
|
m = hashlib.sha1()
|
||||||
|
m.update(info_encoded)
|
||||||
|
info_hash = m.hexdigest()
|
||||||
|
|
||||||
|
announce_list = [
|
||||||
|
tracker[0].decode() for tracker in torrent_data.get(b"announce-list", [])
|
||||||
|
]
|
||||||
|
|
||||||
|
metadata = {"info_hash": info_hash, "announce_list": announce_list, "files": []}
|
||||||
|
|
||||||
|
files = info[b"files"] if b"files" in info else [info]
|
||||||
|
for idx, file in enumerate(files):
|
||||||
|
name = (
|
||||||
|
file[b"path"][-1].decode()
|
||||||
|
if b"path" in file
|
||||||
|
else file[b"name"].decode()
|
||||||
|
)
|
||||||
|
|
||||||
|
if not is_video(name) or "sample" in name.lower():
|
||||||
|
continue
|
||||||
|
|
||||||
|
size = file[b"length"]
|
||||||
|
|
||||||
|
metadata["files"].append({"index": idx, "name": name, "size": size})
|
||||||
|
|
||||||
|
return metadata
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to extract torrent metadata: {e}")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
async def add_torrent(
|
||||||
|
info_hash: str,
|
||||||
|
seeders: int,
|
||||||
|
tracker: str,
|
||||||
|
media_id: str,
|
||||||
|
search_season: int,
|
||||||
|
sources: list,
|
||||||
|
file_index: int,
|
||||||
|
title: str,
|
||||||
|
size: int,
|
||||||
|
parsed: ParsedData,
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
parsed_season = parsed.seasons[0] if parsed.seasons else search_season
|
||||||
|
parsed_episode = parsed.episodes[0] if parsed.episodes else None
|
||||||
|
|
||||||
|
if parsed_episode is not None:
|
||||||
|
await database.execute(
|
||||||
|
"""
|
||||||
|
DELETE FROM torrents
|
||||||
|
WHERE info_hash = :info_hash
|
||||||
|
AND season = :season
|
||||||
|
AND episode IS NULL
|
||||||
|
""",
|
||||||
|
{
|
||||||
|
"info_hash": info_hash,
|
||||||
|
"season": parsed_season,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
logger.log(
|
||||||
|
"SCRAPER",
|
||||||
|
f"Deleted season-only entry for S{parsed_season:02d} of {info_hash}",
|
||||||
|
)
|
||||||
|
|
||||||
|
await database.execute(
|
||||||
|
f"""
|
||||||
|
INSERT {"OR IGNORE " if settings.DATABASE_TYPE == "sqlite" else ""}
|
||||||
|
INTO torrents
|
||||||
|
VALUES (:media_id, :info_hash, :file_index, :season, :episode, :title, :seeders, :size, :tracker, :sources, :parsed, :timestamp)
|
||||||
|
{" ON CONFLICT DO NOTHING" if settings.DATABASE_TYPE == "postgresql" else ""}
|
||||||
|
""",
|
||||||
|
{
|
||||||
|
"media_id": media_id,
|
||||||
|
"info_hash": info_hash,
|
||||||
|
"file_index": file_index,
|
||||||
|
"season": parsed_season,
|
||||||
|
"episode": parsed_episode,
|
||||||
|
"title": title,
|
||||||
|
"seeders": seeders,
|
||||||
|
"size": size,
|
||||||
|
"tracker": tracker,
|
||||||
|
"sources": orjson.dumps(sources).decode("utf-8"),
|
||||||
|
"parsed": orjson.dumps(parsed, default_dump).decode("utf-8"),
|
||||||
|
"timestamp": time.time(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
additional = ""
|
||||||
|
if parsed_season:
|
||||||
|
additional += f" - S{parsed_season:02d}"
|
||||||
|
additional += f"E{parsed_episode:02d}" if parsed_episode else ""
|
||||||
|
|
||||||
|
logger.log("SCRAPER", f"Added torrent for {media_id} - {title}{additional}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to add torrent for {info_hash}: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
class AddTorrentQueue:
|
||||||
|
def __init__(self, max_concurrent: int = 10):
|
||||||
|
self.queue = asyncio.Queue()
|
||||||
|
self.max_concurrent = max_concurrent
|
||||||
|
self.is_running = False
|
||||||
|
self.semaphore = asyncio.Semaphore(max_concurrent)
|
||||||
|
|
||||||
|
async def add_torrent(
|
||||||
|
self,
|
||||||
|
magnet_url: str,
|
||||||
|
seeders: int,
|
||||||
|
tracker: str,
|
||||||
|
media_id: str,
|
||||||
|
search_season: int,
|
||||||
|
):
|
||||||
|
if not settings.DOWNLOAD_TORRENT_FILES:
|
||||||
|
return
|
||||||
|
|
||||||
|
await self.queue.put((magnet_url, seeders, tracker, media_id, search_season))
|
||||||
|
if not self.is_running:
|
||||||
|
self.is_running = True
|
||||||
|
asyncio.create_task(self._process_queue())
|
||||||
|
|
||||||
|
async def _process_queue(self):
|
||||||
|
while self.is_running:
|
||||||
|
try:
|
||||||
|
(
|
||||||
|
magnet_url,
|
||||||
|
seeders,
|
||||||
|
tracker,
|
||||||
|
media_id,
|
||||||
|
search_season,
|
||||||
|
) = await self.queue.get()
|
||||||
|
|
||||||
|
async with self.semaphore:
|
||||||
|
try:
|
||||||
|
content = await get_torrent_from_magnet(magnet_url)
|
||||||
|
if content:
|
||||||
|
metadata = extract_torrent_metadata(content)
|
||||||
|
for file in metadata["files"]:
|
||||||
|
parsed = parse(file["name"])
|
||||||
|
|
||||||
|
await add_torrent(
|
||||||
|
metadata["info_hash"],
|
||||||
|
seeders,
|
||||||
|
tracker,
|
||||||
|
media_id,
|
||||||
|
search_season,
|
||||||
|
metadata["announce_list"],
|
||||||
|
file["index"],
|
||||||
|
file["name"],
|
||||||
|
file["size"],
|
||||||
|
parsed,
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
self.queue.task_done()
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
|
||||||
|
self.is_running = False
|
||||||
|
|
||||||
|
|
||||||
|
add_torrent_queue = AddTorrentQueue()
|
||||||
|
|
||||||
|
|
||||||
|
class TorrentUpdateQueue:
|
||||||
|
def __init__(self, batch_size: int = 100, flush_interval: float = 5.0):
|
||||||
|
self.queue = asyncio.Queue()
|
||||||
|
self.batch_size = batch_size
|
||||||
|
self.flush_interval = flush_interval
|
||||||
|
self.is_running = False
|
||||||
|
self.batches = {"to_check": [], "to_delete": [], "inserts": [], "updates": []}
|
||||||
|
self.last_error_time = 0
|
||||||
|
self.error_backoff = 1
|
||||||
|
|
||||||
|
async def add_torrent_info(self, file_info: dict, media_id: str = None):
|
||||||
|
await self.queue.put((file_info, media_id))
|
||||||
|
if not self.is_running:
|
||||||
|
self.is_running = True
|
||||||
|
asyncio.create_task(self._process_queue())
|
||||||
|
|
||||||
|
async def _process_queue(self):
|
||||||
|
last_flush_time = time.time()
|
||||||
|
|
||||||
|
while self.is_running:
|
||||||
|
try:
|
||||||
|
while not self.queue.empty():
|
||||||
|
try:
|
||||||
|
file_info, media_id = self.queue.get_nowait()
|
||||||
|
await self._process_file_info(file_info, media_id)
|
||||||
|
except asyncio.QueueEmpty:
|
||||||
|
break
|
||||||
|
|
||||||
|
current_time = time.time()
|
||||||
|
|
||||||
|
if (
|
||||||
|
current_time - last_flush_time >= self.flush_interval
|
||||||
|
or self.queue.empty()
|
||||||
|
) and any(len(batch) > 0 for batch in self.batches.values()):
|
||||||
|
await self._flush_batch()
|
||||||
|
last_flush_time = current_time
|
||||||
|
|
||||||
|
if self.queue.empty() and not any(
|
||||||
|
len(batch) > 0 for batch in self.batches.values()
|
||||||
|
):
|
||||||
|
self.is_running = False
|
||||||
|
break
|
||||||
|
|
||||||
|
await asyncio.sleep(0.1)
|
||||||
|
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Error in _process_queue: {e}")
|
||||||
|
await self._handle_error(e)
|
||||||
|
|
||||||
|
if any(len(batch) > 0 for batch in self.batches.values()):
|
||||||
|
await self._flush_batch()
|
||||||
|
|
||||||
|
self.is_running = False
|
||||||
|
|
||||||
|
async def _flush_batch(self):
|
||||||
|
try:
|
||||||
|
if self.batches["to_check"]:
|
||||||
|
sub_batch_size = 100
|
||||||
|
for i in range(0, len(self.batches["to_check"]), sub_batch_size):
|
||||||
|
sub_batch = self.batches["to_check"][i : i + sub_batch_size]
|
||||||
|
placeholders = []
|
||||||
|
params = {}
|
||||||
|
|
||||||
|
for idx, item in enumerate(sub_batch):
|
||||||
|
key = str(idx)
|
||||||
|
placeholders.append(
|
||||||
|
f"(CAST(:info_hash_{key} AS TEXT) = info_hash AND "
|
||||||
|
f"(CAST(:season_{key} AS INTEGER) IS NULL AND season IS NULL OR season = CAST(:season_{key} AS INTEGER)) AND "
|
||||||
|
f"(CAST(:episode_{key} AS INTEGER) IS NULL AND episode IS NULL OR episode = CAST(:episode_{key} AS INTEGER)))"
|
||||||
|
)
|
||||||
|
params[f"info_hash_{key}"] = item["info_hash"]
|
||||||
|
params[f"season_{key}"] = item["season"]
|
||||||
|
params[f"episode_{key}"] = item["episode"]
|
||||||
|
|
||||||
|
query = f"""
|
||||||
|
SELECT info_hash, season, episode
|
||||||
|
FROM torrents
|
||||||
|
WHERE {" OR ".join(placeholders)}
|
||||||
|
"""
|
||||||
|
|
||||||
|
async with database.transaction():
|
||||||
|
existing_rows = await database.fetch_all(query, params)
|
||||||
|
|
||||||
|
existing_set = {
|
||||||
|
(
|
||||||
|
row["info_hash"],
|
||||||
|
row["season"] if row["season"] is not None else None,
|
||||||
|
row["episode"] if row["episode"] is not None else None,
|
||||||
|
)
|
||||||
|
for row in existing_rows
|
||||||
|
}
|
||||||
|
|
||||||
|
for item in sub_batch:
|
||||||
|
key = (item["info_hash"], item["season"], item["episode"])
|
||||||
|
if key in existing_set:
|
||||||
|
self.batches["updates"].append(item["params"])
|
||||||
|
else:
|
||||||
|
self.batches["inserts"].append(item["params"])
|
||||||
|
|
||||||
|
self.batches["to_check"] = []
|
||||||
|
|
||||||
|
if self.batches["to_delete"]:
|
||||||
|
sub_batch_size = 100
|
||||||
|
for i in range(0, len(self.batches["to_delete"]), sub_batch_size):
|
||||||
|
sub_batch = self.batches["to_delete"][i : i + sub_batch_size]
|
||||||
|
|
||||||
|
placeholders = []
|
||||||
|
params = {}
|
||||||
|
for idx, item in enumerate(sub_batch):
|
||||||
|
key_suffix = f"_{idx}"
|
||||||
|
placeholders.append(
|
||||||
|
f"(CAST(:info_hash{key_suffix} AS TEXT), CAST(:season{key_suffix} AS INTEGER))"
|
||||||
|
)
|
||||||
|
params[f"info_hash{key_suffix}"] = item["info_hash"]
|
||||||
|
params[f"season{key_suffix}"] = item["season"]
|
||||||
|
|
||||||
|
async with database.transaction():
|
||||||
|
delete_query = f"""
|
||||||
|
DELETE FROM torrents
|
||||||
|
WHERE (info_hash, season) IN (
|
||||||
|
{",".join(placeholders)}
|
||||||
|
)
|
||||||
|
AND episode IS NULL
|
||||||
|
"""
|
||||||
|
await database.execute(delete_query, params)
|
||||||
|
|
||||||
|
self.batches["to_delete"] = []
|
||||||
|
|
||||||
|
if self.batches["inserts"]:
|
||||||
|
sub_batch_size = 100
|
||||||
|
for i in range(0, len(self.batches["inserts"]), sub_batch_size):
|
||||||
|
sub_batch = self.batches["inserts"][i : i + sub_batch_size]
|
||||||
|
async with database.transaction():
|
||||||
|
insert_query = f"""
|
||||||
|
INSERT {"OR IGNORE " if settings.DATABASE_TYPE == "sqlite" else ""}
|
||||||
|
INTO torrents
|
||||||
|
VALUES (
|
||||||
|
:media_id,
|
||||||
|
:info_hash,
|
||||||
|
:file_index,
|
||||||
|
:season,
|
||||||
|
:episode,
|
||||||
|
:title,
|
||||||
|
:seeders,
|
||||||
|
:size,
|
||||||
|
:tracker,
|
||||||
|
:sources,
|
||||||
|
:parsed,
|
||||||
|
:timestamp
|
||||||
|
)
|
||||||
|
{" ON CONFLICT DO NOTHING" if settings.DATABASE_TYPE == "postgresql" else ""}
|
||||||
|
"""
|
||||||
|
await database.execute_many(insert_query, sub_batch)
|
||||||
|
|
||||||
|
if len(self.batches["inserts"]) > 0:
|
||||||
|
logger.log(
|
||||||
|
"SCRAPER",
|
||||||
|
f"Inserted {len(self.batches['inserts'])} new torrents in batch",
|
||||||
|
)
|
||||||
|
self.batches["inserts"] = []
|
||||||
|
|
||||||
|
if self.batches["updates"]:
|
||||||
|
sub_batch_size = 100
|
||||||
|
for i in range(0, len(self.batches["updates"]), sub_batch_size):
|
||||||
|
sub_batch = self.batches["updates"][i : i + sub_batch_size]
|
||||||
|
async with database.transaction():
|
||||||
|
update_query = """
|
||||||
|
UPDATE torrents
|
||||||
|
SET title = CAST(:title AS TEXT),
|
||||||
|
file_index = CAST(:file_index AS INTEGER),
|
||||||
|
size = CAST(:size AS BIGINT),
|
||||||
|
seeders = CAST(:seeders AS INTEGER),
|
||||||
|
tracker = CAST(:tracker AS TEXT),
|
||||||
|
sources = CAST(:sources AS TEXT),
|
||||||
|
parsed = CAST(:parsed AS TEXT),
|
||||||
|
timestamp = CAST(:timestamp AS FLOAT),
|
||||||
|
media_id = CAST(:media_id AS TEXT)
|
||||||
|
WHERE info_hash = CAST(:info_hash AS TEXT)
|
||||||
|
AND (CAST(:season AS INTEGER) IS NULL AND season IS NULL OR season = CAST(:season AS INTEGER))
|
||||||
|
AND (CAST(:episode AS INTEGER) IS NULL AND episode IS NULL OR episode = CAST(:episode AS INTEGER))
|
||||||
|
"""
|
||||||
|
await database.execute_many(update_query, sub_batch)
|
||||||
|
|
||||||
|
if len(self.batches["updates"]) > 0:
|
||||||
|
logger.log(
|
||||||
|
"SCRAPER",
|
||||||
|
f"Updated {len(self.batches['updates'])} existing torrents in batch",
|
||||||
|
)
|
||||||
|
self.batches["updates"] = []
|
||||||
|
|
||||||
|
self.error_backoff = 1
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
await self._handle_error(e)
|
||||||
|
|
||||||
|
async def _process_file_info(self, file_info: dict, media_id: str = None):
|
||||||
|
try:
|
||||||
|
params = {
|
||||||
|
"info_hash": file_info["info_hash"],
|
||||||
|
"file_index": file_info["index"],
|
||||||
|
"season": file_info["season"],
|
||||||
|
"episode": file_info["episode"],
|
||||||
|
"title": file_info["title"],
|
||||||
|
"seeders": file_info["seeders"],
|
||||||
|
"size": file_info["size"],
|
||||||
|
"tracker": file_info["tracker"],
|
||||||
|
"sources": orjson.dumps(file_info["sources"]).decode("utf-8"),
|
||||||
|
"parsed": orjson.dumps(
|
||||||
|
file_info["parsed"], default=default_dump
|
||||||
|
).decode("utf-8"),
|
||||||
|
"timestamp": time.time(),
|
||||||
|
"media_id": media_id,
|
||||||
|
}
|
||||||
|
|
||||||
|
self.batches["to_check"].append(
|
||||||
|
{
|
||||||
|
"info_hash": file_info["info_hash"],
|
||||||
|
"season": file_info["season"],
|
||||||
|
"episode": file_info["episode"],
|
||||||
|
"params": params,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if file_info["episode"] is not None:
|
||||||
|
self.batches["to_delete"].append(
|
||||||
|
{"info_hash": file_info["info_hash"], "season": file_info["season"]}
|
||||||
|
)
|
||||||
|
|
||||||
|
await self._check_batch_size()
|
||||||
|
|
||||||
|
finally:
|
||||||
|
self.queue.task_done()
|
||||||
|
|
||||||
|
async def _check_batch_size(self):
|
||||||
|
if any(len(batch) >= self.batch_size for batch in self.batches.values()):
|
||||||
|
await self._flush_batch()
|
||||||
|
self.error_backoff = 1
|
||||||
|
|
||||||
|
async def _handle_error(self, e: Exception):
|
||||||
|
current_time = time.time()
|
||||||
|
if current_time - self.last_error_time < 5:
|
||||||
|
self.error_backoff = min(self.error_backoff * 2, 30)
|
||||||
|
else:
|
||||||
|
self.error_backoff = 1
|
||||||
|
|
||||||
|
self.last_error_time = current_time
|
||||||
|
logger.warning(f"Database error in torrent batch processing: {e}")
|
||||||
|
logger.warning(f"Waiting {self.error_backoff} seconds before retry")
|
||||||
|
await asyncio.sleep(self.error_backoff)
|
||||||
|
|
||||||
|
|
||||||
|
torrent_update_queue = TorrentUpdateQueue()
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import aiohttp
|
||||||
|
|
||||||
|
from comet.utils.models import trackers
|
||||||
|
from comet.utils.logger import logger
|
||||||
|
|
||||||
|
|
||||||
|
async def download_best_trackers():
|
||||||
|
try:
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
response = await session.get(
|
||||||
|
"https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_best.txt"
|
||||||
|
)
|
||||||
|
response = await response.text()
|
||||||
|
|
||||||
|
other_trackers = [tracker for tracker in response.split("\n") if tracker]
|
||||||
|
trackers.extend(other_trackers)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to download best trackers: {e}")
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
services:
|
||||||
|
comet:
|
||||||
|
container_name: comet
|
||||||
|
image: g0ldyy/comet:rewrite
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "8000:8000"
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
volumes:
|
||||||
|
- ./data/comet:/app/data
|
||||||
|
healthcheck:
|
||||||
|
test: wget -qO- http://127.0.0.1:8000/health
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 20s
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
server {
|
||||||
|
server_name example.com;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://localhost:8000;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
}
|
||||||
+27
-37
@@ -1,37 +1,27 @@
|
|||||||
[tool.poetry]
|
[project]
|
||||||
name = "comet"
|
name = "comet"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
description = "Stremio's fastest torrent/debrid search add-on."
|
description = "Stremio's fastest torrent/debrid search add-on."
|
||||||
authors = ["Goldy"]
|
authors = [
|
||||||
license = "MIT"
|
{ name = "Goldy", email = "153996346+g0ldyy@users.noreply.github.com" }
|
||||||
readme = "README.md"
|
]
|
||||||
repository = "https://github.com/g0ldyy/comet"
|
requires-python = ">=3.11"
|
||||||
|
dependencies = [
|
||||||
[tool.poetry.dependencies]
|
"aiohttp",
|
||||||
python = "^3.11"
|
"aiosqlite",
|
||||||
uvicorn = "*"
|
"asyncio",
|
||||||
fastapi = "*"
|
"asyncpg",
|
||||||
aiohttp = "*"
|
"bencode-py",
|
||||||
asyncio = "*"
|
"curl-cffi",
|
||||||
loguru = "*"
|
"databases",
|
||||||
databases = "*"
|
"demagnetize",
|
||||||
pydantic-settings = "*"
|
"fastapi",
|
||||||
bencode-py = "*"
|
"gunicorn",
|
||||||
httpx = "*"
|
"jinja2",
|
||||||
curl-cffi = "*"
|
"loguru",
|
||||||
orjson = "*"
|
"mediaflow-proxy",
|
||||||
asyncpg = "*"
|
"orjson",
|
||||||
aiosqlite = "*"
|
"pydantic-settings",
|
||||||
jinja2 = "*"
|
"rank-torrent-name",
|
||||||
rank-torrent-name = "*"
|
"uvicorn",
|
||||||
parsett = "*"
|
]
|
||||||
|
|
||||||
|
|
||||||
[tool.poetry.group.dev.dependencies]
|
|
||||||
isort = "*"
|
|
||||||
pyright = "*"
|
|
||||||
pytest = "*"
|
|
||||||
|
|
||||||
[build-system]
|
|
||||||
requires = ["poetry-core"]
|
|
||||||
build-backend = "poetry.core.masonry.api"
|
|
||||||
|
|||||||
Reference in New Issue
Block a user