torrent-models 0.0.2__py3-none-any.whl

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,64 @@
1
+ from torrent_models.const import BLOCK_SIZE, DEFAULT_TORRENT_CREATOR, GiB, KiB, MiB, TiB, size
2
+ from torrent_models.types import (
3
+ FileItem,
4
+ FileTree,
5
+ FileTreeItem,
6
+ FileTreeType,
7
+ MerkleTree,
8
+ MerkleTreeShape,
9
+ PieceLayerItem,
10
+ PieceLayers,
11
+ PieceLayersType,
12
+ Pieces,
13
+ SHA1Hash,
14
+ SHA256Hash,
15
+ TorrentVersion,
16
+ V1PieceLength,
17
+ V2PieceLength,
18
+ )
19
+ from torrent_models.info import (
20
+ InfoDictHybrid,
21
+ InfoDictHybridCreate,
22
+ InfoDictRoot,
23
+ InfoDictV1,
24
+ InfoDictV1Create,
25
+ InfoDictV2,
26
+ InfoDictV2Create,
27
+ )
28
+ from torrent_models.torrent import Torrent, pprint
29
+ from torrent_models.create import TorrentCreate
30
+
31
+ __all__ = [
32
+ "BLOCK_SIZE",
33
+ "DEFAULT_TORRENT_CREATOR",
34
+ "GiB",
35
+ "KiB",
36
+ "MiB",
37
+ "TiB",
38
+ "FileItem",
39
+ "FileTree",
40
+ "FileTreeItem",
41
+ "FileTreeType",
42
+ "InfoDictHybrid",
43
+ "InfoDictHybridCreate",
44
+ "InfoDictRoot",
45
+ "InfoDictV1",
46
+ "InfoDictV1Create",
47
+ "InfoDictV2",
48
+ "InfoDictV2Create",
49
+ "MerkleTree",
50
+ "MerkleTreeShape",
51
+ "PieceLayerItem",
52
+ "PieceLayers",
53
+ "PieceLayersType",
54
+ "Pieces",
55
+ "SHA1Hash",
56
+ "SHA256Hash",
57
+ "Torrent",
58
+ "TorrentCreate",
59
+ "TorrentVersion",
60
+ "V1PieceLength",
61
+ "V2PieceLength",
62
+ "size",
63
+ "pprint",
64
+ ]
torrent_models/base.py ADDED
@@ -0,0 +1,7 @@
1
+ from pydantic import BaseModel, ConfigDict
2
+
3
+
4
+ class ConfiguredBase(BaseModel):
5
+ """Configured base model"""
6
+
7
+ model_config = ConfigDict(populate_by_name=True, extra="allow")
@@ -0,0 +1,12 @@
1
+ import warnings
2
+ from importlib.util import find_spec
3
+
4
+ __all__ = ["main"]
5
+
6
+ if find_spec("click"):
7
+
8
+ from torrent_models.cli.main import main
9
+ else:
10
+ warnings.warn(
11
+ "cli dependencies are not installed - install with torrent-models[cli]", stacklevel=2
12
+ )
@@ -0,0 +1,253 @@
1
+ from datetime import timedelta
2
+ from pathlib import Path
3
+ from time import time
4
+ from typing import Literal as L
5
+ from typing import cast
6
+
7
+ import click
8
+ import humanize
9
+ from rich import print
10
+ from rich.console import Group
11
+ from rich.pretty import Pretty
12
+ from rich.table import Table
13
+
14
+ from torrent_models import TorrentCreate
15
+ from torrent_models.compat import get_size
16
+ from torrent_models.const import DEFAULT_TORRENT_CREATOR
17
+ from torrent_models.create import list_files
18
+ from torrent_models.info import InfoDictHybrid, InfoDictHybridCreate, InfoDictV1, InfoDictV2
19
+ from torrent_models.torrent import Torrent
20
+ from torrent_models.types.common import TorrentVersion
21
+ from torrent_models.types.v1 import FileItem, V1PieceLength
22
+ from torrent_models.types.v2 import V2PieceLength
23
+
24
+
25
+ @click.group("torrent")
26
+ def main() -> None:
27
+ """
28
+ torrent-models CLI
29
+ """
30
+
31
+
32
+ @main.command("make")
33
+ @click.option(
34
+ "-p",
35
+ "--path",
36
+ required=True,
37
+ help="Path to a directory or file to create .torrent from",
38
+ type=click.Path(exists=True),
39
+ )
40
+ @click.option(
41
+ "-t",
42
+ "--tracker",
43
+ required=False,
44
+ default=None,
45
+ multiple=True,
46
+ help="Trackers to add to the torrent. can be used multiple times for multiple trackers. ",
47
+ )
48
+ @click.option(
49
+ "-s",
50
+ "--piece-size",
51
+ default=512 * (2**10),
52
+ help="Piece size, in bytes",
53
+ show_default=True,
54
+ )
55
+ @click.option(
56
+ "--comment",
57
+ default=None,
58
+ required=False,
59
+ help="Optional comment field for torrent",
60
+ )
61
+ @click.option(
62
+ "--creator",
63
+ default=DEFAULT_TORRENT_CREATOR,
64
+ show_default=True,
65
+ required=False,
66
+ help="Optional creator field for torrent",
67
+ )
68
+ @click.option(
69
+ "-w",
70
+ "--webseed",
71
+ required=False,
72
+ default=None,
73
+ multiple=True,
74
+ help="Add HTTP webseeds as additional sources for torrent. Can be used multiple times. "
75
+ "See https://www.bittorrent.org/beps/bep_0019.html",
76
+ )
77
+ @click.option(
78
+ "--similar",
79
+ required=False,
80
+ default=None,
81
+ multiple=True,
82
+ help="Add infohash of a similar torrent. "
83
+ "Similar torrents are torrents who have files in common with this torrent, "
84
+ "clients are able to reuse files from the other torrents if they already have them downloaded.",
85
+ )
86
+ @click.option(
87
+ "--version",
88
+ default="hybrid",
89
+ type=click.Choice(["v1", "v2", "hybrid"]),
90
+ help="What kind of torrent to create, default is hybrid",
91
+ )
92
+ @click.option("--progress/--no-progress", default=True, help="Enable progress bar (default True)")
93
+ @click.option(
94
+ "-o",
95
+ "--output",
96
+ required=False,
97
+ default=None,
98
+ type=click.Path(exists=False),
99
+ help=".torrent file to write to. Otherwise to stdout",
100
+ )
101
+ def make(
102
+ path: Path,
103
+ tracker: list[str] | tuple[str] | None = None,
104
+ piece_size: V1PieceLength | V2PieceLength = 512 * (2**10),
105
+ comment: str | None = None,
106
+ creator: str = DEFAULT_TORRENT_CREATOR,
107
+ webseed: list[str] | None = None,
108
+ similar: list[str] | None = None,
109
+ version: L["v1", "v2", "hybrid"] = "hybrid",
110
+ progress: bool = True,
111
+ output: Path | None = None,
112
+ ) -> None:
113
+ path = Path(path)
114
+ files = list_files(path)
115
+ start_time = time()
116
+ created = TorrentCreate(
117
+ trackers=tracker,
118
+ files=files,
119
+ path_root=path,
120
+ comment=comment,
121
+ creator=creator,
122
+ info=InfoDictHybridCreate(piece_length=piece_size, name=path.name),
123
+ )
124
+ generated = created.generate(version=version, progress=progress)
125
+ bencoded = generated.bencode()
126
+ if output:
127
+ with open(output, "wb") as f:
128
+ f.write(bencoded)
129
+
130
+ torrent_size = get_size(Path(output))
131
+
132
+ end_time = time()
133
+ duration = end_time - start_time
134
+ total_size = generated.info.total_length
135
+ speed = total_size / duration
136
+ click.echo(
137
+ f"Created torrent {output}\n"
138
+ f"Total size: {humanize.naturalsize(total_size, binary=True)}\n"
139
+ f"Torrent size: {humanize.naturalsize(torrent_size, binary=True)}\n"
140
+ f"Duration: {humanize.naturaldelta(timedelta(seconds=duration))}\n"
141
+ f"Speed: {humanize.naturalsize(speed, binary=True)}/s"
142
+ )
143
+ else:
144
+ click.echo(bencoded)
145
+
146
+
147
+ @main.command("print")
148
+ @click.argument("torrent", type=click.Path(exists=True))
149
+ @click.option(
150
+ "-v",
151
+ "--verbose",
152
+ count=True,
153
+ help="""
154
+ Verbose output, repeat for increasing verbosity\n
155
+
156
+ -v show files in separate table
157
+ -vv show truncated v1 piece hashes
158
+ -vvv show everything as-is
159
+ """,
160
+ )
161
+ def pprint(torrent: Path, verbose: int = 0) -> None:
162
+ """
163
+ Print the contents of a torrent file.
164
+
165
+ By default, prints only the top-level metadata in a way that should always be
166
+ smaller than one screen.
167
+
168
+ Increase verbosity to show more of the torrent.
169
+
170
+ Hashes are printed as hexadecimal numbers and split into individual pieces,
171
+ but they are properly encoded in the torrent.
172
+ """
173
+ torrent = Path(torrent)
174
+ t = Torrent.read(torrent)
175
+ # summary stats
176
+ summary = {
177
+ "# Files": humanize.number.intcomma(t.n_files),
178
+ "Total Size": humanize.naturalsize(t.total_size, binary=True),
179
+ "Torrent Size": humanize.naturalsize(get_size(torrent), binary=True),
180
+ "Piece Size": humanize.naturalsize(t.info.piece_length, binary=True),
181
+ }
182
+ v1_infohash = t.v1_infohash
183
+ v2_infohash = t.v2_infohash
184
+ if v1_infohash:
185
+ summary["V1 Infohash"] = v1_infohash.hex()
186
+ if v2_infohash:
187
+ summary["V2 Infohash"] = v2_infohash.hex()
188
+ table = Table(title=str(torrent.name), show_header=False)
189
+ table.add_column("", justify="left", style="magenta bold", no_wrap=True)
190
+ table.add_column("")
191
+ for k, v in summary.items():
192
+ table.add_row(k, v)
193
+
194
+ exclude = {}
195
+ context = {"mode": "print", "hash_truncate": True}
196
+ file_table = None
197
+ if verbose <= 1:
198
+ exclude = {"info": {"pieces", "file tree", "file_tree", "files"}, "piece_layers": True}
199
+ elif verbose <= 2:
200
+ exclude = {"info": {"file tree", "file_tree", "files"}, "piece_layers": True}
201
+ else:
202
+ context["hash_truncate"] = False
203
+
204
+ # make file table
205
+ if 1 <= verbose <= 2:
206
+ file_table = Table(title="Files")
207
+ file_table.add_column("Path", no_wrap=True)
208
+ file_table.add_column("Size")
209
+
210
+ if t.torrent_version == TorrentVersion.v1:
211
+ t.info = cast(InfoDictV1, t.info)
212
+ tfiles = (
213
+ t.info.files
214
+ if t.info.files is not None
215
+ else [FileItem(path=t.info.name, length=t.info.length)]
216
+ )
217
+
218
+ files = [
219
+ ("/".join(f.path), humanize.naturalsize(f.length, binary=True), "")
220
+ for f in tfiles
221
+ if f.attr not in (b"p", "p")
222
+ ]
223
+ else:
224
+ t.info = cast(InfoDictV2 | InfoDictHybrid, t.info)
225
+ file_table.add_column("Hash")
226
+ tree = t.flat_files
227
+ assert tree is not None
228
+ files = [
229
+ (
230
+ str(k),
231
+ humanize.naturalsize(v["length"], binary=True),
232
+ v["pieces root"].hex()[0:8],
233
+ )
234
+ for k, v in tree.items()
235
+ ]
236
+
237
+ for f in files:
238
+ file_table.add_row(*f)
239
+
240
+ dumped = t.model_dump(
241
+ by_alias=True, exclude=exclude, exclude_none=True, context=context # type: ignore
242
+ )
243
+
244
+ if verbose < 1 or verbose > 2:
245
+ group = Group(
246
+ table,
247
+ Pretty(dumped),
248
+ )
249
+ elif verbose <= 2:
250
+ assert file_table is not None
251
+ group = Group(table, Pretty(dumped), file_table)
252
+
253
+ print(group)
@@ -0,0 +1,18 @@
1
+ import os.path
2
+ import sys
3
+ from pathlib import Path
4
+
5
+
6
+ def get_size(path: Path) -> int:
7
+ """
8
+ Windows, helpfully, reports different sizes to stat,
9
+ so we have to use the os.path.getsize() function instead.
10
+
11
+ This *may* not be necessary, but is left in place in case we need
12
+ compatibility for windows sizes in the future,
13
+ which can be impacted by the NTFS filesystem.
14
+ """
15
+ if sys.platform == "win32":
16
+ return os.path.getsize(path)
17
+ else:
18
+ return path.stat().st_size
@@ -0,0 +1,21 @@
1
+ from importlib.metadata import version
2
+
3
+ DEFAULT_TORRENT_CREATOR = f"torrent-models ({version('torrent-models')})"
4
+ EXCLUDE_FILES = (".DS_Store", "Thumbs.db")
5
+
6
+ KiB = 2**10
7
+ MiB = 2**20
8
+ GiB = 2**30
9
+ TiB = 2**40
10
+
11
+
12
+ class size:
13
+ """Convenience accessor/importer for sizes"""
14
+
15
+ KiB = KiB
16
+ MiB = MiB
17
+ GiB = GiB
18
+ TiB = TiB
19
+
20
+
21
+ BLOCK_SIZE = 16 * KiB