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