torrra 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,28 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - 'v*'
7
+
8
+ jobs:
9
+ release:
10
+ name: Publish to PyPI
11
+ runs-on: ubuntu-latest
12
+ environment: pypi
13
+ permissions:
14
+ id-token: write
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+
18
+ - name: Install uv and setup the python version
19
+ uses: astral-sh/setup-uv@v5
20
+
21
+ - name: Install the project
22
+ run: uv sync --all-groups
23
+
24
+ - name: Build wheel
25
+ run: uv build
26
+
27
+ - name: Publish package
28
+ run: uv publish
@@ -0,0 +1,10 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
@@ -0,0 +1,7 @@
1
+ repos:
2
+ - repo: https://github.com/astral-sh/uv-pre-commit
3
+ # uv version.
4
+ rev: 0.7.19
5
+ hooks:
6
+ - id: uv-lock
7
+ - id: uv-export
@@ -0,0 +1 @@
1
+ 3.13
torrra-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,105 @@
1
+ Metadata-Version: 2.4
2
+ Name: torrra
3
+ Version: 0.1.0
4
+ Summary: CLI-based torrent search and downloader
5
+ Author: stabldev
6
+ Requires-Python: >=3.13
7
+ Requires-Dist: httpx>=0.28.1
8
+ Requires-Dist: questionary>=2.1.0
9
+ Requires-Dist: rich>=14.0.0
10
+ Requires-Dist: selectolax>=0.3.31
11
+ Description-Content-Type: text/markdown
12
+
13
+ # torrra 🎯
14
+
15
+ > CLI-based torrent search and downloader
16
+
17
+ `torrra` is a sleek and minimal command-line tool to search torrents from various indexers and download them- all from your terminal.
18
+
19
+ ## ✨ Features
20
+
21
+ - Search torrents from multiple indexers (`YTS` supported; more coming)
22
+ - Fetch magnet links directly
23
+ - Download torrents via libtorrent
24
+ - Pretty CLI with Rich-powered progress bars
25
+ - Modular and easily extensible indexer architecture
26
+
27
+ ## 📦 Installation
28
+
29
+ > Requires Python 3.11+ and [libtorrent-rasterbar](https://libtorrent.org/).
30
+
31
+ ### 1. Install `libtorrent`
32
+
33
+ ```bash
34
+ # Linux (Arch)
35
+ sudo pacman -S libtorrent-rasterbar
36
+ # Linux (Debian/Ubuntu)
37
+ sudo apt install python3-libtorrent
38
+
39
+ # macOS (Homebrew)
40
+ brew install libtorrent-rasterbar
41
+
42
+ # Windows/macOS (Python wheels)
43
+ pip install python-libtorrent
44
+ ```
45
+
46
+ > Note: For Windows users, the `python-libtorrent` wheel includes all dependencies and is the recommended method.
47
+
48
+ ### 2. Install `torrra`
49
+ ```bash
50
+ pip install torrra
51
+ ```
52
+
53
+ ### OR: compile from source (advanced)
54
+
55
+ See libtorrent's build guide:
56
+ https://libtorrent.org/building.html
57
+
58
+ For local development, fork the repo, then:
59
+
60
+ ```bash
61
+ git clone https://github.com/yourusername/torrra
62
+ cd torrra
63
+ uv sync
64
+ ```
65
+
66
+ ## 💡 Usage
67
+
68
+ ```bash
69
+ torrra
70
+ ```
71
+
72
+ Follow the prompts:
73
+
74
+ 1. Enter search query
75
+ 2. Pick an indexer
76
+ 3. Select a torrent
77
+ 4. Choose quality / magnet link
78
+ 5. Pick a download folder
79
+
80
+ ## 🧩 Indexer Support
81
+
82
+ Currently supported:
83
+
84
+ - YTS (movies)
85
+
86
+ Planned:
87
+
88
+ - 1337x
89
+ - GLodls
90
+ - Nyaa (anime-only, optional)
91
+ - Community-driven additions
92
+
93
+ ## 🛠️ Dev Notes
94
+
95
+ - Modular indexer structure (`torrra/indexers`)
96
+ - Extend `BaseIndexer` to add new sources
97
+ - Built using:
98
+ - `httpx` + `selectolax` for scraping
99
+ - `libtorrent` for torrenting
100
+ - `rich` for CLI visuals
101
+ - `questionary` for input prompts
102
+
103
+ ## 📜 License
104
+
105
+ MIT © [stabldev](https://github.com/stabldev)
torrra-0.1.0/README.md ADDED
@@ -0,0 +1,93 @@
1
+ # torrra 🎯
2
+
3
+ > CLI-based torrent search and downloader
4
+
5
+ `torrra` is a sleek and minimal command-line tool to search torrents from various indexers and download them- all from your terminal.
6
+
7
+ ## ✨ Features
8
+
9
+ - Search torrents from multiple indexers (`YTS` supported; more coming)
10
+ - Fetch magnet links directly
11
+ - Download torrents via libtorrent
12
+ - Pretty CLI with Rich-powered progress bars
13
+ - Modular and easily extensible indexer architecture
14
+
15
+ ## 📦 Installation
16
+
17
+ > Requires Python 3.11+ and [libtorrent-rasterbar](https://libtorrent.org/).
18
+
19
+ ### 1. Install `libtorrent`
20
+
21
+ ```bash
22
+ # Linux (Arch)
23
+ sudo pacman -S libtorrent-rasterbar
24
+ # Linux (Debian/Ubuntu)
25
+ sudo apt install python3-libtorrent
26
+
27
+ # macOS (Homebrew)
28
+ brew install libtorrent-rasterbar
29
+
30
+ # Windows/macOS (Python wheels)
31
+ pip install python-libtorrent
32
+ ```
33
+
34
+ > Note: For Windows users, the `python-libtorrent` wheel includes all dependencies and is the recommended method.
35
+
36
+ ### 2. Install `torrra`
37
+ ```bash
38
+ pip install torrra
39
+ ```
40
+
41
+ ### OR: compile from source (advanced)
42
+
43
+ See libtorrent's build guide:
44
+ https://libtorrent.org/building.html
45
+
46
+ For local development, fork the repo, then:
47
+
48
+ ```bash
49
+ git clone https://github.com/yourusername/torrra
50
+ cd torrra
51
+ uv sync
52
+ ```
53
+
54
+ ## 💡 Usage
55
+
56
+ ```bash
57
+ torrra
58
+ ```
59
+
60
+ Follow the prompts:
61
+
62
+ 1. Enter search query
63
+ 2. Pick an indexer
64
+ 3. Select a torrent
65
+ 4. Choose quality / magnet link
66
+ 5. Pick a download folder
67
+
68
+ ## 🧩 Indexer Support
69
+
70
+ Currently supported:
71
+
72
+ - YTS (movies)
73
+
74
+ Planned:
75
+
76
+ - 1337x
77
+ - GLodls
78
+ - Nyaa (anime-only, optional)
79
+ - Community-driven additions
80
+
81
+ ## 🛠️ Dev Notes
82
+
83
+ - Modular indexer structure (`torrra/indexers`)
84
+ - Extend `BaseIndexer` to add new sources
85
+ - Built using:
86
+ - `httpx` + `selectolax` for scraping
87
+ - `libtorrent` for torrenting
88
+ - `rich` for CLI visuals
89
+ - `questionary` for input prompts
90
+
91
+ ## 📜 License
92
+
93
+ MIT © [stabldev](https://github.com/stabldev)
@@ -0,0 +1,25 @@
1
+ [project]
2
+ name = "torrra"
3
+ version = "0.1.0"
4
+ description = "CLI-based torrent search and downloader"
5
+ readme = "README.md"
6
+ authors = [{ name = "stabldev" }]
7
+ requires-python = ">=3.13"
8
+ dependencies = [
9
+ "httpx>=0.28.1",
10
+ "questionary>=2.1.0",
11
+ "rich>=14.0.0",
12
+ "selectolax>=0.3.31",
13
+ ]
14
+
15
+ [project.scripts]
16
+ torrra = "torrra:main"
17
+
18
+ [build-system]
19
+ requires = ["hatchling"]
20
+ build-backend = "hatchling.build"
21
+
22
+ [dependency-groups]
23
+ dev = [
24
+ "libtorrent>=2.0.11",
25
+ ]
@@ -0,0 +1,82 @@
1
+ # This file was autogenerated by uv via the following command:
2
+ # uv export --frozen --output-file=requirements.txt
3
+ -e .
4
+ anyio==4.9.0 \
5
+ --hash=sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028 \
6
+ --hash=sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c
7
+ # via httpx
8
+ certifi==2025.6.15 \
9
+ --hash=sha256:2e0c7ce7cb5d8f8634ca55d2ba7e6ec2689a2fd6537d8dec1296a477a4910057 \
10
+ --hash=sha256:d747aa5a8b9bbbb1bb8c22bb13e22bd1f18e9796defa16bab421f7f7a317323b
11
+ # via
12
+ # httpcore
13
+ # httpx
14
+ h11==0.16.0 \
15
+ --hash=sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1 \
16
+ --hash=sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86
17
+ # via httpcore
18
+ httpcore==1.0.9 \
19
+ --hash=sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55 \
20
+ --hash=sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8
21
+ # via httpx
22
+ httpx==0.28.1 \
23
+ --hash=sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc \
24
+ --hash=sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad
25
+ # via torrra
26
+ idna==3.10 \
27
+ --hash=sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9 \
28
+ --hash=sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3
29
+ # via
30
+ # anyio
31
+ # httpx
32
+ libtorrent==2.0.11 \
33
+ --hash=sha256:474e792c52319cc918a007aa61cb34fb7f9236fa6c9603953e8c0fb3e6d922c7 \
34
+ --hash=sha256:51daa58d0958ac99d88a1749d6848c6023ea899da98e503b12a1a311189d79fd \
35
+ --hash=sha256:55aa67cdc8cbf777e41998d5432a3ab01f198c955ec6d2fb14c500b309db3f37 \
36
+ --hash=sha256:63fcb829d2b9fed566771426c7b1473a4acb7ccb4957ef07793b28de8468cd3e \
37
+ --hash=sha256:758662c88a186c6018e4062d1b1b5e58ed55e17189ad4a7389c87b8b2b5efeb4 \
38
+ --hash=sha256:8be0e27f40594bfee42e60e7785261834fdaf938267dceee8a08eae08a12a3c2 \
39
+ --hash=sha256:e234f665c987262d4102f6d2e099d76b12dfa20bcc77b77891184b075ab84ea8 \
40
+ --hash=sha256:f4489732bbc44114c7fb1893b71c578052d1aca352852002938b23ac8a6fb5c3 \
41
+ --hash=sha256:f72f0075b84aa18d25ee3ab3c39ba5193777d34d016c9a38294c92ffc60f6225
42
+ markdown-it-py==3.0.0 \
43
+ --hash=sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1 \
44
+ --hash=sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb
45
+ # via rich
46
+ mdurl==0.1.2 \
47
+ --hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \
48
+ --hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba
49
+ # via markdown-it-py
50
+ prompt-toolkit==3.0.51 \
51
+ --hash=sha256:52742911fde84e2d423e2f9a4cf1de7d7ac4e51958f648d9540e0fb8db077b07 \
52
+ --hash=sha256:931a162e3b27fc90c86f1b48bb1fb2c528c2761475e57c9c06de13311c7b54ed
53
+ # via questionary
54
+ pygments==2.19.2 \
55
+ --hash=sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887 \
56
+ --hash=sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b
57
+ # via rich
58
+ questionary==2.1.0 \
59
+ --hash=sha256:44174d237b68bc828e4878c763a9ad6790ee61990e0ae72927694ead57bab8ec \
60
+ --hash=sha256:6302cdd645b19667d8f6e6634774e9538bfcd1aad9be287e743d96cacaf95587
61
+ # via torrra
62
+ rich==14.0.0 \
63
+ --hash=sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0 \
64
+ --hash=sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725
65
+ # via torrra
66
+ selectolax==0.3.31 \
67
+ --hash=sha256:0f3b32d50c663b27d5b59c303c1aef1b4a3ae654b5e6921e17f25ed6f09a6d95 \
68
+ --hash=sha256:1427c2c7d83e8ca04fde77cae3222ab3f97b9f98db1ada06ceb93682b3cf202d \
69
+ --hash=sha256:68fdf557b6ebbb55f28b3747f29591694d12eec16f56cb8a5f8f4fab04b4fa0a \
70
+ --hash=sha256:90b63149e1e44e4b8fcd06669f30a7c68062bfb264d8c621d03fdc4850e6e5c9 \
71
+ --hash=sha256:c8937b21aef5c752b03368a88c6c889bc1557299926194cf248ed16f64617e6c \
72
+ --hash=sha256:e1985c95c91c1244d99ce5ec3284a1256a2da75232cdfde6932f3bdb469a7a8d \
73
+ --hash=sha256:ee1414a76142c3474f26df0140cda1edb92cc5f44d995de88bcf48982dc6c986
74
+ # via torrra
75
+ sniffio==1.3.1 \
76
+ --hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \
77
+ --hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc
78
+ # via anyio
79
+ wcwidth==0.2.13 \
80
+ --hash=sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859 \
81
+ --hash=sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5
82
+ # via prompt-toolkit
@@ -0,0 +1,3 @@
1
+ from torrra.cli.main import main
2
+
3
+ __all__ = ["main"]
File without changes
@@ -0,0 +1,69 @@
1
+ import importlib
2
+ from prompt_toolkit.shortcuts import CompleteStyle
3
+ import questionary
4
+ from questionary import Choice
5
+ from typing import List
6
+ from rich.console import Console
7
+
8
+ from torrra.downloader import download_magnet
9
+ from torrra.indexers import INDEXERS
10
+ from torrra.types import Magnet, Torrent
11
+
12
+ console = Console()
13
+
14
+
15
+ def main() -> None:
16
+ query = questionary.text("Search:").ask()
17
+
18
+ if not query:
19
+ console.print("[red]No query entered. Exiting...[/red]")
20
+ return
21
+
22
+ indexer_name = questionary.select(
23
+ "Choose an indexer:", choices=list(INDEXERS.keys())
24
+ ).ask()
25
+ indexer_module_path = INDEXERS[indexer_name]
26
+ indexer = importlib.import_module(indexer_module_path).Indexer()
27
+
28
+ with console.status(
29
+ f"[bold green]Searching {indexer_name} for '{query}'...[/bold green]"
30
+ ):
31
+ torrents: List[Torrent] = indexer.search(query)
32
+
33
+ if not torrents:
34
+ console.print("[yellow]Could not find any torrents. Exiting...[/yellow]")
35
+ return
36
+
37
+ torrent_choices = [
38
+ Choice(title=torrent.title, value=torrent) for torrent in torrents
39
+ ]
40
+ selected_torrent: Torrent | None = questionary.select(
41
+ "Select a torrent:", choices=torrent_choices
42
+ ).ask()
43
+
44
+ if not selected_torrent:
45
+ console.print("[yellow]No torrent selected. Exiting...[/yellow]")
46
+ return
47
+
48
+ with console.status(
49
+ f"[bold green]Fetching magnet links for '{selected_torrent.title}'...[/bold green]"
50
+ ):
51
+ magnets: List[Magnet] = indexer.get_magnets(selected_torrent.link)
52
+
53
+ if not magnets:
54
+ console.print("[yellow]No magnet links found. Exiting...[/yellow]")
55
+ return
56
+
57
+ magnet_choices = [Choice(title=magnet.title, value=magnet) for magnet in magnets]
58
+ selected_magnet: Magnet | None = questionary.select(
59
+ "Select:", choices=magnet_choices
60
+ ).ask()
61
+
62
+ if not selected_magnet:
63
+ console.print("[yellow]No magnet selected. Exiting...[/yellow]")
64
+ return
65
+
66
+ save_path = questionary.path(
67
+ "Download path:", only_directories=True, complete_style=CompleteStyle.COLUMN
68
+ ).ask()
69
+ download_magnet(selected_magnet.magnet_uri, save_path)
@@ -0,0 +1,77 @@
1
+ import libtorrent as lt
2
+ import time
3
+ from rich.progress import (
4
+ Progress,
5
+ BarColumn,
6
+ TransferSpeedColumn,
7
+ TimeRemainingColumn,
8
+ TextColumn,
9
+ TaskProgressColumn,
10
+ DownloadColumn,
11
+ )
12
+ from rich.console import Console
13
+
14
+ console = Console()
15
+
16
+
17
+ def download_magnet(magnet_uri: str, save_path: str):
18
+ ses = lt.session()
19
+ ses.listen_on(6881, 6891)
20
+
21
+ params = {
22
+ "save_path": save_path,
23
+ "storage_mode": lt.storage_mode_t(2),
24
+ }
25
+
26
+ handle = lt.add_magnet_uri(ses, magnet_uri, params)
27
+
28
+ console.print("[bold green]Downloading metadata...[/bold green]")
29
+ while not handle.has_metadata():
30
+ time.sleep(0.5)
31
+
32
+ torrent_info = handle.get_torrent_info()
33
+ total_size = torrent_info.total_size()
34
+ file_count = torrent_info.num_files()
35
+
36
+ console.print(
37
+ f"[bold green]Starting download: [cyan]{handle.name()}[/cyan][/bold green]"
38
+ )
39
+ console.print(
40
+ f"[yellow]Size:[/yellow] {total_size/(1024*1024):.2f} MB | [yellow]Files:[/yellow] {file_count}"
41
+ )
42
+
43
+ with Progress(
44
+ TextColumn("[bold blue]{task.fields[filename]}"),
45
+ BarColumn(),
46
+ TaskProgressColumn(),
47
+ DownloadColumn(),
48
+ TransferSpeedColumn(),
49
+ TimeRemainingColumn(),
50
+ console=console,
51
+ ) as progress:
52
+ task = progress.add_task(
53
+ "download", filename=handle.name(), peers=0, total=total_size
54
+ )
55
+
56
+ prev_downloaded = 0
57
+ prev_time = time.time()
58
+
59
+ while not handle.is_seed():
60
+ s = handle.status()
61
+
62
+ # calculate real-time download speed
63
+ cur_time = time.time()
64
+ downloaded = s.total_done
65
+ elapsed = cur_time - prev_time
66
+ speed = (downloaded - prev_downloaded) / elapsed if elapsed > 0 else 0
67
+
68
+ progress.update(task, completed=downloaded, speed=speed)
69
+
70
+ prev_downloaded = downloaded
71
+ prev_time = cur_time
72
+ time.sleep(0.5)
73
+
74
+ console.print(
75
+ f"\n[bold green]✓ Download complete: [cyan]{handle.name()}[/cyan][/bold green]"
76
+ )
77
+ console.print(f"[green]Saved to: [underline]{save_path}[/underline][/green]")
@@ -0,0 +1 @@
1
+ INDEXERS = {"yts.mx": "torrra.indexers.yts_mx"}
@@ -0,0 +1,20 @@
1
+ from typing import List
2
+ from selectolax.parser import HTMLParser
3
+
4
+ from torrra.types import Magnet, Torrent
5
+ from torrra.utils.html import parse_html
6
+ from abc import ABC, abstractmethod
7
+
8
+
9
+ class BaseIndexer(ABC):
10
+ def _get_parser(self, url: str) -> HTMLParser:
11
+ html = parse_html(url)
12
+ return HTMLParser(html)
13
+
14
+ @abstractmethod
15
+ def search(self, query: str) -> List[Torrent]:
16
+ pass
17
+
18
+ @abstractmethod
19
+ def get_magnets(self, link: str) -> List[Magnet]:
20
+ pass
@@ -0,0 +1,56 @@
1
+ from typing import List
2
+
3
+ from torrra.indexers.base import BaseIndexer
4
+ from torrra.types import Magnet, Torrent
5
+
6
+
7
+ class Indexer(BaseIndexer):
8
+ def search(self, query: str) -> List[Torrent]:
9
+ url = f"https://yts.mx/browse-movies/{query}/all/all/0/latest/0/all"
10
+ parser = self._get_parser(url)
11
+ res = []
12
+
13
+ nodes = parser.css(
14
+ "div.browse-content div.browse-movie-wrap div.browse-movie-bottom"
15
+ )
16
+ for node in nodes:
17
+ title_node = node.css_first("a.browse-movie-title")
18
+ year_node = node.css_first("div.browse-movie-year")
19
+
20
+ title = title_node.text(strip=True) if title_node else ""
21
+ link = title_node.attributes.get("href") if title_node else ""
22
+
23
+ if query not in title.lower() or link is None:
24
+ continue
25
+
26
+ year = year_node.text(strip=True) if year_node else ""
27
+
28
+ torrent_title = f"{title} {year}".strip()
29
+ res.append(Torrent(title=torrent_title, link=link))
30
+ return res
31
+
32
+ def get_magnets(self, link: str) -> List[Magnet]:
33
+ parser = self._get_parser(link)
34
+ res = []
35
+
36
+ nodes = parser.css("div.modal div.modal-torrent")
37
+ for node in nodes:
38
+ resolution_node = node.css_first("div.modal-quality span")
39
+ resolution = resolution_node.text(strip=True) if resolution_node else ""
40
+
41
+ quality_info = resolution
42
+
43
+ quality_size_nodes = node.css("p.quality-size")
44
+ if len(quality_size_nodes) >= 2:
45
+ source = quality_size_nodes[0].text(strip=True)
46
+ size = quality_size_nodes[1].text(strip=True)
47
+ quality_info = f"{source} {resolution} {size}"
48
+
49
+ magnet_node = node.css_first("a.magnet")
50
+ magnet_uri = magnet_node.attributes.get("href") if magnet_node else ""
51
+
52
+ if not magnet_uri:
53
+ continue
54
+
55
+ res.append(Magnet(title=quality_info, magnet_uri=magnet_uri))
56
+ return res
@@ -0,0 +1,13 @@
1
+ from dataclasses import dataclass
2
+
3
+
4
+ @dataclass
5
+ class Torrent:
6
+ title: str
7
+ link: str
8
+
9
+
10
+ @dataclass
11
+ class Magnet:
12
+ title: str
13
+ magnet_uri: str
File without changes
@@ -0,0 +1,8 @@
1
+ import httpx
2
+
3
+
4
+ def parse_html(url: str) -> str:
5
+ res = httpx.get(url, headers={"User-Agent": "Mozilla/5.0"})
6
+ html = res.text
7
+
8
+ return html
torrra-0.1.0/uv.lock ADDED
@@ -0,0 +1,213 @@
1
+ version = 1
2
+ revision = 2
3
+ requires-python = ">=3.13"
4
+
5
+ [[package]]
6
+ name = "anyio"
7
+ version = "4.9.0"
8
+ source = { registry = "https://pypi.org/simple" }
9
+ dependencies = [
10
+ { name = "idna" },
11
+ { name = "sniffio" },
12
+ ]
13
+ sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949, upload-time = "2025-03-17T00:02:54.77Z" }
14
+ wheels = [
15
+ { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916, upload-time = "2025-03-17T00:02:52.713Z" },
16
+ ]
17
+
18
+ [[package]]
19
+ name = "certifi"
20
+ version = "2025.6.15"
21
+ source = { registry = "https://pypi.org/simple" }
22
+ sdist = { url = "https://files.pythonhosted.org/packages/73/f7/f14b46d4bcd21092d7d3ccef689615220d8a08fb25e564b65d20738e672e/certifi-2025.6.15.tar.gz", hash = "sha256:d747aa5a8b9bbbb1bb8c22bb13e22bd1f18e9796defa16bab421f7f7a317323b", size = 158753, upload-time = "2025-06-15T02:45:51.329Z" }
23
+ wheels = [
24
+ { url = "https://files.pythonhosted.org/packages/84/ae/320161bd181fc06471eed047ecce67b693fd7515b16d495d8932db763426/certifi-2025.6.15-py3-none-any.whl", hash = "sha256:2e0c7ce7cb5d8f8634ca55d2ba7e6ec2689a2fd6537d8dec1296a477a4910057", size = 157650, upload-time = "2025-06-15T02:45:49.977Z" },
25
+ ]
26
+
27
+ [[package]]
28
+ name = "h11"
29
+ version = "0.16.0"
30
+ source = { registry = "https://pypi.org/simple" }
31
+ sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
32
+ wheels = [
33
+ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
34
+ ]
35
+
36
+ [[package]]
37
+ name = "httpcore"
38
+ version = "1.0.9"
39
+ source = { registry = "https://pypi.org/simple" }
40
+ dependencies = [
41
+ { name = "certifi" },
42
+ { name = "h11" },
43
+ ]
44
+ sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
45
+ wheels = [
46
+ { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
47
+ ]
48
+
49
+ [[package]]
50
+ name = "httpx"
51
+ version = "0.28.1"
52
+ source = { registry = "https://pypi.org/simple" }
53
+ dependencies = [
54
+ { name = "anyio" },
55
+ { name = "certifi" },
56
+ { name = "httpcore" },
57
+ { name = "idna" },
58
+ ]
59
+ sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
60
+ wheels = [
61
+ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
62
+ ]
63
+
64
+ [[package]]
65
+ name = "idna"
66
+ version = "3.10"
67
+ source = { registry = "https://pypi.org/simple" }
68
+ sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" }
69
+ wheels = [
70
+ { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" },
71
+ ]
72
+
73
+ [[package]]
74
+ name = "libtorrent"
75
+ version = "2.0.11"
76
+ source = { registry = "https://pypi.org/simple" }
77
+ wheels = [
78
+ { url = "https://files.pythonhosted.org/packages/92/74/b689c070c1649c69b6f70c27cd9f3418fb3cba7532b6b5f857c1689edcdc/libtorrent-2.0.11-cp313-cp313-macosx_13_0_x86_64.whl", hash = "sha256:f4489732bbc44114c7fb1893b71c578052d1aca352852002938b23ac8a6fb5c3", size = 5555119, upload-time = "2025-01-29T11:34:57.644Z" },
79
+ { url = "https://files.pythonhosted.org/packages/c1/b8/24e072c2270fc0193332987f1c3082a1cc8276bc462167b3e45fa0bb9a07/libtorrent-2.0.11-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:758662c88a186c6018e4062d1b1b5e58ed55e17189ad4a7389c87b8b2b5efeb4", size = 5654645, upload-time = "2025-01-29T11:35:00.79Z" },
80
+ { url = "https://files.pythonhosted.org/packages/07/62/5e3bbebfcf19c963f3f960b8a28ad2bf8cad1e56d8dce3ff9387084ef705/libtorrent-2.0.11-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:55aa67cdc8cbf777e41998d5432a3ab01f198c955ec6d2fb14c500b309db3f37", size = 5653493, upload-time = "2025-01-29T11:35:03.139Z" },
81
+ { url = "https://files.pythonhosted.org/packages/5c/85/e067ca5544e79525b80a7a986cb58e28977f274c4f341d1294c0ea35d7c7/libtorrent-2.0.11-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:474e792c52319cc918a007aa61cb34fb7f9236fa6c9603953e8c0fb3e6d922c7", size = 8251482, upload-time = "2025-01-29T11:35:05.522Z" },
82
+ { url = "https://files.pythonhosted.org/packages/a4/4c/86d834a8bbdd34276e4d87400d105ddab1dd4b8938f2c1229accc03a5b0d/libtorrent-2.0.11-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e234f665c987262d4102f6d2e099d76b12dfa20bcc77b77891184b075ab84ea8", size = 8514771, upload-time = "2025-01-29T11:35:09.604Z" },
83
+ { url = "https://files.pythonhosted.org/packages/f6/7e/e369802ae56bad9de629d9ad7c719dc4328aecf9aac8f61d33755d20b269/libtorrent-2.0.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8be0e27f40594bfee42e60e7785261834fdaf938267dceee8a08eae08a12a3c2", size = 12076824, upload-time = "2025-01-29T11:35:12.773Z" },
84
+ { url = "https://files.pythonhosted.org/packages/51/c6/b78e0f1515e65ca1a22383124cd867a1c9eee40053e7971064dd937c4802/libtorrent-2.0.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51daa58d0958ac99d88a1749d6848c6023ea899da98e503b12a1a311189d79fd", size = 12065489, upload-time = "2025-01-29T11:35:16.533Z" },
85
+ { url = "https://files.pythonhosted.org/packages/db/4a/cac02c0d907ec37f22131207f69e78a97045d8e8cab6c6b0a29647dbf732/libtorrent-2.0.11-cp313-cp313-win32.whl", hash = "sha256:63fcb829d2b9fed566771426c7b1473a4acb7ccb4957ef07793b28de8468cd3e", size = 1772278, upload-time = "2025-01-29T11:35:19.333Z" },
86
+ { url = "https://files.pythonhosted.org/packages/3d/7b/49ebb7df766398d49d59335481dafd7f4a3e8669a1aee5a6d22f9338b09c/libtorrent-2.0.11-cp313-cp313-win_amd64.whl", hash = "sha256:f72f0075b84aa18d25ee3ab3c39ba5193777d34d016c9a38294c92ffc60f6225", size = 1973890, upload-time = "2025-01-29T11:35:22.157Z" },
87
+ ]
88
+
89
+ [[package]]
90
+ name = "markdown-it-py"
91
+ version = "3.0.0"
92
+ source = { registry = "https://pypi.org/simple" }
93
+ dependencies = [
94
+ { name = "mdurl" },
95
+ ]
96
+ sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" }
97
+ wheels = [
98
+ { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" },
99
+ ]
100
+
101
+ [[package]]
102
+ name = "mdurl"
103
+ version = "0.1.2"
104
+ source = { registry = "https://pypi.org/simple" }
105
+ sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
106
+ wheels = [
107
+ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
108
+ ]
109
+
110
+ [[package]]
111
+ name = "prompt-toolkit"
112
+ version = "3.0.51"
113
+ source = { registry = "https://pypi.org/simple" }
114
+ dependencies = [
115
+ { name = "wcwidth" },
116
+ ]
117
+ sdist = { url = "https://files.pythonhosted.org/packages/bb/6e/9d084c929dfe9e3bfe0c6a47e31f78a25c54627d64a66e884a8bf5474f1c/prompt_toolkit-3.0.51.tar.gz", hash = "sha256:931a162e3b27fc90c86f1b48bb1fb2c528c2761475e57c9c06de13311c7b54ed", size = 428940, upload-time = "2025-04-15T09:18:47.731Z" }
118
+ wheels = [
119
+ { url = "https://files.pythonhosted.org/packages/ce/4f/5249960887b1fbe561d9ff265496d170b55a735b76724f10ef19f9e40716/prompt_toolkit-3.0.51-py3-none-any.whl", hash = "sha256:52742911fde84e2d423e2f9a4cf1de7d7ac4e51958f648d9540e0fb8db077b07", size = 387810, upload-time = "2025-04-15T09:18:44.753Z" },
120
+ ]
121
+
122
+ [[package]]
123
+ name = "pygments"
124
+ version = "2.19.2"
125
+ source = { registry = "https://pypi.org/simple" }
126
+ sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
127
+ wheels = [
128
+ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
129
+ ]
130
+
131
+ [[package]]
132
+ name = "questionary"
133
+ version = "2.1.0"
134
+ source = { registry = "https://pypi.org/simple" }
135
+ dependencies = [
136
+ { name = "prompt-toolkit" },
137
+ ]
138
+ sdist = { url = "https://files.pythonhosted.org/packages/a8/b8/d16eb579277f3de9e56e5ad25280fab52fc5774117fb70362e8c2e016559/questionary-2.1.0.tar.gz", hash = "sha256:6302cdd645b19667d8f6e6634774e9538bfcd1aad9be287e743d96cacaf95587", size = 26775, upload-time = "2024-12-29T11:49:17.802Z" }
139
+ wheels = [
140
+ { url = "https://files.pythonhosted.org/packages/ad/3f/11dd4cd4f39e05128bfd20138faea57bec56f9ffba6185d276e3107ba5b2/questionary-2.1.0-py3-none-any.whl", hash = "sha256:44174d237b68bc828e4878c763a9ad6790ee61990e0ae72927694ead57bab8ec", size = 36747, upload-time = "2024-12-29T11:49:16.734Z" },
141
+ ]
142
+
143
+ [[package]]
144
+ name = "rich"
145
+ version = "14.0.0"
146
+ source = { registry = "https://pypi.org/simple" }
147
+ dependencies = [
148
+ { name = "markdown-it-py" },
149
+ { name = "pygments" },
150
+ ]
151
+ sdist = { url = "https://files.pythonhosted.org/packages/a1/53/830aa4c3066a8ab0ae9a9955976fb770fe9c6102117c8ec4ab3ea62d89e8/rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725", size = 224078, upload-time = "2025-03-30T14:15:14.23Z" }
152
+ wheels = [
153
+ { url = "https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", size = 243229, upload-time = "2025-03-30T14:15:12.283Z" },
154
+ ]
155
+
156
+ [[package]]
157
+ name = "selectolax"
158
+ version = "0.3.31"
159
+ source = { registry = "https://pypi.org/simple" }
160
+ sdist = { url = "https://files.pythonhosted.org/packages/14/38/27c125553ead2b003b8deb0e0d6a2a9c0fab8a33dee71dc492f710c2b911/selectolax-0.3.31.tar.gz", hash = "sha256:1427c2c7d83e8ca04fde77cae3222ab3f97b9f98db1ada06ceb93682b3cf202d", size = 4705062, upload-time = "2025-07-01T13:12:55.372Z" }
161
+ wheels = [
162
+ { url = "https://files.pythonhosted.org/packages/f5/4a/b57fb6d4d6f7dbdf46538fa8bbcdf23d038fa5e38f6b3d568bac4c29b59b/selectolax-0.3.31-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:68fdf557b6ebbb55f28b3747f29591694d12eec16f56cb8a5f8f4fab04b4fa0a", size = 2075492, upload-time = "2025-07-01T13:12:32.078Z" },
163
+ { url = "https://files.pythonhosted.org/packages/b2/1c/eafcfb255b3fa7eb0d4a563c789fc17540e89da303c3da642f906cc6718d/selectolax-0.3.31-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c8937b21aef5c752b03368a88c6c889bc1557299926194cf248ed16f64617e6c", size = 2069797, upload-time = "2025-07-01T13:12:33.696Z" },
164
+ { url = "https://files.pythonhosted.org/packages/b3/0b/75ebd89e4391bc959a0d66ae18c3be274fa3542cba7bf64c1d9c8f4370c4/selectolax-0.3.31-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee1414a76142c3474f26df0140cda1edb92cc5f44d995de88bcf48982dc6c986", size = 5746598, upload-time = "2025-07-01T13:12:35.496Z" },
165
+ { url = "https://files.pythonhosted.org/packages/da/d3/0d9ae2678597f2073507b2762ab9854c2cd2564912510618595be645f89e/selectolax-0.3.31-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90b63149e1e44e4b8fcd06669f30a7c68062bfb264d8c621d03fdc4850e6e5c9", size = 5781753, upload-time = "2025-07-01T13:12:37.118Z" },
166
+ { url = "https://files.pythonhosted.org/packages/53/49/b96bd5ccb0e3a22c968b1a4780396e68d69288187185dac5cec344eada65/selectolax-0.3.31-cp313-cp313-win_amd64.whl", hash = "sha256:0f3b32d50c663b27d5b59c303c1aef1b4a3ae654b5e6921e17f25ed6f09a6d95", size = 1789255, upload-time = "2025-07-01T13:12:38.607Z" },
167
+ { url = "https://files.pythonhosted.org/packages/f0/41/d94e2608bcbda96605d888ba02668c6d1fc56fd7ef8b39b4d0f0f235b283/selectolax-0.3.31-cp313-cp313-win_arm64.whl", hash = "sha256:e1985c95c91c1244d99ce5ec3284a1256a2da75232cdfde6932f3bdb469a7a8d", size = 1734157, upload-time = "2025-07-01T13:12:39.934Z" },
168
+ ]
169
+
170
+ [[package]]
171
+ name = "sniffio"
172
+ version = "1.3.1"
173
+ source = { registry = "https://pypi.org/simple" }
174
+ sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" }
175
+ wheels = [
176
+ { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
177
+ ]
178
+
179
+ [[package]]
180
+ name = "torrra"
181
+ version = "0.1.0"
182
+ source = { editable = "." }
183
+ dependencies = [
184
+ { name = "httpx" },
185
+ { name = "questionary" },
186
+ { name = "rich" },
187
+ { name = "selectolax" },
188
+ ]
189
+
190
+ [package.dev-dependencies]
191
+ dev = [
192
+ { name = "libtorrent" },
193
+ ]
194
+
195
+ [package.metadata]
196
+ requires-dist = [
197
+ { name = "httpx", specifier = ">=0.28.1" },
198
+ { name = "questionary", specifier = ">=2.1.0" },
199
+ { name = "rich", specifier = ">=14.0.0" },
200
+ { name = "selectolax", specifier = ">=0.3.31" },
201
+ ]
202
+
203
+ [package.metadata.requires-dev]
204
+ dev = [{ name = "libtorrent", specifier = ">=2.0.11" }]
205
+
206
+ [[package]]
207
+ name = "wcwidth"
208
+ version = "0.2.13"
209
+ source = { registry = "https://pypi.org/simple" }
210
+ sdist = { url = "https://files.pythonhosted.org/packages/6c/63/53559446a878410fc5a5974feb13d31d78d752eb18aeba59c7fef1af7598/wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5", size = 101301, upload-time = "2024-01-06T02:10:57.829Z" }
211
+ wheels = [
212
+ { url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166, upload-time = "2024-01-06T02:10:55.763Z" },
213
+ ]