From 464683e9ce71ed01ada2733920113a02d12572f4 Mon Sep 17 00:00:00 2001 From: Vladan Popovic Date: Sun, 15 Sep 2024 00:39:25 +0200 Subject: [PATCH] everything works, needs makeup though --- .gitignore | 10 +++++ .python-version | 1 + README.md | 3 ++ pyproject.toml | 50 +++++++++++++++++++++ requirements-dev.lock | 70 +++++++++++++++++++++++++++++ requirements.lock | 65 +++++++++++++++++++++++++++ src/torrent_downloader/__init__.py | 2 + src/torrent_downloader/__main__.py | 3 ++ src/torrent_downloader/app.py | 48 ++++++++++++++++++++ src/torrent_downloader/client.py | 71 ++++++++++++++++++++++++++++++ src/torrent_downloader/server.py | 4 ++ templates/active_torrents.html | 12 +++++ templates/index.html | 23 ++++++++++ templates/torrents.html | 15 +++++++ 14 files changed, 377 insertions(+) create mode 100644 .gitignore create mode 100644 .python-version create mode 100644 README.md create mode 100644 pyproject.toml create mode 100644 requirements-dev.lock create mode 100644 requirements.lock create mode 100644 src/torrent_downloader/__init__.py create mode 100644 src/torrent_downloader/__main__.py create mode 100644 src/torrent_downloader/app.py create mode 100644 src/torrent_downloader/client.py create mode 100644 src/torrent_downloader/server.py create mode 100644 templates/active_torrents.html create mode 100644 templates/index.html create mode 100644 templates/torrents.html diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ae8554d --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +# python generated files +__pycache__/ +*.py[oc] +build/ +dist/ +wheels/ +*.egg-info + +# venv +.venv diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..d9506ce --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.12.5 diff --git a/README.md b/README.md new file mode 100644 index 0000000..4110ed1 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# torrent-downloader + +Search TPB and download torrents with transmission \o/ diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..a673db7 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,50 @@ +[project] +name = "torrent-downloader" +version = "0.1.0" +description = "Add your description here" +authors = [ + { name = "Vladan Popovic", email = "vladanovic@gmail.com" } +] +dependencies = [ + "tpblite>=0.8.0", + "transmission-rpc>=7.0.11", + "pydantic-settings>=2.4.0", + "fastapi>=0.114.2", + "jinja2>=3.1.4", + "cinemagoer>=2023.5.1", +] +readme = "README.md" +requires-python = ">= 3.8" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.rye] +managed = true +dev-dependencies = [ + "uvicorn>=0.30.6", +] + +[tool.hatch.metadata] +allow-direct-references = true + +[tool.hatch.build.targets.wheel] +packages = ["src/torrent_downloader"] + +[tool.rye.scripts] +client = "python -m torrent_downloader" +server = "python -m torrent_downloader.server" + +[tool.pyright] +venv = ".venv" +venvPath = "." + +[tool.ruff] +line-length = 100 +indent-width = 4 + +[tool.ruff.format] +quote-style = "double" +indent-style = "space" +skip-magic-trailing-comma = false diff --git a/requirements-dev.lock b/requirements-dev.lock new file mode 100644 index 0000000..2630043 --- /dev/null +++ b/requirements-dev.lock @@ -0,0 +1,70 @@ +# generated by rye +# use `rye lock` or `rye sync` to update this lockfile +# +# last locked with the following flags: +# pre: false +# features: [] +# all-features: false +# with-sources: false +# generate-hashes: false +# universal: false + +-e file:. +annotated-types==0.7.0 + # via pydantic +anyio==4.4.0 + # via starlette +certifi==2024.8.30 + # via requests +charset-normalizer==3.3.2 + # via requests +cinemagoer==2023.5.1 + # via torrent-downloader +click==8.1.7 + # via uvicorn +fastapi==0.114.2 + # via torrent-downloader +greenlet==3.1.0 + # via sqlalchemy +h11==0.14.0 + # via uvicorn +idna==3.8 + # via anyio + # via requests +jinja2==3.1.4 + # via torrent-downloader +lxml==5.3.0 + # via cinemagoer + # via tpblite +markupsafe==2.1.5 + # via jinja2 +pydantic==2.8.2 + # via fastapi + # via pydantic-settings +pydantic-core==2.20.1 + # via pydantic +pydantic-settings==2.4.0 + # via torrent-downloader +python-dotenv==1.0.1 + # via pydantic-settings +requests==2.32.3 + # via transmission-rpc +sniffio==1.3.1 + # via anyio +sqlalchemy==2.0.34 + # via cinemagoer +starlette==0.38.5 + # via fastapi +tpblite==0.8.0 + # via torrent-downloader +transmission-rpc==7.0.11 + # via torrent-downloader +typing-extensions==4.12.2 + # via fastapi + # via pydantic + # via pydantic-core + # via sqlalchemy + # via transmission-rpc +urllib3==2.2.2 + # via requests +uvicorn==0.30.6 diff --git a/requirements.lock b/requirements.lock new file mode 100644 index 0000000..ce300e2 --- /dev/null +++ b/requirements.lock @@ -0,0 +1,65 @@ +# generated by rye +# use `rye lock` or `rye sync` to update this lockfile +# +# last locked with the following flags: +# pre: false +# features: [] +# all-features: false +# with-sources: false +# generate-hashes: false +# universal: false + +-e file:. +annotated-types==0.7.0 + # via pydantic +anyio==4.4.0 + # via starlette +certifi==2024.8.30 + # via requests +charset-normalizer==3.3.2 + # via requests +cinemagoer==2023.5.1 + # via torrent-downloader +fastapi==0.114.2 + # via torrent-downloader +greenlet==3.1.0 + # via sqlalchemy +idna==3.8 + # via anyio + # via requests +jinja2==3.1.4 + # via torrent-downloader +lxml==5.3.0 + # via cinemagoer + # via tpblite +markupsafe==2.1.5 + # via jinja2 +pydantic==2.8.2 + # via fastapi + # via pydantic-settings +pydantic-core==2.20.1 + # via pydantic +pydantic-settings==2.4.0 + # via torrent-downloader +python-dotenv==1.0.1 + # via pydantic-settings +requests==2.32.3 + # via transmission-rpc +sniffio==1.3.1 + # via anyio +sqlalchemy==2.0.34 + # via cinemagoer +starlette==0.38.5 + # via fastapi +tpblite==0.8.0 + # via torrent-downloader +transmission-rpc==7.0.11 + # via torrent-downloader +typing-extensions==4.12.2 + # via fastapi + # via pydantic + # via pydantic-core + # via sqlalchemy + # via transmission-rpc +urllib3==2.2.2 + # via requests diff --git a/src/torrent_downloader/__init__.py b/src/torrent_downloader/__init__.py new file mode 100644 index 0000000..eb76009 --- /dev/null +++ b/src/torrent_downloader/__init__.py @@ -0,0 +1,2 @@ +def hello() -> str: + return "Hello from torrent-downloader!" diff --git a/src/torrent_downloader/__main__.py b/src/torrent_downloader/__main__.py new file mode 100644 index 0000000..b661db6 --- /dev/null +++ b/src/torrent_downloader/__main__.py @@ -0,0 +1,3 @@ +from .client import main + +main() diff --git a/src/torrent_downloader/app.py b/src/torrent_downloader/app.py new file mode 100644 index 0000000..f2c9efa --- /dev/null +++ b/src/torrent_downloader/app.py @@ -0,0 +1,48 @@ +import urllib.parse +import transmission_rpc +from fastapi import Depends, FastAPI, Request +from fastapi.responses import HTMLResponse +from fastapi.templating import Jinja2Templates + +from torrent_downloader.client import TorrentDownloader + +app = FastAPI() +templates = Jinja2Templates(directory="templates") + + +def get_downloader() -> TorrentDownloader: + return TorrentDownloader() + + +def get_active_torrents(downloader=get_downloader()) -> list[transmission_rpc.Torrent]: + return [t for t in downloader.get_active_torrents() if t.format_eta() != "not available"] + + +@app.get("/", response_class=HTMLResponse) +async def index(request: Request): + return templates.TemplateResponse(request=request, name="index.html") + + +@app.get("/list", response_class=HTMLResponse) +async def list_torrents(request: Request, query: str, downloader=Depends(get_downloader)): + torrents = downloader.search(query) + return templates.TemplateResponse( + request=request, name="torrents.html", context={"torrents": torrents} + ) + + +@app.get("/download", response_class=HTMLResponse) +async def download(request: Request, downloader=Depends(get_downloader)): + magnet = urllib.parse.unquote(str(request.query_params)) + downloader.download_magnet(magnet) + active = downloader.get_active_torrents() + return templates.TemplateResponse( + request=request, name="active_torrents.html", context={"torrents": active} + ) + + +@app.get("/active", response_class=HTMLResponse) +async def active(request: Request, active=Depends(get_active_torrents)): + return templates.TemplateResponse( + request=request, name="active_torrents.html", context={"torrents": active} + ) diff --git a/src/torrent_downloader/client.py b/src/torrent_downloader/client.py new file mode 100644 index 0000000..95add0b --- /dev/null +++ b/src/torrent_downloader/client.py @@ -0,0 +1,71 @@ +from pydantic import SecretStr +from pydantic_settings import BaseSettings, SettingsConfigDict +from tpblite import CATEGORIES, ORDERS, TPB +from tpblite.models.torrents import Torrent as TpbTorrent, Torrents +import transmission_rpc + + +class Settings(BaseSettings): + model_config = SettingsConfigDict(env_prefix="TD_", case_sensitive=False) + + tpb_site: str = "https://tpb.party" + transmission_host: str = "192.168.0.29" + transmission_port: int = 9091 + transmission_user: str = "transmission" + transmission_pass: SecretStr = SecretStr("1304c2ea35e4e3f685691998c67ab58e20cf5d29CLC0a8Wn") + + +class TorrentDownloader: + def __init__(self, config: Settings = Settings()): + self.tpb = TPB(config.tpb_site) + self.transmission = transmission_rpc.Client( + host=config.transmission_host, + port=config.transmission_port, + username=config.transmission_user, + password=config.transmission_pass.get_secret_value(), + ) + + def search(self, term: str, category: int = CATEGORIES.ALL) -> Torrents: + return self.tpb.search( + term, + page=1, + order=ORDERS.SEEDERS.DES, + category=category, + ) + + def download(self, torrent: TpbTorrent): + if torrent is not None and torrent.magnetlink: + self.transmission.add_torrent(torrent.magnetlink) + + def download_magnet(self, magnetlink: str): + self.transmission.add_torrent(magnetlink) + + def get_active_torrents(self) -> list[transmission_rpc.Torrent]: + active, _ = self.transmission.get_recently_active_torrents() + return active + + +def main(): + config = Settings() + td = TorrentDownloader(config) + + initial = input("Search torrent: ") + search_result: Torrents = td.search(initial, CATEGORIES.ALL) + + for idx, item in enumerate(search_result): + print(f"{idx+1}. {item}") + + try: + choice = int(input("Choose your destiny: ")) + if 30 > int(choice) > 0: + torrent = search_result[choice - 1] + else: + print("Wrong destiny :( getting best torrent by hand") + torrent = search_result.getBestTorrent(min_seeds=10) + except Exception as _: + torrent = search_result.getBestTorrent(min_seeds=10) + + if torrent is not None: + td.download(torrent) + else: + print("Cannot find good enough torrent :D") diff --git a/src/torrent_downloader/server.py b/src/torrent_downloader/server.py new file mode 100644 index 0000000..4f513f3 --- /dev/null +++ b/src/torrent_downloader/server.py @@ -0,0 +1,4 @@ +import uvicorn + +if __name__ == "__main__": + uvicorn.run("torrent_downloader.app:app", host="0.0.0.0", port=8000, reload=True) diff --git a/templates/active_torrents.html b/templates/active_torrents.html new file mode 100644 index 0000000..7e124ac --- /dev/null +++ b/templates/active_torrents.html @@ -0,0 +1,12 @@ +{% extends 'index.html' %} + +{% block content %} + +{% endblock %} diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..51769ac --- /dev/null +++ b/templates/index.html @@ -0,0 +1,23 @@ + + + + + + Search and download torrents directly to mediacenter + + + +
+ +
  • Search
  • +
  • Active downloads
  • +
    + {% block content %} +
    + + +
    + {% endblock %} +
    + + diff --git a/templates/torrents.html b/templates/torrents.html new file mode 100644 index 0000000..c8111f4 --- /dev/null +++ b/templates/torrents.html @@ -0,0 +1,15 @@ +{% extends 'index.html' %} + +{% block content %} + +{% endblock %}