everything works, needs makeup though
This commit is contained in:
parent
5130450355
commit
464683e9ce
14 changed files with 377 additions and 0 deletions
10
.gitignore
vendored
Normal file
10
.gitignore
vendored
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
# python generated files
|
||||||
|
__pycache__/
|
||||||
|
*.py[oc]
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
wheels/
|
||||||
|
*.egg-info
|
||||||
|
|
||||||
|
# venv
|
||||||
|
.venv
|
1
.python-version
Normal file
1
.python-version
Normal file
|
@ -0,0 +1 @@
|
||||||
|
3.12.5
|
3
README.md
Normal file
3
README.md
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
# torrent-downloader
|
||||||
|
|
||||||
|
Search TPB and download torrents with transmission \o/
|
50
pyproject.toml
Normal file
50
pyproject.toml
Normal 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
70
requirements-dev.lock
Normal 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
65
requirements.lock
Normal 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
|
2
src/torrent_downloader/__init__.py
Normal file
2
src/torrent_downloader/__init__.py
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
def hello() -> str:
|
||||||
|
return "Hello from torrent-downloader!"
|
3
src/torrent_downloader/__main__.py
Normal file
3
src/torrent_downloader/__main__.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from .client import main
|
||||||
|
|
||||||
|
main()
|
48
src/torrent_downloader/app.py
Normal file
48
src/torrent_downloader/app.py
Normal 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}
|
||||||
|
)
|
71
src/torrent_downloader/client.py
Normal file
71
src/torrent_downloader/client.py
Normal 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")
|
4
src/torrent_downloader/server.py
Normal file
4
src/torrent_downloader/server.py
Normal 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)
|
12
templates/active_torrents.html
Normal file
12
templates/active_torrents.html
Normal 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
23
templates/index.html
Normal 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
15
templates/torrents.html
Normal 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 %}
|
Loading…
Add table
Reference in a new issue