everything works, needs makeup though

This commit is contained in:
Vladan Popovic 2024-09-15 00:39:25 +02:00
parent 5130450355
commit 464683e9ce
14 changed files with 377 additions and 0 deletions

10
.gitignore vendored Normal file
View File

@ -0,0 +1,10 @@
# python generated files
__pycache__/
*.py[oc]
build/
dist/
wheels/
*.egg-info
# venv
.venv

1
.python-version Normal file
View File

@ -0,0 +1 @@
3.12.5

3
README.md Normal file
View File

@ -0,0 +1,3 @@
# torrent-downloader
Search TPB and download torrents with transmission \o/

50
pyproject.toml Normal file
View File

@ -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

70
requirements-dev.lock Normal file
View File

@ -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

65
requirements.lock Normal file
View File

@ -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

View File

@ -0,0 +1,2 @@
def hello() -> str:
return "Hello from torrent-downloader!"

View File

@ -0,0 +1,3 @@
from .client import main
main()

View File

@ -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}
)

View File

@ -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")

View File

@ -0,0 +1,4 @@
import uvicorn
if __name__ == "__main__":
uvicorn.run("torrent_downloader.app:app", host="0.0.0.0", port=8000, reload=True)

View File

@ -0,0 +1,12 @@
{% extends 'index.html' %}
{% block content %}
<ul>
{% for torrent in torrents %}
<li>
<span>{{ torrent.name }}</span>
<span>{{ torrent.format_eta() }}</span>
</li>
{% endfor %}
</ul>
{% endblock %}

23
templates/index.html Normal file
View File

@ -0,0 +1,23 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Search and download torrents directly to mediacenter</title>
<script src="https://unpkg.com/htmx.org@2.0.0" integrity="sha384-wS5l5IKJBvK6sPTKa2WZ1js3d947pvWXbPJ1OmWfEuxLgeHcEbjUUA5i9V5ZkpCw" crossorigin="anonymous"></script>
</head>
<body>
<div id="content">
<menu>
<li hx-get="/" hx-replace-url="/" hx-target="#content">Search</li>
<li hx-get="/active" hx-replace-url="/active" hx-target="#content">Active downloads</li>
</menu>
{% block content %}
<form hx-get="/list" hx-replace-url="true" hx-target="#content">
<input type=text name=query />
<button type=submit>search</button>
</form>
{% endblock %}
</div>
</body>
</html>

15
templates/torrents.html Normal file
View File

@ -0,0 +1,15 @@
{% extends 'index.html' %}
{% block content %}
<ul>
{% for torrent in torrents %}
<li hx-get="/download?{{ torrent.magnetlink | safe }}" hx-target="#content" hx-replace-url="/active">
<span>{{ torrent.title }}</span>
<span>({{ torrent.seeds }})</span>
<span>({{ torrent.leeches }})</span>
<span>({{ torrent.filesize }})</span>
<span>{{ torrent.category }}</span>
</li>
{% endfor %}
</ul>
{% endblock %}